@mohasinac/appkit 4.62.1 → 4.63.1
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 +157 -0
- package/dist/features/ads/schemas/booking-write.js +88 -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-entry.d.ts +1 -1
- package/dist/server-entry.js +10 -1
- package/dist/server.d.ts +29 -1
- package/dist/server.js +158 -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
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Correcting a delivery address after the order was placed.
|
|
3
|
+
*
|
|
4
|
+
* ## 🛑 This APPENDS. It never touches the snapshot.
|
|
5
|
+
*
|
|
6
|
+
* `delivery*` on the order is what the buyer chose when they paid — it is
|
|
7
|
+
* what the invoice states and what a dispute argues over. A correction is a
|
|
8
|
+
* new fact about where the parcel should go, not a revision of what was
|
|
9
|
+
* agreed, and collapsing the two is how an invoice silently restates itself.
|
|
10
|
+
*
|
|
11
|
+
* `deliveryPartyFor` reads the latest amendment; `placeOfSupplyFrom` reads
|
|
12
|
+
* the snapshot. Tax is fixed at the time of supply.
|
|
13
|
+
*
|
|
14
|
+
* ## 🛑 There is a window, and it closes at dispatch
|
|
15
|
+
*
|
|
16
|
+
* Once a tracking id exists the parcel is with the courier and a new address
|
|
17
|
+
* changes nothing we control — accepting one would tell the buyer their
|
|
18
|
+
* correction took effect when it did not. That is a worse outcome than
|
|
19
|
+
* refusing, which at least routes them to support.
|
|
20
|
+
*/
|
|
21
|
+
import { type DeliveryAmendment, type OrderDocument } from "../../../../features/orders/schemas/firestore";
|
|
22
|
+
import { type HistoryActor } from "../../../shared/history";
|
|
23
|
+
/**
|
|
24
|
+
* How many corrections one order may carry.
|
|
25
|
+
*
|
|
26
|
+
* 🛑 Bounded for the same reason `statusHistory` is: this array rides on the
|
|
27
|
+
* order document, which has a 1 MiB ceiling and is read by every list query.
|
|
28
|
+
* Unlike `statusHistory` it does NOT evict — reaching the cap is refused,
|
|
29
|
+
* because dropping the oldest amendment would silently discard an address a
|
|
30
|
+
* parcel may already have gone to.
|
|
31
|
+
*
|
|
32
|
+
* Ten is far past any honest use. A buyer correcting an address eleven times
|
|
33
|
+
* is a support conversation, not a form submission.
|
|
34
|
+
*/
|
|
35
|
+
export declare const DELIVERY_AMENDMENT_MAX = 10;
|
|
36
|
+
export interface AmendDeliveryInput {
|
|
37
|
+
orderId: string;
|
|
38
|
+
/** An `addresses` row belonging to the actor. Never a loose address. */
|
|
39
|
+
addressId: string;
|
|
40
|
+
reason?: string;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* 🛑 The new address is a REFERENCE the caller must own, resolved here — not
|
|
44
|
+
* a free-form address object.
|
|
45
|
+
*
|
|
46
|
+
* Accepting fields directly would let a correction route a parcel anywhere
|
|
47
|
+
* with no record that the destination was ever the buyer's, which is the
|
|
48
|
+
* same hole `assertOwnedAddresses` closes for coordinated purchases. The
|
|
49
|
+
* resolved values are then COPIED into the amendment, because the row can be
|
|
50
|
+
* edited or deleted afterwards and a label must not depend on that.
|
|
51
|
+
*/
|
|
52
|
+
export declare function amendDeliveryAddress(input: AmendDeliveryInput, actor: HistoryActor & {
|
|
53
|
+
uid: string;
|
|
54
|
+
}): Promise<DeliveryAmendment>;
|
|
55
|
+
/** Plaintext amendments, for a surface that renders them. */
|
|
56
|
+
export declare function revealAmendments(order: OrderDocument): DeliveryAmendment[];
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Correcting a delivery address after the order was placed.
|
|
3
|
+
*
|
|
4
|
+
* ## 🛑 This APPENDS. It never touches the snapshot.
|
|
5
|
+
*
|
|
6
|
+
* `delivery*` on the order is what the buyer chose when they paid — it is
|
|
7
|
+
* what the invoice states and what a dispute argues over. A correction is a
|
|
8
|
+
* new fact about where the parcel should go, not a revision of what was
|
|
9
|
+
* agreed, and collapsing the two is how an invoice silently restates itself.
|
|
10
|
+
*
|
|
11
|
+
* `deliveryPartyFor` reads the latest amendment; `placeOfSupplyFrom` reads
|
|
12
|
+
* the snapshot. Tax is fixed at the time of supply.
|
|
13
|
+
*
|
|
14
|
+
* ## 🛑 There is a window, and it closes at dispatch
|
|
15
|
+
*
|
|
16
|
+
* Once a tracking id exists the parcel is with the courier and a new address
|
|
17
|
+
* changes nothing we control — accepting one would tell the buyer their
|
|
18
|
+
* correction took effect when it did not. That is a worse outcome than
|
|
19
|
+
* refusing, which at least routes them to support.
|
|
20
|
+
*/
|
|
21
|
+
import { addressesRepository } from "../../../../features/addresses/repository/addresses.repository.js";
|
|
22
|
+
import { orderRepository } from "../../../../features/orders/repository/orders.repository.js";
|
|
23
|
+
import { AMENDMENT_PII_FIELDS, } from "../../../../features/orders/schemas/firestore.js";
|
|
24
|
+
import { revealPii } from "../../../../security/pii-accessors.js";
|
|
25
|
+
import { ValidationError } from "../../../../errors/validation-error.js";
|
|
26
|
+
import { NotFoundError } from "../../../../errors/not-found-error.js";
|
|
27
|
+
import { HISTORY_ACTOR_ROLE } from "../../../shared/history/index.js";
|
|
28
|
+
/**
|
|
29
|
+
* How many corrections one order may carry.
|
|
30
|
+
*
|
|
31
|
+
* 🛑 Bounded for the same reason `statusHistory` is: this array rides on the
|
|
32
|
+
* order document, which has a 1 MiB ceiling and is read by every list query.
|
|
33
|
+
* Unlike `statusHistory` it does NOT evict — reaching the cap is refused,
|
|
34
|
+
* because dropping the oldest amendment would silently discard an address a
|
|
35
|
+
* parcel may already have gone to.
|
|
36
|
+
*
|
|
37
|
+
* Ten is far past any honest use. A buyer correcting an address eleven times
|
|
38
|
+
* is a support conversation, not a form submission.
|
|
39
|
+
*/
|
|
40
|
+
export const DELIVERY_AMENDMENT_MAX = 10;
|
|
41
|
+
/**
|
|
42
|
+
* 🛑 The new address is a REFERENCE the caller must own, resolved here — not
|
|
43
|
+
* a free-form address object.
|
|
44
|
+
*
|
|
45
|
+
* Accepting fields directly would let a correction route a parcel anywhere
|
|
46
|
+
* with no record that the destination was ever the buyer's, which is the
|
|
47
|
+
* same hole `assertOwnedAddresses` closes for coordinated purchases. The
|
|
48
|
+
* resolved values are then COPIED into the amendment, because the row can be
|
|
49
|
+
* edited or deleted afterwards and a label must not depend on that.
|
|
50
|
+
*/
|
|
51
|
+
export async function amendDeliveryAddress(input, actor) {
|
|
52
|
+
const order = (await orderRepository.findById(input.orderId));
|
|
53
|
+
if (!order)
|
|
54
|
+
throw new NotFoundError("Order not found.");
|
|
55
|
+
/*
|
|
56
|
+
* Ownership. A seller or admin may correct on a buyer's behalf — that is
|
|
57
|
+
* the support flow — but a BUYER may only touch their own order.
|
|
58
|
+
*/
|
|
59
|
+
if (actor.role === HISTORY_ACTOR_ROLE.BUYER && order.userId !== actor.uid) {
|
|
60
|
+
throw new NotFoundError("Order not found.");
|
|
61
|
+
}
|
|
62
|
+
if (order.trackingNumber) {
|
|
63
|
+
throw new ValidationError("This order has already been dispatched, so its address can no longer be changed.", { addressId: "Already dispatched — contact support." });
|
|
64
|
+
}
|
|
65
|
+
const existing = order.deliveryAmendments ?? [];
|
|
66
|
+
if (existing.length >= DELIVERY_AMENDMENT_MAX) {
|
|
67
|
+
throw new ValidationError("This order has been corrected too many times. Please contact support.", { addressId: `Limit of ${DELIVERY_AMENDMENT_MAX} corrections reached.` });
|
|
68
|
+
}
|
|
69
|
+
const addressDoc = await addressesRepository.findById(input.addressId);
|
|
70
|
+
/*
|
|
71
|
+
* Owned by the ORDER'S BUYER, not by the actor. A support agent correcting
|
|
72
|
+
* on someone's behalf must still pick from that buyer's own addresses —
|
|
73
|
+
* checking the actor would let staff route a parcel to their own address.
|
|
74
|
+
*/
|
|
75
|
+
if (!addressDoc || addressDoc.ownerType !== "user" || addressDoc.ownerId !== order.userId) {
|
|
76
|
+
throw new ValidationError("That delivery address is not available.", {
|
|
77
|
+
addressId: "That address cannot be used.",
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
const address = addressesRepository.revealAddress(addressDoc);
|
|
81
|
+
const amendment = {
|
|
82
|
+
at: new Date(),
|
|
83
|
+
actorUid: actor.uid,
|
|
84
|
+
actorRole: actor.role,
|
|
85
|
+
...(input.reason?.trim() ? { reason: input.reason.trim() } : {}),
|
|
86
|
+
fullName: String(address.fullName ?? ""),
|
|
87
|
+
phone: String(address.phone ?? ""),
|
|
88
|
+
addressLine1: String(address.addressLine1 ?? ""),
|
|
89
|
+
...(address.addressLine2 ? { addressLine2: String(address.addressLine2) } : {}),
|
|
90
|
+
...(address.landmark ? { landmark: String(address.landmark) } : {}),
|
|
91
|
+
city: address.city ?? "",
|
|
92
|
+
state: address.state ?? "",
|
|
93
|
+
pincode: address.postalCode ?? "",
|
|
94
|
+
country: address.country ?? "",
|
|
95
|
+
};
|
|
96
|
+
/*
|
|
97
|
+
* 🛑 NOT encrypted here. `ORDER_PII_FIELDS` declares
|
|
98
|
+
* `deliveryAmendments[].fullName` and friends, and the repository's write
|
|
99
|
+
* hook runs that list through `encryptInArray` — which the encryptor has
|
|
100
|
+
* always supported.
|
|
101
|
+
*
|
|
102
|
+
* The first draft of this file encrypted by hand, on the belief that
|
|
103
|
+
* `encryptPiiFields` "is a flat top-level loop that never descends". That
|
|
104
|
+
* is CLAUDE.md's wording and it is false: the function handles `a.b` and
|
|
105
|
+
* `a[].b` in both directions, and `CONVERSATION_PII_FIELDS` has been using
|
|
106
|
+
* the dotted form in production. Encrypting here as well would double-
|
|
107
|
+
* encrypt — the guard is a `enc:v1:` prefix test, so the second pass is a
|
|
108
|
+
* no-op today and a silent corruption the day the prefix changes.
|
|
109
|
+
*/
|
|
110
|
+
/*
|
|
111
|
+
* 🛑 Whole array, not `arrayUnion`. `arrayUnion` cannot enforce the cap —
|
|
112
|
+
* the same reason `withHistory`'s batch paths fold in memory — and it
|
|
113
|
+
* de-duplicates by value, so two identical corrections would collapse into
|
|
114
|
+
* one and the record would under-state what happened.
|
|
115
|
+
*/
|
|
116
|
+
await orderRepository.update(input.orderId, {
|
|
117
|
+
deliveryAmendments: [...existing, amendment],
|
|
118
|
+
});
|
|
119
|
+
return amendment;
|
|
120
|
+
}
|
|
121
|
+
/** Plaintext amendments, for a surface that renders them. */
|
|
122
|
+
export function revealAmendments(order) {
|
|
123
|
+
return (order.deliveryAmendments ?? []).map((a) => revealPii(a, AMENDMENT_PII_FIELDS));
|
|
124
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* D55 — collecting a pre-order balance.
|
|
3
|
+
*
|
|
4
|
+
* ## 🛑 `balanceDue` has been computed and rendered since Part 2, and
|
|
5
|
+
* nothing has ever collected it
|
|
6
|
+
*
|
|
7
|
+
* `OrderSourcePreOrder.balanceDue` is written at order creation and
|
|
8
|
+
* `OrderProvenanceCard` prints it — so `/user/pre-orders` has been
|
|
9
|
+
* advertising a figure the product has no way to accept. That is worse than
|
|
10
|
+
* omitting it: a buyer who sees "Balance due ₹4,000" and finds no control
|
|
11
|
+
* reasonably concludes the site is broken, and a seller waiting on it has no
|
|
12
|
+
* mechanism to chase.
|
|
13
|
+
*
|
|
14
|
+
* ## Manual only, and that is a decision rather than a stopping point
|
|
15
|
+
*
|
|
16
|
+
* The manual path needs no mandate, no scheduled debit and no account
|
|
17
|
+
* change, so it is what makes the balance payable AT ALL today. The auto
|
|
18
|
+
* path is blocked on RBI requirements and on Razorpay recurring being
|
|
19
|
+
* enabled against live keys — see the schema's own note, which declares the
|
|
20
|
+
* union so adding `auto` is a compile-checked change.
|
|
21
|
+
*
|
|
22
|
+
* ## 🛑 It reuses the manual-payment REVIEW flow and none of its STORAGE
|
|
23
|
+
*
|
|
24
|
+
* The vocabulary is the same (submit a proof, an admin approves or rejects
|
|
25
|
+
* with a note) and the fields are separate, because a pre-order has two
|
|
26
|
+
* payments months apart. Writing the balance's proof over the deposit's
|
|
27
|
+
* would destroy the evidence for the payment that created the order — and
|
|
28
|
+
* `paymentWindowTimeout`, which sweeps `pending`/`pending` orders with no
|
|
29
|
+
* `paymentProofUrl`, would then cancel and restock a paid one.
|
|
30
|
+
*/
|
|
31
|
+
import type { OrderDocument } from "../../../../features/orders/schemas/firestore";
|
|
32
|
+
/**
|
|
33
|
+
* How long a buyer gets, once the balance is opened for collection.
|
|
34
|
+
*
|
|
35
|
+
* 🛑 Days, not the 15 MINUTES the deposit window uses. That window exists to
|
|
36
|
+
* stop an unpaid order holding stock; this one is a bill falling due on goods
|
|
37
|
+
* that have arrived, and the buyer may legitimately need to move money. A
|
|
38
|
+
* 15-minute balance window would cancel real orders for no benefit.
|
|
39
|
+
*/
|
|
40
|
+
export declare const BALANCE_WINDOW_DAYS = 7;
|
|
41
|
+
/** The balance a pre-order still owes, or `null` when it is not one. */
|
|
42
|
+
export declare function balanceDueOn(order: OrderDocument): number | null;
|
|
43
|
+
/**
|
|
44
|
+
* Open the balance for collection — the seller's "the goods have landed"
|
|
45
|
+
* signal.
|
|
46
|
+
*
|
|
47
|
+
* 🛑 **Opening is an EVENT, not a derived state.** The balance could be
|
|
48
|
+
* computed as payable the moment the order exists, and that would be wrong:
|
|
49
|
+
* a pre-order's whole premise is that the goods do not exist yet, so asking
|
|
50
|
+
* for the balance before they do is asking a buyer to fund inventory. The
|
|
51
|
+
* seller opens it when stock arrives, and `dueBy` runs from that moment.
|
|
52
|
+
*
|
|
53
|
+
* 🛑 Idempotent. A seller clicking twice must not restart the buyer's
|
|
54
|
+
* window — which, since `dueBy` is what a reminder and any future
|
|
55
|
+
* auto-cancel would read, would silently extend the deadline every time
|
|
56
|
+
* anyone touched the button.
|
|
57
|
+
*
|
|
58
|
+
* 🛑 **Ownership is checked HERE, and it has to be**: the route that calls
|
|
59
|
+
* this is role-gated to sellers, and every seller passes that gate. Without
|
|
60
|
+
* this block any seller could open — and therefore start the payment clock
|
|
61
|
+
* on, and notify the buyer about — another seller's pre-order.
|
|
62
|
+
*
|
|
63
|
+
* It was missing, and the reason it was invisible is worth recording: the
|
|
64
|
+
* route also carried `permission: "orders:write"` alongside a `roles` array
|
|
65
|
+
* holding `seller`, which `getServerPermissions()` resolves for `employee`
|
|
66
|
+
* only — so every seller got a 403 before reaching this function at all. A
|
|
67
|
+
* broken gate masked an absent one, exactly as in Root Cause #98.
|
|
68
|
+
*/
|
|
69
|
+
export declare function openBalanceCollection(orderId: string, actor: {
|
|
70
|
+
uid: string;
|
|
71
|
+
role: string;
|
|
72
|
+
}, now?: Date): Promise<OrderDocument>;
|
|
73
|
+
/**
|
|
74
|
+
* The buyer submits a proof.
|
|
75
|
+
*
|
|
76
|
+
* 🛑 Allowed from `awaiting` AND from `rejected`. A rejected proof is the
|
|
77
|
+
* commonest reason to need a second attempt — a blurred screenshot, the
|
|
78
|
+
* wrong reference — and locking the buyer out after one rejection turns a
|
|
79
|
+
* recoverable mistake into a support ticket.
|
|
80
|
+
*/
|
|
81
|
+
export declare function submitBalanceProof(orderId: string, buyerUid: string, proofUrl: string, transactionId: string, now?: Date): Promise<void>;
|
|
82
|
+
/**
|
|
83
|
+
* An admin approves or rejects the balance proof.
|
|
84
|
+
*
|
|
85
|
+
* 🛑 A rejection REQUIRES a note, for the same reason a reversal does: the
|
|
86
|
+
* buyer has to know what to send instead, and "rejected" alone guarantees
|
|
87
|
+
* the next attempt is the same proof again.
|
|
88
|
+
*/
|
|
89
|
+
export declare function reviewBalanceProof(orderId: string, reviewerUid: string, approve: boolean, note?: string, now?: Date): Promise<void>;
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* D55 — collecting a pre-order balance.
|
|
3
|
+
*
|
|
4
|
+
* ## 🛑 `balanceDue` has been computed and rendered since Part 2, and
|
|
5
|
+
* nothing has ever collected it
|
|
6
|
+
*
|
|
7
|
+
* `OrderSourcePreOrder.balanceDue` is written at order creation and
|
|
8
|
+
* `OrderProvenanceCard` prints it — so `/user/pre-orders` has been
|
|
9
|
+
* advertising a figure the product has no way to accept. That is worse than
|
|
10
|
+
* omitting it: a buyer who sees "Balance due ₹4,000" and finds no control
|
|
11
|
+
* reasonably concludes the site is broken, and a seller waiting on it has no
|
|
12
|
+
* mechanism to chase.
|
|
13
|
+
*
|
|
14
|
+
* ## Manual only, and that is a decision rather than a stopping point
|
|
15
|
+
*
|
|
16
|
+
* The manual path needs no mandate, no scheduled debit and no account
|
|
17
|
+
* change, so it is what makes the balance payable AT ALL today. The auto
|
|
18
|
+
* path is blocked on RBI requirements and on Razorpay recurring being
|
|
19
|
+
* enabled against live keys — see the schema's own note, which declares the
|
|
20
|
+
* union so adding `auto` is a compile-checked change.
|
|
21
|
+
*
|
|
22
|
+
* ## 🛑 It reuses the manual-payment REVIEW flow and none of its STORAGE
|
|
23
|
+
*
|
|
24
|
+
* The vocabulary is the same (submit a proof, an admin approves or rejects
|
|
25
|
+
* with a note) and the fields are separate, because a pre-order has two
|
|
26
|
+
* payments months apart. Writing the balance's proof over the deposit's
|
|
27
|
+
* would destroy the evidence for the payment that created the order — and
|
|
28
|
+
* `paymentWindowTimeout`, which sweeps `pending`/`pending` orders with no
|
|
29
|
+
* `paymentProofUrl`, would then cancel and restock a paid one.
|
|
30
|
+
*/
|
|
31
|
+
import { orderRepository } from "../../../../features/orders/repository/orders.repository.js";
|
|
32
|
+
import { ValidationError } from "../../../../errors/validation-error.js";
|
|
33
|
+
import { NotFoundError } from "../../../../errors/not-found-error.js";
|
|
34
|
+
import { AuthorizationError } from "../../../../errors/authorization-error.js";
|
|
35
|
+
import { storeRepository } from "../../../../features/stores/repository/store.repository.js";
|
|
36
|
+
import { sendNotification } from "../../../../features/admin/actions/notification-actions.js";
|
|
37
|
+
import { serverLogger } from "../../../../monitoring/server-logger.js";
|
|
38
|
+
/*
|
|
39
|
+
* 🛑 ONE message, for the same reason the ads collector names its own: an
|
|
40
|
+
* order that is not the caller's answers 404 rather than 403, so "no such
|
|
41
|
+
* order" and "not yours" must read identically. Two matching literals are a
|
|
42
|
+
* coincidence; a constant is the rule.
|
|
43
|
+
*/
|
|
44
|
+
const ORDER_NOT_FOUND = "Order not found.";
|
|
45
|
+
/**
|
|
46
|
+
* How long a buyer gets, once the balance is opened for collection.
|
|
47
|
+
*
|
|
48
|
+
* 🛑 Days, not the 15 MINUTES the deposit window uses. That window exists to
|
|
49
|
+
* stop an unpaid order holding stock; this one is a bill falling due on goods
|
|
50
|
+
* that have arrived, and the buyer may legitimately need to move money. A
|
|
51
|
+
* 15-minute balance window would cancel real orders for no benefit.
|
|
52
|
+
*/
|
|
53
|
+
export const BALANCE_WINDOW_DAYS = 7;
|
|
54
|
+
/** The balance a pre-order still owes, or `null` when it is not one. */
|
|
55
|
+
export function balanceDueOn(order) {
|
|
56
|
+
const ctx = order.sourceContext;
|
|
57
|
+
if (!ctx || ctx.path !== "pre-order")
|
|
58
|
+
return null;
|
|
59
|
+
return ctx.balanceDue > 0 ? ctx.balanceDue : null;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Open the balance for collection — the seller's "the goods have landed"
|
|
63
|
+
* signal.
|
|
64
|
+
*
|
|
65
|
+
* 🛑 **Opening is an EVENT, not a derived state.** The balance could be
|
|
66
|
+
* computed as payable the moment the order exists, and that would be wrong:
|
|
67
|
+
* a pre-order's whole premise is that the goods do not exist yet, so asking
|
|
68
|
+
* for the balance before they do is asking a buyer to fund inventory. The
|
|
69
|
+
* seller opens it when stock arrives, and `dueBy` runs from that moment.
|
|
70
|
+
*
|
|
71
|
+
* 🛑 Idempotent. A seller clicking twice must not restart the buyer's
|
|
72
|
+
* window — which, since `dueBy` is what a reminder and any future
|
|
73
|
+
* auto-cancel would read, would silently extend the deadline every time
|
|
74
|
+
* anyone touched the button.
|
|
75
|
+
*
|
|
76
|
+
* 🛑 **Ownership is checked HERE, and it has to be**: the route that calls
|
|
77
|
+
* this is role-gated to sellers, and every seller passes that gate. Without
|
|
78
|
+
* this block any seller could open — and therefore start the payment clock
|
|
79
|
+
* on, and notify the buyer about — another seller's pre-order.
|
|
80
|
+
*
|
|
81
|
+
* It was missing, and the reason it was invisible is worth recording: the
|
|
82
|
+
* route also carried `permission: "orders:write"` alongside a `roles` array
|
|
83
|
+
* holding `seller`, which `getServerPermissions()` resolves for `employee`
|
|
84
|
+
* only — so every seller got a 403 before reaching this function at all. A
|
|
85
|
+
* broken gate masked an absent one, exactly as in Root Cause #98.
|
|
86
|
+
*/
|
|
87
|
+
export async function openBalanceCollection(orderId, actor, now = new Date()) {
|
|
88
|
+
const order = (await orderRepository.findById(orderId));
|
|
89
|
+
if (!order)
|
|
90
|
+
throw new NotFoundError(ORDER_NOT_FOUND);
|
|
91
|
+
/*
|
|
92
|
+
* 🛑 `order.storeId` is the store SLUG, never the seller's Firebase UID —
|
|
93
|
+
* two namespaces by design (§ "Store Identity Architecture"). Comparing it
|
|
94
|
+
* to `actor.uid` returns false for every legitimate seller, which is the
|
|
95
|
+
* other half of Root Cause #98. Resolve the store first.
|
|
96
|
+
*/
|
|
97
|
+
if (actor.role !== "admin") {
|
|
98
|
+
const store = await storeRepository.findByOwnerId(actor.uid);
|
|
99
|
+
if (!store || order.storeId !== store.id) {
|
|
100
|
+
throw new AuthorizationError("You do not own this order");
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
const due = balanceDueOn(order);
|
|
104
|
+
if (due == null) {
|
|
105
|
+
throw new ValidationError("This order has no pre-order balance.", {
|
|
106
|
+
orderId: "Only a pre-order carries a balance.",
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
if (order.balance)
|
|
110
|
+
return order; // already open — see the idempotency note.
|
|
111
|
+
const dueBy = new Date(now.getTime() + BALANCE_WINDOW_DAYS * 24 * 60 * 60 * 1000);
|
|
112
|
+
await orderRepository.update(orderId, {
|
|
113
|
+
balance: {
|
|
114
|
+
/*
|
|
115
|
+
* 🛑 Snapshotted HERE, from `sourceContext`, and never read live
|
|
116
|
+
* again. The gap between deposit and balance is months — the worst in
|
|
117
|
+
* the codebase for D61 — so a price or tax change in between must not
|
|
118
|
+
* move what the buyer was told they owe.
|
|
119
|
+
*/
|
|
120
|
+
amount: due,
|
|
121
|
+
method: "manual",
|
|
122
|
+
status: "awaiting",
|
|
123
|
+
dueBy,
|
|
124
|
+
},
|
|
125
|
+
});
|
|
126
|
+
await sendNotification({
|
|
127
|
+
userId: order.userId,
|
|
128
|
+
type: "order_confirmed",
|
|
129
|
+
priority: "high",
|
|
130
|
+
title: "Your pre-order balance is due",
|
|
131
|
+
message: `The balance of ₹${due.toFixed(2)} on order ${orderId} is now payable. Your item is ready to ship once it clears.`,
|
|
132
|
+
relatedId: orderId,
|
|
133
|
+
relatedType: "order",
|
|
134
|
+
});
|
|
135
|
+
return { ...order, balance: { amount: due, method: "manual", status: "awaiting", dueBy } };
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* The buyer submits a proof.
|
|
139
|
+
*
|
|
140
|
+
* 🛑 Allowed from `awaiting` AND from `rejected`. A rejected proof is the
|
|
141
|
+
* commonest reason to need a second attempt — a blurred screenshot, the
|
|
142
|
+
* wrong reference — and locking the buyer out after one rejection turns a
|
|
143
|
+
* recoverable mistake into a support ticket.
|
|
144
|
+
*/
|
|
145
|
+
export async function submitBalanceProof(orderId, buyerUid, proofUrl, transactionId, now = new Date()) {
|
|
146
|
+
const order = (await orderRepository.findById(orderId));
|
|
147
|
+
if (!order)
|
|
148
|
+
throw new NotFoundError(ORDER_NOT_FOUND);
|
|
149
|
+
/*
|
|
150
|
+
* 🛑 Ownership checked against `userId`, which is the buyer field on
|
|
151
|
+
* `OrderDocument` — NOT `buyerId`, which does not exist. A query or guard
|
|
152
|
+
* written against `buyerId` returns nothing and raises nothing, which is
|
|
153
|
+
* indistinguishable from "not your order" (CLAUDE.md records this costing
|
|
154
|
+
* real time once already).
|
|
155
|
+
*/
|
|
156
|
+
if (order.userId !== buyerUid) {
|
|
157
|
+
throw new NotFoundError(ORDER_NOT_FOUND);
|
|
158
|
+
}
|
|
159
|
+
if (!order.balance) {
|
|
160
|
+
throw new ValidationError("No balance has been opened on this order yet.", {
|
|
161
|
+
orderId: "The seller opens the balance when the goods arrive.",
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
if (order.balance.status === "paid") {
|
|
165
|
+
throw new ValidationError("This balance is already paid.", {
|
|
166
|
+
orderId: "Nothing further is owed.",
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
if (order.balance.status === "submitted") {
|
|
170
|
+
throw new ValidationError("A proof is already awaiting review.", {
|
|
171
|
+
orderId: "Wait for the current one to be reviewed before sending another.",
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
if (!transactionId.trim()) {
|
|
175
|
+
throw new ValidationError("Enter the transaction reference.", {
|
|
176
|
+
transactionId: "The UTR or reference from your bank or UPI app.",
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
await orderRepository.update(orderId, {
|
|
180
|
+
balance: {
|
|
181
|
+
...order.balance,
|
|
182
|
+
status: "submitted",
|
|
183
|
+
proofUrl,
|
|
184
|
+
transactionId: transactionId.trim(),
|
|
185
|
+
submittedAt: now,
|
|
186
|
+
/*
|
|
187
|
+
* 🛑 The previous rejection's note is CLEARED on resubmission. Leaving
|
|
188
|
+
* it would show the buyer a stale "your proof was unreadable" beside a
|
|
189
|
+
* proof they have just replaced.
|
|
190
|
+
*/
|
|
191
|
+
reviewNote: undefined,
|
|
192
|
+
},
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* An admin approves or rejects the balance proof.
|
|
197
|
+
*
|
|
198
|
+
* 🛑 A rejection REQUIRES a note, for the same reason a reversal does: the
|
|
199
|
+
* buyer has to know what to send instead, and "rejected" alone guarantees
|
|
200
|
+
* the next attempt is the same proof again.
|
|
201
|
+
*/
|
|
202
|
+
export async function reviewBalanceProof(orderId, reviewerUid, approve, note, now = new Date()) {
|
|
203
|
+
const order = (await orderRepository.findById(orderId));
|
|
204
|
+
if (!order)
|
|
205
|
+
throw new NotFoundError(ORDER_NOT_FOUND);
|
|
206
|
+
if (order.balance?.status !== "submitted") {
|
|
207
|
+
throw new ValidationError("There is no submitted balance proof to review.", {
|
|
208
|
+
orderId: `The balance is "${order.balance?.status ?? "not opened"}".`,
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
if (!approve && !note?.trim()) {
|
|
212
|
+
throw new ValidationError("A rejection needs a reason.", {
|
|
213
|
+
note: "Say what was wrong, or the buyer will send the same proof again.",
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
await orderRepository.update(orderId, {
|
|
217
|
+
balance: {
|
|
218
|
+
...order.balance,
|
|
219
|
+
status: approve ? "paid" : "rejected",
|
|
220
|
+
reviewedBy: reviewerUid,
|
|
221
|
+
reviewedAt: now,
|
|
222
|
+
...(note?.trim() ? { reviewNote: note.trim() } : {}),
|
|
223
|
+
},
|
|
224
|
+
});
|
|
225
|
+
/*
|
|
226
|
+
* 🛑 The LEDGER is not posted here, and that is deliberate.
|
|
227
|
+
*
|
|
228
|
+
* A balance payment is a second capture against an order whose sale was
|
|
229
|
+
* already posted in full at creation — `orderPlacedPosting` recognises the
|
|
230
|
+
* whole `totalPrice`, not the deposit. Posting again would double the
|
|
231
|
+
* sale. What actually changes is which asset holds the money, and that is
|
|
232
|
+
* the settlement's job: the gateway receivable clears when the settlement
|
|
233
|
+
* lands, exactly as it does for a single-payment order.
|
|
234
|
+
*
|
|
235
|
+
* Recording this is left to whoever wires the deposit/balance split into
|
|
236
|
+
* `orderPlacedPosting`, which is a change to that rule rather than an
|
|
237
|
+
* extra entry here. An extra entry would balance and be wrong, which is
|
|
238
|
+
* the failure mode Part 4's whole design is about.
|
|
239
|
+
*/
|
|
240
|
+
serverLogger.info("balance proof reviewed", {
|
|
241
|
+
orderId,
|
|
242
|
+
approved: approve,
|
|
243
|
+
reviewerUid,
|
|
244
|
+
});
|
|
245
|
+
await sendNotification({
|
|
246
|
+
userId: order.userId,
|
|
247
|
+
type: approve ? "order_confirmed" : "payment_review",
|
|
248
|
+
priority: "high",
|
|
249
|
+
title: approve ? "Balance received" : "We could not accept your payment proof",
|
|
250
|
+
message: approve
|
|
251
|
+
? `Your balance on order ${orderId} has been confirmed. It is ready to ship.`
|
|
252
|
+
: `Your balance proof on order ${orderId} was not accepted: ${note?.trim() ?? ""}`,
|
|
253
|
+
relatedId: orderId,
|
|
254
|
+
relatedType: "order",
|
|
255
|
+
});
|
|
256
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The ONE write path for a platform rate.
|
|
3
|
+
*
|
|
4
|
+
* ## Why there is exactly one
|
|
5
|
+
*
|
|
6
|
+
* Rates live on `siteSettings`, which `PUT /api/admin/site` can already write
|
|
7
|
+
* — so this is not the only way the numbers CAN move. It is the only way they
|
|
8
|
+
* move with a recorded reason, an effective date and an `adminAuditLog` row,
|
|
9
|
+
* and that difference is the whole point of `/admin/pricing`.
|
|
10
|
+
*
|
|
11
|
+
* The general settings form stays able to write them. Locking it out would
|
|
12
|
+
* mean an admin fixing a typo in the COD ceiling gets a 400 from the page that
|
|
13
|
+
* has always owned it, and the fix would be to come here — which is a worse
|
|
14
|
+
* outcome than an occasional unlogged edit. What this buys is that the
|
|
15
|
+
* DELIBERATE act of repricing has one door with a record behind it.
|
|
16
|
+
*
|
|
17
|
+
* ## 🛑 Forward-only, enforced at the WRITE
|
|
18
|
+
*
|
|
19
|
+
* `rejectRateChange` is a pure function and lives in the registry so the form
|
|
20
|
+
* can run it too — but the form running it is a courtesy, and this is the
|
|
21
|
+
* enforcement. A backdated change here would restate figures already invoiced
|
|
22
|
+
* and already paid out; see the registry header.
|
|
23
|
+
*/
|
|
24
|
+
/**
|
|
25
|
+
* How many changes the log keeps, newest first.
|
|
26
|
+
*
|
|
27
|
+
* Sized so a year of ordinary repricing fits: 16 rates changed a few times
|
|
28
|
+
* each is well under this, and the page shows only the most recent per rate
|
|
29
|
+
* anyway. The cap exists because the array rides on a document every request
|
|
30
|
+
* reads, not because 200 entries is a lot of data.
|
|
31
|
+
*/
|
|
32
|
+
export declare const RATE_CHANGE_LOG_MAX = 200;
|
|
33
|
+
export interface ApplyRateChangeInput {
|
|
34
|
+
path: string;
|
|
35
|
+
newValue: number;
|
|
36
|
+
/** Omit for "now". Never earlier than now — see `rejectRateChange`. */
|
|
37
|
+
effectiveFrom?: Date;
|
|
38
|
+
reason?: string;
|
|
39
|
+
}
|
|
40
|
+
export interface ApplyRateChangeResult {
|
|
41
|
+
path: string;
|
|
42
|
+
previousValue: number | null;
|
|
43
|
+
newValue: number;
|
|
44
|
+
effectiveFrom: Date;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Change one rate.
|
|
48
|
+
*
|
|
49
|
+
* 🛑 **One rate per call, on purpose.** A bulk form would need one reason
|
|
50
|
+
* covering a heterogeneous set, which is the same argument that removed the
|
|
51
|
+
* bulk offer-cancel: one shared reason across unlike records is worse audit
|
|
52
|
+
* data than no bulk action. Repricing three things is three decisions.
|
|
53
|
+
*/
|
|
54
|
+
export declare function applyRateChange(input: ApplyRateChangeInput, actor: {
|
|
55
|
+
uid: string;
|
|
56
|
+
name?: string;
|
|
57
|
+
isAdmin: boolean;
|
|
58
|
+
}, now?: Date): Promise<ApplyRateChangeResult>;
|