@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,144 @@
|
|
|
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
|
+
import { getAdminDb } from "../../../../providers/db-firebase/index.js";
|
|
25
|
+
import { siteSettingsRepository } from "../../../../features/admin/repository/site-settings.repository.js";
|
|
26
|
+
import { SITE_SETTINGS_COLLECTION, } from "../../../../features/admin/schemas/firestore.js";
|
|
27
|
+
import { canEditRate, rateByPath, rejectRateChange, } from "../../../shared/features/pricing/rate-registry.js";
|
|
28
|
+
import { recordAdminAction } from "../audit-log/actions.js";
|
|
29
|
+
import { AdminAuditActionValues } from "../../../../features/audit-log/schemas/firestore.js";
|
|
30
|
+
import { normalizeError } from "../../../../errors/normalize.js";
|
|
31
|
+
import { serverLogger } from "../../../../monitoring/server-logger.js";
|
|
32
|
+
import { ValidationError } from "../../../../errors/validation-error.js";
|
|
33
|
+
const SINGLETON_ID = "global";
|
|
34
|
+
/**
|
|
35
|
+
* How many changes the log keeps, newest first.
|
|
36
|
+
*
|
|
37
|
+
* Sized so a year of ordinary repricing fits: 16 rates changed a few times
|
|
38
|
+
* each is well under this, and the page shows only the most recent per rate
|
|
39
|
+
* anyway. The cap exists because the array rides on a document every request
|
|
40
|
+
* reads, not because 200 entries is a lot of data.
|
|
41
|
+
*/
|
|
42
|
+
export const RATE_CHANGE_LOG_MAX = 200;
|
|
43
|
+
/** Read a dotted path off the settings document. `null` when absent. */
|
|
44
|
+
function readRate(settings, path) {
|
|
45
|
+
const value = path
|
|
46
|
+
.split(".")
|
|
47
|
+
.reduce((acc, key) => (acc && typeof acc === "object" ? acc[key] : undefined), settings);
|
|
48
|
+
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Change one rate.
|
|
52
|
+
*
|
|
53
|
+
* 🛑 **One rate per call, on purpose.** A bulk form would need one reason
|
|
54
|
+
* covering a heterogeneous set, which is the same argument that removed the
|
|
55
|
+
* bulk offer-cancel: one shared reason across unlike records is worse audit
|
|
56
|
+
* data than no bulk action. Repricing three things is three decisions.
|
|
57
|
+
*/
|
|
58
|
+
export async function applyRateChange(input, actor, now = new Date()) {
|
|
59
|
+
const descriptor = rateByPath(input.path);
|
|
60
|
+
if (!descriptor) {
|
|
61
|
+
throw new ValidationError("That is not a rate this platform charges.", {
|
|
62
|
+
path: `Unknown rate: ${input.path}`,
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
if (!canEditRate(descriptor.tier, actor.isAdmin)) {
|
|
66
|
+
/*
|
|
67
|
+
* A statutory rate reaches this branch for EVERYONE, admins included —
|
|
68
|
+
* which is why the message names the real destination rather than
|
|
69
|
+
* implying the caller lacks a permission they could be granted.
|
|
70
|
+
*/
|
|
71
|
+
throw new ValidationError(descriptor.tier === "statutory"
|
|
72
|
+
? "Statutory rates are not set here — they are sourced per HSN code in Tax codes."
|
|
73
|
+
: "Only an administrator can change a policy ceiling.", { path: descriptor.label });
|
|
74
|
+
}
|
|
75
|
+
const settings = await siteSettingsRepository.getSingleton();
|
|
76
|
+
const previousValue = readRate(settings, descriptor.path);
|
|
77
|
+
const effectiveFrom = input.effectiveFrom ?? now;
|
|
78
|
+
const problem = rejectRateChange({ effectiveFrom, previousValue, newValue: input.newValue, reason: input.reason }, descriptor.tier, now);
|
|
79
|
+
if (problem) {
|
|
80
|
+
throw new ValidationError(problem, { newValue: problem });
|
|
81
|
+
}
|
|
82
|
+
const entry = {
|
|
83
|
+
path: descriptor.path,
|
|
84
|
+
previousValue,
|
|
85
|
+
newValue: input.newValue,
|
|
86
|
+
effectiveFrom,
|
|
87
|
+
changedByUid: actor.uid,
|
|
88
|
+
changedByName: actor.name ?? "",
|
|
89
|
+
...(input.reason?.trim() ? { reason: input.reason.trim() } : {}),
|
|
90
|
+
changedAt: now,
|
|
91
|
+
};
|
|
92
|
+
/*
|
|
93
|
+
* 🛑 A DOTTED path for the rate, and a whole-array write for the log.
|
|
94
|
+
*
|
|
95
|
+
* `update()` replaces a nested map wholesale (D13, recorded at length in
|
|
96
|
+
* `updateSingleton`), so writing `{ commissions: { platformFeePercent: 2 } }`
|
|
97
|
+
* would delete every other commission. The dotted form touches one leaf.
|
|
98
|
+
*
|
|
99
|
+
* The log cannot use `arrayUnion` because that cannot enforce the cap — the
|
|
100
|
+
* same reason `withHistory`'s batch paths fold in memory. Newest first, so
|
|
101
|
+
* the slice drops the OLDEST.
|
|
102
|
+
*/
|
|
103
|
+
const nextLog = [entry, ...(settings.rateChanges ?? [])].slice(0, RATE_CHANGE_LOG_MAX);
|
|
104
|
+
await getAdminDb()
|
|
105
|
+
.collection(SITE_SETTINGS_COLLECTION)
|
|
106
|
+
.doc(SINGLETON_ID)
|
|
107
|
+
.update({
|
|
108
|
+
[descriptor.path]: input.newValue,
|
|
109
|
+
rateChanges: nextLog,
|
|
110
|
+
updatedAt: now,
|
|
111
|
+
});
|
|
112
|
+
/*
|
|
113
|
+
* Best-effort, never fails the change — the same contract every other
|
|
114
|
+
* `recordAdminAction` caller has. The rate has already moved; refusing to
|
|
115
|
+
* acknowledge it because the log write failed would leave the admin
|
|
116
|
+
* believing it had not.
|
|
117
|
+
*/
|
|
118
|
+
try {
|
|
119
|
+
await recordAdminAction({
|
|
120
|
+
actorUid: actor.uid,
|
|
121
|
+
actorName: actor.name,
|
|
122
|
+
action: AdminAuditActionValues.PRICING_UPDATE,
|
|
123
|
+
targetType: "siteSettings",
|
|
124
|
+
targetId: descriptor.path,
|
|
125
|
+
targetLabel: descriptor.label,
|
|
126
|
+
...(input.reason?.trim() ? { reason: input.reason.trim() } : {}),
|
|
127
|
+
metadata: {
|
|
128
|
+
tier: descriptor.tier,
|
|
129
|
+
unit: descriptor.unit,
|
|
130
|
+
previousValue,
|
|
131
|
+
newValue: input.newValue,
|
|
132
|
+
effectiveFrom: effectiveFrom.toISOString(),
|
|
133
|
+
},
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
catch (err) {
|
|
137
|
+
const normalized = normalizeError(err);
|
|
138
|
+
serverLogger.warn("applyRateChange: audit log write failed", {
|
|
139
|
+
path: descriptor.path,
|
|
140
|
+
message: normalized.message,
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
return { path: descriptor.path, previousValue, newValue: input.newValue, effectiveFrom };
|
|
144
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* S5a — one parcel per destination on a coordinated purchase.
|
|
3
|
+
*
|
|
4
|
+
* A coordinated purchase can send each member's share to their own address
|
|
5
|
+
* (`CoordinationMember.destinationAddressId`). That is N parcels from one
|
|
6
|
+
* order, which is N bookings and N serviceability checks.
|
|
7
|
+
*
|
|
8
|
+
* ## 🛑 Serviceability is checked for EVERY destination BEFORE any booking
|
|
9
|
+
*
|
|
10
|
+
* Booking as you go and failing on the fourth leaves three parcels already
|
|
11
|
+
* accepted by the carrier and a coordinator who cannot undo them — the
|
|
12
|
+
* cancel path exists but the money and the pickup slot are gone. A
|
|
13
|
+
* pre-flight over all destinations turns that into one refusal naming the
|
|
14
|
+
* pincode that cannot be served, before anything is committed.
|
|
15
|
+
*
|
|
16
|
+
* Two-phase on purpose, and it is NOT a transaction: a carrier booking has
|
|
17
|
+
* no rollback. What the pre-flight buys is that the common failure — an
|
|
18
|
+
* unservable pincode — is caught while rolling back is still free.
|
|
19
|
+
*
|
|
20
|
+
* ## 🛑 Bounded by `SHARED_PURCHASE_MAX_MEMBERS`
|
|
21
|
+
*
|
|
22
|
+
* Five destinations is five outbound calls for serviceability and five more
|
|
23
|
+
* to book, on a path somebody is waiting on. That ceiling is why the limit
|
|
24
|
+
* exists at all; see the registry's own header.
|
|
25
|
+
*/
|
|
26
|
+
import { type CoordinationMember } from "../../../shared/features/offers/coordination";
|
|
27
|
+
export interface DestinationCheck {
|
|
28
|
+
addressId: string;
|
|
29
|
+
pincode: string;
|
|
30
|
+
serviceable: boolean;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Can every named destination actually be delivered to?
|
|
34
|
+
*
|
|
35
|
+
* 🛑 Returns the FULL list, not the first failure. A coordinator fixing one
|
|
36
|
+
* unservable address only to be told about the next one is three round trips
|
|
37
|
+
* for a problem that was knowable in one — and each round trip is another
|
|
38
|
+
* chance for someone to give up with the arrangement half-built.
|
|
39
|
+
*
|
|
40
|
+
* 🛑 An UNREADABLE check is treated as serviceable. A provider timeout is not
|
|
41
|
+
* evidence that a pincode cannot be served, and refusing on it would block a
|
|
42
|
+
* legitimate purchase on an outage we caused. The booking itself will fail
|
|
43
|
+
* loudly if the address really is unservable, which is the recoverable
|
|
44
|
+
* direction — and it is the same reasoning `bookShipment` applies to a wallet
|
|
45
|
+
* balance it could not read.
|
|
46
|
+
*
|
|
47
|
+
* A MISSING pincode is the opposite case and is reported unserviceable: that
|
|
48
|
+
* is a fact about the stored address, not about the provider, and it will not
|
|
49
|
+
* fix itself on a retry.
|
|
50
|
+
*/
|
|
51
|
+
export declare function checkDestinations(members: readonly CoordinationMember[],
|
|
52
|
+
/**
|
|
53
|
+
* 🛑 The parcel's weight in kg, NOT an origin pincode.
|
|
54
|
+
*
|
|
55
|
+
* `IShippingProvider.checkServiceability(pincode, weight)` reads its first
|
|
56
|
+
* argument as the DESTINATION. A reasonable guess of `(origin, destination)`
|
|
57
|
+
* would pass two strings into a `(string, number)` signature — caught by
|
|
58
|
+
* `tsc` here, but worth naming because the argument ORDER is the part a
|
|
59
|
+
* reader assumes.
|
|
60
|
+
*/
|
|
61
|
+
weightKg: number): Promise<DestinationCheck[]>;
|
|
62
|
+
/**
|
|
63
|
+
* Refuse the whole arrangement if any destination cannot be served.
|
|
64
|
+
*
|
|
65
|
+
* Names every failing pincode, for the reason above.
|
|
66
|
+
*/
|
|
67
|
+
export declare function assertAllServiceable(checks: readonly DestinationCheck[]): void;
|
|
68
|
+
/**
|
|
69
|
+
* The parcels one coordinated order produces.
|
|
70
|
+
*
|
|
71
|
+
* 🛑 **Keyed by ADDRESS, not by member.** Two members naming the same address
|
|
72
|
+
* are one parcel, not two — that is the whole reason `destinationIds` returns
|
|
73
|
+
* a distinct set. Fanning out per member would book the courier twice to the
|
|
74
|
+
* same door and bill for both.
|
|
75
|
+
*
|
|
76
|
+
* 🛑 **A member with no `destinationAddressId` is NOT in this list.** Absent
|
|
77
|
+
* means "the coordinator's address", which is the ordinary one-parcel case
|
|
78
|
+
* and is handled by `bookShipment`'s existing single-delivery path — the
|
|
79
|
+
* order's own snapshot. Inventing a parcel for them here would duplicate it.
|
|
80
|
+
*/
|
|
81
|
+
export interface ParcelPlan {
|
|
82
|
+
addressId: string;
|
|
83
|
+
/** Members whose share rides in this parcel. Never empty. */
|
|
84
|
+
memberUids: string[];
|
|
85
|
+
}
|
|
86
|
+
export declare function planParcels(members: readonly CoordinationMember[]): ParcelPlan[];
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* S5a — one parcel per destination on a coordinated purchase.
|
|
3
|
+
*
|
|
4
|
+
* A coordinated purchase can send each member's share to their own address
|
|
5
|
+
* (`CoordinationMember.destinationAddressId`). That is N parcels from one
|
|
6
|
+
* order, which is N bookings and N serviceability checks.
|
|
7
|
+
*
|
|
8
|
+
* ## 🛑 Serviceability is checked for EVERY destination BEFORE any booking
|
|
9
|
+
*
|
|
10
|
+
* Booking as you go and failing on the fourth leaves three parcels already
|
|
11
|
+
* accepted by the carrier and a coordinator who cannot undo them — the
|
|
12
|
+
* cancel path exists but the money and the pickup slot are gone. A
|
|
13
|
+
* pre-flight over all destinations turns that into one refusal naming the
|
|
14
|
+
* pincode that cannot be served, before anything is committed.
|
|
15
|
+
*
|
|
16
|
+
* Two-phase on purpose, and it is NOT a transaction: a carrier booking has
|
|
17
|
+
* no rollback. What the pre-flight buys is that the common failure — an
|
|
18
|
+
* unservable pincode — is caught while rolling back is still free.
|
|
19
|
+
*
|
|
20
|
+
* ## 🛑 Bounded by `SHARED_PURCHASE_MAX_MEMBERS`
|
|
21
|
+
*
|
|
22
|
+
* Five destinations is five outbound calls for serviceability and five more
|
|
23
|
+
* to book, on a path somebody is waiting on. That ceiling is why the limit
|
|
24
|
+
* exists at all; see the registry's own header.
|
|
25
|
+
*/
|
|
26
|
+
import { getProviders } from "../../../../contracts/registry.js";
|
|
27
|
+
import { addressesRepository } from "../../../../features/addresses/repository/addresses.repository.js";
|
|
28
|
+
import { SHARED_PURCHASE_MAX_MEMBERS, destinationIds, } from "../../../shared/features/offers/coordination.js";
|
|
29
|
+
import { ValidationError } from "../../../../errors/validation-error.js";
|
|
30
|
+
import { normalizeError } from "../../../../errors/normalize.js";
|
|
31
|
+
import { serverLogger } from "../../../../monitoring/server-logger.js";
|
|
32
|
+
/**
|
|
33
|
+
* Can every named destination actually be delivered to?
|
|
34
|
+
*
|
|
35
|
+
* 🛑 Returns the FULL list, not the first failure. A coordinator fixing one
|
|
36
|
+
* unservable address only to be told about the next one is three round trips
|
|
37
|
+
* for a problem that was knowable in one — and each round trip is another
|
|
38
|
+
* chance for someone to give up with the arrangement half-built.
|
|
39
|
+
*
|
|
40
|
+
* 🛑 An UNREADABLE check is treated as serviceable. A provider timeout is not
|
|
41
|
+
* evidence that a pincode cannot be served, and refusing on it would block a
|
|
42
|
+
* legitimate purchase on an outage we caused. The booking itself will fail
|
|
43
|
+
* loudly if the address really is unservable, which is the recoverable
|
|
44
|
+
* direction — and it is the same reasoning `bookShipment` applies to a wallet
|
|
45
|
+
* balance it could not read.
|
|
46
|
+
*
|
|
47
|
+
* A MISSING pincode is the opposite case and is reported unserviceable: that
|
|
48
|
+
* is a fact about the stored address, not about the provider, and it will not
|
|
49
|
+
* fix itself on a retry.
|
|
50
|
+
*/
|
|
51
|
+
export async function checkDestinations(members,
|
|
52
|
+
/**
|
|
53
|
+
* 🛑 The parcel's weight in kg, NOT an origin pincode.
|
|
54
|
+
*
|
|
55
|
+
* `IShippingProvider.checkServiceability(pincode, weight)` reads its first
|
|
56
|
+
* argument as the DESTINATION. A reasonable guess of `(origin, destination)`
|
|
57
|
+
* would pass two strings into a `(string, number)` signature — caught by
|
|
58
|
+
* `tsc` here, but worth naming because the argument ORDER is the part a
|
|
59
|
+
* reader assumes.
|
|
60
|
+
*/
|
|
61
|
+
weightKg) {
|
|
62
|
+
const ids = destinationIds(members);
|
|
63
|
+
if (ids.length === 0)
|
|
64
|
+
return [];
|
|
65
|
+
if (ids.length > SHARED_PURCHASE_MAX_MEMBERS) {
|
|
66
|
+
throw new ValidationError(`A coordinated purchase can ship to at most ${SHARED_PURCHASE_MAX_MEMBERS} addresses.`, { destinations: `${ids.length} distinct addresses named.` });
|
|
67
|
+
}
|
|
68
|
+
/*
|
|
69
|
+
* 🛑 Optional on the registry. This is the serviceability PRE-FLIGHT, so
|
|
70
|
+
* with no provider the honest answer is "unknown", not "unserviceable":
|
|
71
|
+
* reporting every destination as unserviceable would block an accept that
|
|
72
|
+
* manual dispatch handles perfectly well.
|
|
73
|
+
*/
|
|
74
|
+
const provider = getProviders().shipping;
|
|
75
|
+
if (!provider) {
|
|
76
|
+
return ids.map((addressId) => ({ addressId, pincode: "", serviceable: true }));
|
|
77
|
+
}
|
|
78
|
+
const docs = await Promise.all(ids.map((id) => addressesRepository.findById(id)));
|
|
79
|
+
return Promise.all(ids.map(async (addressId, i) => {
|
|
80
|
+
/*
|
|
81
|
+
* 🛑 `.postalCode` needs no reveal — it is not PII and
|
|
82
|
+
* `AddressRepository` deliberately leaves it unencrypted so it stays
|
|
83
|
+
* queryable (see its own `computeAddressHash` comment). Calling
|
|
84
|
+
* `revealAddress` here would decrypt a name and a phone number we have
|
|
85
|
+
* no use for, on a path that only needs six digits.
|
|
86
|
+
*/
|
|
87
|
+
const pincode = docs[i]?.postalCode ?? "";
|
|
88
|
+
if (!pincode)
|
|
89
|
+
return { addressId, pincode, serviceable: false };
|
|
90
|
+
try {
|
|
91
|
+
const res = await provider.checkServiceability(pincode, weightKg);
|
|
92
|
+
return { addressId, pincode, serviceable: res.isServiceable };
|
|
93
|
+
}
|
|
94
|
+
catch (err) {
|
|
95
|
+
void normalizeError(err);
|
|
96
|
+
serverLogger.warn("checkDestinations: serviceability unreadable", { addressId, pincode });
|
|
97
|
+
return { addressId, pincode, serviceable: true };
|
|
98
|
+
}
|
|
99
|
+
}));
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Refuse the whole arrangement if any destination cannot be served.
|
|
103
|
+
*
|
|
104
|
+
* Names every failing pincode, for the reason above.
|
|
105
|
+
*/
|
|
106
|
+
export function assertAllServiceable(checks) {
|
|
107
|
+
const bad = checks.filter((c) => !c.serviceable);
|
|
108
|
+
if (bad.length === 0)
|
|
109
|
+
return;
|
|
110
|
+
throw new ValidationError(bad.length === 1
|
|
111
|
+
? "One delivery address cannot be served by the courier."
|
|
112
|
+
: `${bad.length} delivery addresses cannot be served by the courier.`, {
|
|
113
|
+
destinations: bad
|
|
114
|
+
.map((b) => (b.pincode ? `${b.pincode}: not serviceable` : "missing pincode"))
|
|
115
|
+
.join("; "),
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
export function planParcels(members) {
|
|
119
|
+
const byAddress = new Map();
|
|
120
|
+
for (const m of members) {
|
|
121
|
+
if (!m.destinationAddressId)
|
|
122
|
+
continue;
|
|
123
|
+
const uids = byAddress.get(m.destinationAddressId);
|
|
124
|
+
if (uids)
|
|
125
|
+
uids.push(m.uid);
|
|
126
|
+
else
|
|
127
|
+
byAddress.set(m.destinationAddressId, [m.uid]);
|
|
128
|
+
}
|
|
129
|
+
return [...byAddress].map(([addressId, memberUids]) => ({ addressId, memberUids }));
|
|
130
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Booking a parcel through the platform's own carrier account — Part 1′.
|
|
3
|
+
*
|
|
4
|
+
* ## 🛑 Why this could not be built until Part 14
|
|
5
|
+
*
|
|
6
|
+
* `CreateShipmentInput` needs a structured `{name, phone, addressLine1, city,
|
|
7
|
+
* state, pincode, country}` for BOTH ends. Until the delivery snapshot landed
|
|
8
|
+
* the order carried `shippingAddress?: string` — flat free text with no
|
|
9
|
+
* pincode and no phone — and the only way to produce a party from it was to
|
|
10
|
+
* parse prose. The `placeOfSupply` comment already refuses that for tax, and
|
|
11
|
+
* the same argument holds here: a mis-parsed pincode is a parcel that goes to
|
|
12
|
+
* the wrong city, except nothing errors and the label prints fine.
|
|
13
|
+
*
|
|
14
|
+
* `deliveryPartyFor(order)` now returns exactly that shape, or `null` when a
|
|
15
|
+
* required field is missing — which is the only honest answer for an order
|
|
16
|
+
* placed before the snapshot existed.
|
|
17
|
+
*
|
|
18
|
+
* ## 🛑 `platformBookedShipment` is the flag this writes
|
|
19
|
+
*
|
|
20
|
+
* `order-math.ts` gates shipping reimbursement on it
|
|
21
|
+
* (`if (order.platformBookedShipment === true) return 0`) and
|
|
22
|
+
* `quote.ts:241` records that it has had **ZERO writers** — so the
|
|
23
|
+
* reimbursement rule has been reading a flag nothing sets. This is its
|
|
24
|
+
* writer, which is why the booking and the dispatch form are one unit of
|
|
25
|
+
* work and why the form must not ship first: a "Book through us" button with
|
|
26
|
+
* no booking behind it is a control the server refuses.
|
|
27
|
+
*/
|
|
28
|
+
export interface BookShipmentInput {
|
|
29
|
+
orderId: string;
|
|
30
|
+
/** Centimetres and kilograms, from the dispatch form's box preset. */
|
|
31
|
+
dimensions: {
|
|
32
|
+
weight: number;
|
|
33
|
+
length: number;
|
|
34
|
+
width: number;
|
|
35
|
+
height: number;
|
|
36
|
+
};
|
|
37
|
+
/** The store address the courier collects from. */
|
|
38
|
+
pickupAddressId: string;
|
|
39
|
+
}
|
|
40
|
+
export interface BookShipmentResult {
|
|
41
|
+
trackingId: string;
|
|
42
|
+
courier?: string;
|
|
43
|
+
estimatedDelivery?: string;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Book one parcel.
|
|
47
|
+
*
|
|
48
|
+
* ## 🛑 The wallet is checked BEFORE the booking, and a failure is a 409
|
|
49
|
+
*
|
|
50
|
+
* A carrier account with no balance rejects the booking — but some providers
|
|
51
|
+
* accept it and then silently fail to hand over, which produces a tracking id
|
|
52
|
+
* that never moves. Checking first turns "the parcel vanished" into "top up
|
|
53
|
+
* the account", and the 409 is deliberate: the request is well-formed and the
|
|
54
|
+
* seller is not confused, the world is simply not ready.
|
|
55
|
+
*
|
|
56
|
+
* 🛑 `getWalletBalance` is NOT abstract on `IShippingProvider` — manual
|
|
57
|
+
* shipping has no wallet, so a provider that does not implement it returns
|
|
58
|
+
* undefined and the check is SKIPPED rather than failing. A missing balance
|
|
59
|
+
* must never block a manual dispatch.
|
|
60
|
+
*/
|
|
61
|
+
export declare function bookShipment(input: BookShipmentInput): Promise<BookShipmentResult>;
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Booking a parcel through the platform's own carrier account — Part 1′.
|
|
3
|
+
*
|
|
4
|
+
* ## 🛑 Why this could not be built until Part 14
|
|
5
|
+
*
|
|
6
|
+
* `CreateShipmentInput` needs a structured `{name, phone, addressLine1, city,
|
|
7
|
+
* state, pincode, country}` for BOTH ends. Until the delivery snapshot landed
|
|
8
|
+
* the order carried `shippingAddress?: string` — flat free text with no
|
|
9
|
+
* pincode and no phone — and the only way to produce a party from it was to
|
|
10
|
+
* parse prose. The `placeOfSupply` comment already refuses that for tax, and
|
|
11
|
+
* the same argument holds here: a mis-parsed pincode is a parcel that goes to
|
|
12
|
+
* the wrong city, except nothing errors and the label prints fine.
|
|
13
|
+
*
|
|
14
|
+
* `deliveryPartyFor(order)` now returns exactly that shape, or `null` when a
|
|
15
|
+
* required field is missing — which is the only honest answer for an order
|
|
16
|
+
* placed before the snapshot existed.
|
|
17
|
+
*
|
|
18
|
+
* ## 🛑 `platformBookedShipment` is the flag this writes
|
|
19
|
+
*
|
|
20
|
+
* `order-math.ts` gates shipping reimbursement on it
|
|
21
|
+
* (`if (order.platformBookedShipment === true) return 0`) and
|
|
22
|
+
* `quote.ts:241` records that it has had **ZERO writers** — so the
|
|
23
|
+
* reimbursement rule has been reading a flag nothing sets. This is its
|
|
24
|
+
* writer, which is why the booking and the dispatch form are one unit of
|
|
25
|
+
* work and why the form must not ship first: a "Book through us" button with
|
|
26
|
+
* no booking behind it is a control the server refuses.
|
|
27
|
+
*/
|
|
28
|
+
import { getProviders } from "../../../../contracts/registry.js";
|
|
29
|
+
import { orderRepository } from "../../../../features/orders/repository/orders.repository.js";
|
|
30
|
+
import { storeRepository } from "../../../../features/stores/repository/store.repository.js";
|
|
31
|
+
import { addressesRepository } from "../../../../features/addresses/repository/addresses.repository.js";
|
|
32
|
+
import { deliveryPartyFor } from "../../../shared/features/orders/delivery-snapshot.js";
|
|
33
|
+
import { revealPii } from "../../../../security/pii-accessors.js";
|
|
34
|
+
import { ORDER_PII_FIELDS } from "../../../../security/pii-schemas.js";
|
|
35
|
+
import { ValidationError } from "../../../../errors/validation-error.js";
|
|
36
|
+
import { NotFoundError } from "../../../../errors/not-found-error.js";
|
|
37
|
+
import { normalizeError } from "../../../../errors/normalize.js";
|
|
38
|
+
import { serverLogger } from "../../../../monitoring/server-logger.js";
|
|
39
|
+
/**
|
|
40
|
+
* Book one parcel.
|
|
41
|
+
*
|
|
42
|
+
* ## 🛑 The wallet is checked BEFORE the booking, and a failure is a 409
|
|
43
|
+
*
|
|
44
|
+
* A carrier account with no balance rejects the booking — but some providers
|
|
45
|
+
* accept it and then silently fail to hand over, which produces a tracking id
|
|
46
|
+
* that never moves. Checking first turns "the parcel vanished" into "top up
|
|
47
|
+
* the account", and the 409 is deliberate: the request is well-formed and the
|
|
48
|
+
* seller is not confused, the world is simply not ready.
|
|
49
|
+
*
|
|
50
|
+
* 🛑 `getWalletBalance` is NOT abstract on `IShippingProvider` — manual
|
|
51
|
+
* shipping has no wallet, so a provider that does not implement it returns
|
|
52
|
+
* undefined and the check is SKIPPED rather than failing. A missing balance
|
|
53
|
+
* must never block a manual dispatch.
|
|
54
|
+
*/
|
|
55
|
+
export async function bookShipment(input) {
|
|
56
|
+
const order = (await orderRepository.findById(input.orderId));
|
|
57
|
+
if (!order)
|
|
58
|
+
throw new NotFoundError("Order not found.");
|
|
59
|
+
if (order.trackingNumber) {
|
|
60
|
+
/*
|
|
61
|
+
* 409, not 400. Double-booking is the expensive mistake here — two
|
|
62
|
+
* parcels, two charges, one of them never collected — and it is easy to
|
|
63
|
+
* cause by refreshing a form.
|
|
64
|
+
*/
|
|
65
|
+
throw new ValidationError("This order already has a shipment.", {
|
|
66
|
+
orderId: `Already booked as ${order.trackingNumber}.`,
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
/*
|
|
70
|
+
* 🛑 DECRYPTED first. `OrderDocument`'s delivery name, phone and address
|
|
71
|
+
* lines are `Encrypted<string>` (ciphertext-by-default), and `Encrypted<T>`
|
|
72
|
+
* is deliberately assignable to nothing that wants a plain `string` — which
|
|
73
|
+
* is what made this a compile error rather than a courier label printed
|
|
74
|
+
* with `enc:v1:…` on it.
|
|
75
|
+
*
|
|
76
|
+
* A courier booking is the one place the plaintext is genuinely required:
|
|
77
|
+
* the carrier has to deliver to a real person at a real door. Masked would
|
|
78
|
+
* be the wrong default HERE and the right one almost everywhere else, which
|
|
79
|
+
* is why the two accessors are separately named.
|
|
80
|
+
*/
|
|
81
|
+
const delivery = deliveryPartyFor(revealPii(order, ORDER_PII_FIELDS));
|
|
82
|
+
if (!delivery) {
|
|
83
|
+
/*
|
|
84
|
+
* An order placed before the delivery snapshot existed. Refused, loudly,
|
|
85
|
+
* rather than guessed at — see the module header on parsing prose.
|
|
86
|
+
*/
|
|
87
|
+
throw new ValidationError("This order has no structured delivery address, so it cannot be booked automatically.", { orderId: "Placed before structured addresses; dispatch it manually." });
|
|
88
|
+
}
|
|
89
|
+
const pickupDoc = await addressesRepository.findById(input.pickupAddressId);
|
|
90
|
+
const store = order.storeId ? await storeRepository.findBySlug(order.storeId) : null;
|
|
91
|
+
/*
|
|
92
|
+
* 🛑 The pickup address must belong to the ORDER'S STORE. Checking only
|
|
93
|
+
* that it exists would let one seller's parcel be collected from another
|
|
94
|
+
* seller's premises — the same class of hole `assertOwnedAddresses` closes
|
|
95
|
+
* for coordinated purchases, and it is reachable here by id alone.
|
|
96
|
+
*/
|
|
97
|
+
if (!pickupDoc ||
|
|
98
|
+
pickupDoc.ownerType !== "store" ||
|
|
99
|
+
!store ||
|
|
100
|
+
pickupDoc.ownerId !== store.id) {
|
|
101
|
+
throw new ValidationError("That pickup address is not available for this store.", {
|
|
102
|
+
pickupAddressId: "Pick one of this store's own addresses.",
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
const pickupPlain = addressesRepository.revealAddress(pickupDoc);
|
|
106
|
+
const pickup = {
|
|
107
|
+
name: String(pickupPlain.fullName ?? store.storeName ?? ""),
|
|
108
|
+
phone: String(pickupPlain.phone ?? ""),
|
|
109
|
+
addressLine1: String(pickupPlain.addressLine1 ?? ""),
|
|
110
|
+
...(pickupPlain.addressLine2 ? { addressLine2: String(pickupPlain.addressLine2) } : {}),
|
|
111
|
+
city: pickupPlain.city ?? "",
|
|
112
|
+
state: pickupPlain.state ?? "",
|
|
113
|
+
pincode: pickupPlain.postalCode ?? "",
|
|
114
|
+
country: pickupPlain.country ?? "",
|
|
115
|
+
};
|
|
116
|
+
/*
|
|
117
|
+
* 🛑 `getProviders().shipping` is OPTIONAL on the registry, and refusing
|
|
118
|
+
* here is the honest handling. `providers.config.ts` always registers one
|
|
119
|
+
* today, so this is unreachable in this app — but a non-null assertion
|
|
120
|
+
* would turn the day somebody ships a build without it into a
|
|
121
|
+
* `Cannot read properties of undefined` at the moment money is involved,
|
|
122
|
+
* instead of a sentence a seller can act on.
|
|
123
|
+
*/
|
|
124
|
+
const provider = getProviders().shipping;
|
|
125
|
+
if (!provider) {
|
|
126
|
+
throw new ValidationError("No shipping provider is configured.", {
|
|
127
|
+
orderId: "Automatic booking is unavailable — dispatch this order manually.",
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
/*
|
|
131
|
+
* 🛑 `WalletBalance` is a THREE-state union and collapsing it is the whole
|
|
132
|
+
* hazard the contract's own header describes. Only `{known: true, amount: 0}`
|
|
133
|
+
* is a decision to skip. `{known: false}` means either "no wallet exists"
|
|
134
|
+
* (manual shipping) or "could not read it right now" — and refusing on an
|
|
135
|
+
* unreadable balance would fail every dispatch on a transient blip.
|
|
136
|
+
*
|
|
137
|
+
* An earlier draft of this function tested `typeof balance === "number"`,
|
|
138
|
+
* which is never true against this contract: the guard was dead, so a
|
|
139
|
+
* genuinely empty wallet would have been booked into anyway.
|
|
140
|
+
*/
|
|
141
|
+
const balance = await provider.getWalletBalance().catch((err) => {
|
|
142
|
+
void normalizeError(err);
|
|
143
|
+
serverLogger.warn("bookShipment: wallet balance unavailable", { orderId: input.orderId });
|
|
144
|
+
return { known: false, supported: true };
|
|
145
|
+
});
|
|
146
|
+
if (balance.known && balance.amount <= 0) {
|
|
147
|
+
throw new ValidationError("The courier account has no balance, so the booking was not attempted.", { orderId: "Top up the carrier wallet and try again." });
|
|
148
|
+
}
|
|
149
|
+
const payload = {
|
|
150
|
+
orderId: input.orderId,
|
|
151
|
+
dimensions: input.dimensions,
|
|
152
|
+
pickup,
|
|
153
|
+
delivery,
|
|
154
|
+
...(order.paymentMethod === "cod"
|
|
155
|
+
? { isCod: true, codAmount: order.codRemainingAmount ?? order.totalPrice }
|
|
156
|
+
: {}),
|
|
157
|
+
};
|
|
158
|
+
const shipment = await provider.createShipment(payload);
|
|
159
|
+
/*
|
|
160
|
+
* 🛑 `platformBookedShipment: true` is the WHOLE POINT of this function.
|
|
161
|
+
*
|
|
162
|
+
* `order-math.ts` already gates shipping reimbursement on it and nothing
|
|
163
|
+
* has ever set it. Writing the tracking id without it would book the parcel
|
|
164
|
+
* AND reimburse the seller for postage the platform just paid.
|
|
165
|
+
*/
|
|
166
|
+
await orderRepository.update(input.orderId, {
|
|
167
|
+
trackingNumber: shipment.trackingId,
|
|
168
|
+
...(shipment.courier ? { carrier: shipment.courier } : {}),
|
|
169
|
+
platformBookedShipment: true,
|
|
170
|
+
});
|
|
171
|
+
return {
|
|
172
|
+
trackingId: shipment.trackingId,
|
|
173
|
+
...(shipment.courier ? { courier: shipment.courier } : {}),
|
|
174
|
+
...(shipment.estimatedDelivery ? { estimatedDelivery: shipment.estimatedDelivery } : {}),
|
|
175
|
+
};
|
|
176
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
1
2
|
/**
|
|
2
3
|
* Raising and clearing a courier block, and telling the ADMIN about it.
|
|
3
4
|
*
|
|
@@ -21,6 +22,9 @@
|
|
|
21
22
|
* whole 80/day budget saying the same thing fifty times and leave the
|
|
22
23
|
* operator blind to everything else.
|
|
23
24
|
*/
|
|
25
|
+
import { Fragment } from "react";
|
|
26
|
+
import { renderToStaticMarkup } from "react-dom/server";
|
|
27
|
+
import { EmailContainer, EmailDoc, EmailRow, } from "../../../../features/email/primitives.js";
|
|
24
28
|
import { paymentCasesRepository } from "../../../../features/payment-cases/repository/payment-cases.repository.js";
|
|
25
29
|
// `adminNotificationsRepository` lives behind the repositories barrel — the
|
|
26
30
|
// same import the two existing staff-signal call sites use.
|
|
@@ -227,7 +231,16 @@ async function emailStaff(input) {
|
|
|
227
231
|
const { error } = await sendEmail({
|
|
228
232
|
to: recipients,
|
|
229
233
|
subject: input.title,
|
|
230
|
-
|
|
234
|
+
/*
|
|
235
|
+
* Composed from the Email primitives rather than hand-built HTML.
|
|
236
|
+
*
|
|
237
|
+
* The old line string-replaced `\n\n` into `</p><p>`, which produces
|
|
238
|
+
* markup no email client was ever tested against and which
|
|
239
|
+
* `audit-email-raw-html` blocks for that reason. `EmailDoc` renders the
|
|
240
|
+
* table-based, inline-styled structure clients actually honour, and the
|
|
241
|
+
* paragraph split now happens on the DATA before it reaches the markup.
|
|
242
|
+
*/
|
|
243
|
+
html: renderStaffAlertEmail(input.title, body),
|
|
231
244
|
text: body,
|
|
232
245
|
}, { channel: "email", feature: "courier_wallet_low", audience: "staff" });
|
|
233
246
|
if (error) {
|
|
@@ -237,3 +250,15 @@ async function emailStaff(input) {
|
|
|
237
250
|
});
|
|
238
251
|
}
|
|
239
252
|
}
|
|
253
|
+
/**
|
|
254
|
+
* The staff courier-block alert, as an email document.
|
|
255
|
+
*
|
|
256
|
+
* Kept local to this file rather than exported: it is one alert with one
|
|
257
|
+
* shape, and a shared "generic staff alert" helper would invite the next
|
|
258
|
+
* caller to pass admin-editable text through it — which is how an email
|
|
259
|
+
* template becomes an injection surface. The body here is built entirely from
|
|
260
|
+
* our own constants and the provider/reason enums.
|
|
261
|
+
*/
|
|
262
|
+
function renderStaffAlertEmail(title, body) {
|
|
263
|
+
return `<!DOCTYPE html>${renderToStaticMarkup(_jsx(EmailDoc, { title: title, children: _jsx(EmailContainer, { children: _jsx(EmailRow, { children: body.split("\n\n").map((para, i) => (_jsxs(Fragment, { children: [i > 0 ? _jsx("br", {}) : null, i > 0 ? _jsx("br", {}) : null, para.split("\n").map((line, j) => (_jsxs(Fragment, { children: [j > 0 ? _jsx("br", {}) : null, line] }, j)))] }, i))) }) }) }))}`;
|
|
264
|
+
}
|
|
@@ -39,7 +39,7 @@ import type { PublicSiteSettings } from "../../../shared/features/site-settings/
|
|
|
39
39
|
*/
|
|
40
40
|
export declare const PUBLIC_SITE_SETTINGS_FIELDS: readonly ["contact", "payment", "listings", "notificationChannels", "announcementBar", "support", "navConfig", "actionConfig", "background", "watermark", "disabledRoutes", "chat", "search"];
|
|
41
41
|
/** Everything else, with the reason it stays server-side. */
|
|
42
|
-
export declare const PRIVATE_SITE_SETTINGS_FIELDS: readonly ["id", "createdAt", "updatedAt", "credentials", "emailSettings", "legalPages", "commissions", "emi", "gst", "laborRate", "adSettings", "integrations", "platformLimits", "auctionConfig", "siteName", "tagline", "motto", "logo", "favicon", "seo", "socialLinks", "features", "shipping", "returns", "faq", "aboutContent", "navbarConfig", "footerConfig", "theme", "featuredResults", "messaging"];
|
|
42
|
+
export declare const PRIVATE_SITE_SETTINGS_FIELDS: readonly ["rateChanges", "id", "createdAt", "updatedAt", "credentials", "emailSettings", "legalPages", "commissions", "emi", "gst", "laborRate", "adSettings", "integrations", "platformLimits", "auctionConfig", "siteName", "tagline", "motto", "logo", "favicon", "seo", "socialLinks", "features", "shipping", "returns", "faq", "aboutContent", "navbarConfig", "footerConfig", "theme", "featuredResults", "messaging"];
|
|
43
43
|
export interface ToPublicSiteSettingsOptions {
|
|
44
44
|
/**
|
|
45
45
|
* The resolved marker → wordmark → text watermark chain. Injected because
|