@mohasinac/appkit 4.21.0 → 4.23.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/_internal/server/features/checkout/locked-lines.js +12 -2
- package/dist/_internal/server/features/stores/adapters.d.ts +1 -1
- package/dist/_internal/server/features/stores/adapters.js +5 -0
- package/dist/_internal/server/jobs/core/auctionSettlement.js +10 -4
- package/dist/_internal/server/jobs/core/offerExpiry.js +10 -2
- package/dist/_internal/server/jobs/core/payoutBatch.js +15 -3
- package/dist/_internal/shared/features/products/schema.d.ts +8 -8
- package/dist/_internal/shared/features/promotions/schema.d.ts +4 -4
- package/dist/_internal/shared/history/index.d.ts +1 -1
- package/dist/_internal/shared/history/types.d.ts +16 -0
- package/dist/client.d.ts +7 -0
- package/dist/client.js +4 -0
- package/dist/constants/field-names.d.ts +12 -0
- package/dist/constants/field-names.js +12 -0
- package/dist/features/account/components/AddressForm.js +45 -29
- package/dist/features/admin/actions/admin-actions.js +13 -3
- package/dist/features/admin/components/AdminBidsView.js +7 -1
- package/dist/features/admin/components/AdminBundleEditorView.js +3 -0
- package/dist/features/admin/components/AdminScammerEditorView.d.ts +5 -1
- package/dist/features/admin/components/AdminScammerEditorView.js +3 -2
- package/dist/features/admin/components/AdminSupportTicketDetailView.d.ts +5 -1
- package/dist/features/admin/components/AdminSupportTicketDetailView.js +3 -2
- package/dist/features/auctions/components/PlaceBidFormClient.js +3 -1
- package/dist/features/auctions/repository/bid.repository.d.ts +21 -8
- package/dist/features/auctions/repository/bid.repository.js +57 -33
- package/dist/features/auctions/schemas/firestore.d.ts +27 -0
- package/dist/features/auctions/schemas/firestore.js +21 -0
- package/dist/features/auth/schemas/index.d.ts +2 -2
- package/dist/features/catalogue/actions/approve-catalogue-listing.js +7 -6
- package/dist/features/catalogue/actions/list-from-catalogue.js +2 -2
- package/dist/features/catalogue/actions/submit-for-approval.js +1 -4
- package/dist/features/catalogue/components/AdminCatalogueApprovalsView.js +5 -1
- package/dist/features/catalogue/components/CatalogueItemEditorView.js +3 -1
- package/dist/features/catalogue/repository/catalogue.repository.d.ts +23 -0
- package/dist/features/catalogue/repository/catalogue.repository.js +28 -1
- package/dist/features/catalogue/schemas/firestore.d.ts +19 -0
- package/dist/features/catalogue/schemas/firestore.js +12 -0
- package/dist/features/categories/schemas/index.d.ts +2 -2
- package/dist/features/layout/BottomActions.js +18 -1
- package/dist/features/layout/BottomActionsContext.d.ts +19 -0
- package/dist/features/layout/BottomActionsContext.js +37 -0
- package/dist/features/layout/hooks/useBottomActions.d.ts +10 -0
- package/dist/features/layout/hooks/useBottomActions.js +48 -11
- package/dist/features/layout/hooks/useFormBottomActions.d.ts +57 -0
- package/dist/features/layout/hooks/useFormBottomActions.js +89 -0
- package/dist/features/layout/index.d.ts +2 -0
- package/dist/features/layout/index.js +1 -0
- package/dist/features/messages/schemas/index.d.ts +6 -6
- package/dist/features/orders/schemas/index.d.ts +12 -12
- package/dist/features/payments/repository/payout.repository.d.ts +18 -5
- package/dist/features/payments/repository/payout.repository.js +45 -19
- package/dist/features/payments/schemas/firestore.d.ts +33 -1
- package/dist/features/payments/schemas/firestore.js +39 -0
- package/dist/features/payments/schemas/index.d.ts +12 -12
- package/dist/features/products/components/MakeOfferButton.js +5 -3
- package/dist/features/products/schemas/index.d.ts +2 -2
- package/dist/features/promotions/schemas/index.d.ts +12 -12
- package/dist/features/scams/repository/scammer.repository.d.ts +18 -1
- package/dist/features/scams/repository/scammer.repository.js +24 -2
- package/dist/features/scams/schemas/firestore.d.ts +24 -0
- package/dist/features/scams/schemas/firestore.js +30 -0
- package/dist/features/shell/QuickFormDrawer.js +15 -1
- package/dist/features/shell/SectionForm.d.ts +16 -2
- package/dist/features/shell/SectionForm.js +28 -2
- package/dist/features/shipments/components/AdminShipmentEditorView.js +5 -2
- package/dist/features/shipments/repository/shipments.repository.d.ts +18 -0
- package/dist/features/shipments/repository/shipments.repository.js +23 -1
- package/dist/features/shipments/schemas/firestore.d.ts +14 -0
- package/dist/features/shipments/schemas/firestore.js +19 -0
- package/dist/features/status-history/components/RecordStatusTimeline.d.ts +12 -0
- package/dist/features/status-history/components/RecordStatusTimeline.js +90 -0
- package/dist/features/status-history/index.d.ts +2 -0
- package/dist/features/status-history/index.js +1 -0
- package/dist/features/stores/repository/store.repository.d.ts +30 -2
- package/dist/features/stores/repository/store.repository.js +43 -4
- package/dist/features/stores/schemas/firestore.d.ts +29 -0
- package/dist/features/stores/schemas/firestore.js +31 -0
- package/dist/features/support/repository/support.repository.d.ts +47 -3
- package/dist/features/support/repository/support.repository.js +72 -12
- package/dist/features/support/schemas/firestore.d.ts +38 -1
- package/dist/features/support/schemas/firestore.js +34 -0
- package/dist/features/support/schemas/index.d.ts +2 -2
- package/dist/features/tester/seed-data/tester-checklist-seed-data.js +90 -0
- package/dist/index.d.ts +7 -1
- package/dist/index.js +16 -1
- package/dist/schemas/registry.d.ts +32 -32
- package/dist/seed/bids-seed-data.js +8 -0
- package/dist/ui/components/Form.d.ts +22 -1
- package/dist/ui/components/Form.js +30 -2
- package/dist/ui/components/Modal.js +4 -3
- package/dist/ui/components/SideDrawer.js +5 -4
- package/dist/ui/components/overlay-context.d.ts +21 -0
- package/dist/ui/components/overlay-context.js +25 -0
- package/dist/ui/forms/FormErrorList.d.ts +29 -0
- package/dist/ui/forms/FormErrorList.js +36 -0
- package/dist/ui/forms/FormErrorSummary.d.ts +8 -0
- package/dist/ui/forms/FormErrorSummary.js +15 -8
- package/dist/ui/forms/FormShell.d.ts +25 -1
- package/dist/ui/forms/FormShell.js +39 -4
- package/dist/ui/forms/index.d.ts +2 -0
- package/dist/ui/forms/index.js +1 -0
- package/package.json +1 -1
|
@@ -25,6 +25,15 @@ export function QuickFormDrawer({ isOpen, onClose, title, fields, defaultValues,
|
|
|
25
25
|
const drawerRef = useRef(null);
|
|
26
26
|
const [values, setValues] = useState(() => initValues(fields, defaultValues));
|
|
27
27
|
const [errors, setErrors] = useState({});
|
|
28
|
+
/*
|
|
29
|
+
* The drawer's `errors` state is already submit-gated (only `validate()`
|
|
30
|
+
* writes it), but the summary below reads `liveErrors`, which is a pure
|
|
31
|
+
* function of the current values and therefore non-empty from first paint.
|
|
32
|
+
* Without this flag the drawer opens listing every field it is about to
|
|
33
|
+
* ask for. Reset on close, so reopening starts clean.
|
|
34
|
+
*/
|
|
35
|
+
const [submitAttemptCount, setSubmitAttemptCount] = useState(0);
|
|
36
|
+
const submitAttempted = submitAttemptCount > 0;
|
|
28
37
|
const [submitting, setSubmitting] = useState(false);
|
|
29
38
|
// Re-initialise when defaultValues change (e.g. edit mode)
|
|
30
39
|
useEffect(() => {
|
|
@@ -101,6 +110,7 @@ export function QuickFormDrawer({ isOpen, onClose, title, fields, defaultValues,
|
|
|
101
110
|
})();
|
|
102
111
|
const handleSubmit = useCallback(async (e) => {
|
|
103
112
|
e?.preventDefault();
|
|
113
|
+
setSubmitAttemptCount((n) => n + 1);
|
|
104
114
|
if (!validate())
|
|
105
115
|
return;
|
|
106
116
|
setSubmitting(true);
|
|
@@ -118,6 +128,7 @@ export function QuickFormDrawer({ isOpen, onClose, title, fields, defaultValues,
|
|
|
118
128
|
const handleClose = useCallback(() => {
|
|
119
129
|
setValues(initValues(fields, defaultValues));
|
|
120
130
|
setErrors({});
|
|
131
|
+
setSubmitAttemptCount(0);
|
|
121
132
|
onClose();
|
|
122
133
|
}, [fields, defaultValues, onClose]);
|
|
123
134
|
// Keyboard: Esc → close; Tab → trap focus
|
|
@@ -175,7 +186,10 @@ export function QuickFormDrawer({ isOpen, onClose, title, fields, defaultValues,
|
|
|
175
186
|
isDirty: false,
|
|
176
187
|
isSubmitting: submitting,
|
|
177
188
|
stepErrorCounts: [],
|
|
178
|
-
|
|
189
|
+
submitAttempted,
|
|
190
|
+
submitAttemptCount,
|
|
191
|
+
markSubmitAttempted: () => setSubmitAttemptCount((n) => n + 1),
|
|
192
|
+
}), [liveErrors, submitting, submitAttempted, submitAttemptCount]);
|
|
179
193
|
if (!isOpen)
|
|
180
194
|
return null;
|
|
181
195
|
const busy = submitting || isLoading;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { type ReactNode } from "react";
|
|
2
2
|
import type { JsonValue } from "@mohasinac/appkit/client";
|
|
3
3
|
import type { ZodTypeAny } from "zod";
|
|
4
|
-
import type
|
|
4
|
+
import { type FormShellStep } from "../../ui/forms/FormShell";
|
|
5
5
|
/**
|
|
6
6
|
* A collapsible form segment. Replaces `StepDef` — the differences are the
|
|
7
7
|
* point, not incidental:
|
|
@@ -64,6 +64,20 @@ export interface SectionFormProps<T extends object = Record<string, JsonValue>>
|
|
|
64
64
|
expandMode?: "single" | "multi";
|
|
65
65
|
/** Suppress the built-in submit row (a FormShell footer owns it instead). */
|
|
66
66
|
hideActions?: boolean;
|
|
67
|
+
/**
|
|
68
|
+
* Secondary action for the pinned mobile bar. Omit on a settings page with
|
|
69
|
+
* nowhere to go back to — the bar then shows Save alone.
|
|
70
|
+
*/
|
|
71
|
+
onCancel?: () => void;
|
|
72
|
+
cancelLabel?: string;
|
|
73
|
+
/**
|
|
74
|
+
* Publish Save/Cancel and the error sheet into the mobile bottom-chrome
|
|
75
|
+
* tier. On by default: a SectionForm is a page-level form, which is exactly
|
|
76
|
+
* the case the pinned bar exists for. The hook suppresses itself inside a
|
|
77
|
+
* Modal or SideDrawer regardless, so this is only for the rarer case of a
|
|
78
|
+
* page that wants to own the bar itself.
|
|
79
|
+
*/
|
|
80
|
+
bottomBar?: boolean;
|
|
67
81
|
/**
|
|
68
82
|
* Fires whenever live validation recomputes. SectionForm deliberately does
|
|
69
83
|
* NOT mount its own `FormShellContext.Provider` — the owning view keeps the
|
|
@@ -95,5 +109,5 @@ export interface UseSectionFormNavResult<T extends object> {
|
|
|
95
109
|
export declare function useSectionFormNav<T extends object = Record<string, JsonValue>>(sections: SectionDef<T>[], values?: T): UseSectionFormNavResult<T>;
|
|
96
110
|
/** DOM id for a section panel — kept in one place so nav and render agree. */
|
|
97
111
|
export declare function sectionAnchorId(id: string): string;
|
|
98
|
-
export declare function SectionForm<T extends object = Record<string, JsonValue>>({ sections, values, onChange, onSubmit, submitLabel, isLoading, schema, openIds: controlledOpenIds, onOpenChange, expandMode, hideActions, onValidationChange, }: SectionFormProps<T>): import("react").JSX.Element;
|
|
112
|
+
export declare function SectionForm<T extends object = Record<string, JsonValue>>({ sections, values, onChange, onSubmit, submitLabel, isLoading, schema, openIds: controlledOpenIds, onOpenChange, expandMode, hideActions, onValidationChange, onCancel, cancelLabel, bottomBar, }: SectionFormProps<T>): import("react").JSX.Element;
|
|
99
113
|
export default SectionForm;
|
|
@@ -5,6 +5,8 @@ import { Button } from "../../ui/components/Button";
|
|
|
5
5
|
import { CollapsibleSection } from "../../ui/components/CollapsibleSection";
|
|
6
6
|
import { Badge } from "../../ui/components/Badge";
|
|
7
7
|
import { Row, Span, Stack } from "../../ui";
|
|
8
|
+
import { FormShellContext } from "../../ui/forms/FormShell";
|
|
9
|
+
import { useFormBottomActions } from "../layout/hooks/useFormBottomActions";
|
|
8
10
|
import { FormSchemaContext } from "./FormShell";
|
|
9
11
|
/** Sections in render order: required first, original order preserved within each group. */
|
|
10
12
|
export function orderSections(sections) {
|
|
@@ -53,8 +55,15 @@ export function useSectionFormNav(sections, values) {
|
|
|
53
55
|
export function sectionAnchorId(id) {
|
|
54
56
|
return `section-${id}`;
|
|
55
57
|
}
|
|
56
|
-
export function SectionForm({ sections, values, onChange, onSubmit, submitLabel = "Save", isLoading = false, schema, openIds: controlledOpenIds, onOpenChange, expandMode = "multi", hideActions = false, onValidationChange, }) {
|
|
58
|
+
export function SectionForm({ sections, values, onChange, onSubmit, submitLabel = "Save", isLoading = false, schema, openIds: controlledOpenIds, onOpenChange, expandMode = "multi", hideActions = false, onValidationChange, onCancel, cancelLabel, bottomBar = true, }) {
|
|
57
59
|
const inheritedSchema = useContext(FormSchemaContext);
|
|
60
|
+
/*
|
|
61
|
+
* SectionForm deliberately does not OWN a FormShellContext (the owning view
|
|
62
|
+
* keeps the provider — see the prop docs above), but it does own the submit
|
|
63
|
+
* button, so it is the one place that knows an attempt happened. Null when
|
|
64
|
+
* a caller renders it outside a provider, which is legitimate.
|
|
65
|
+
*/
|
|
66
|
+
const shellCtx = useContext(FormShellContext);
|
|
58
67
|
const [fieldErrors, setFieldErrors] = useState({});
|
|
59
68
|
const [uncontrolledOpenIds, setUncontrolledOpenIds] = useState(() => orderSections(sections).filter((s) => s.required).map((s) => s.id));
|
|
60
69
|
const ordered = useMemo(() => orderSections(sections.filter((s) => (s.when ? s.when(values) : true))), [sections, values]);
|
|
@@ -100,6 +109,23 @@ export function SectionForm({ sections, values, onChange, onSubmit, submitLabel
|
|
|
100
109
|
onChange(partial);
|
|
101
110
|
runValidation({ ...values, ...partial });
|
|
102
111
|
}, [values, onChange, runValidation]);
|
|
112
|
+
/*
|
|
113
|
+
* The one submit path, shared by the inline button below and the pinned
|
|
114
|
+
* mobile bar — so an attempt is recorded and the error sheet revealed
|
|
115
|
+
* identically whichever the user presses.
|
|
116
|
+
*/
|
|
117
|
+
const handleSubmit = useCallback(() => {
|
|
118
|
+
shellCtx?.markSubmitAttempted();
|
|
119
|
+
void onSubmit();
|
|
120
|
+
}, [shellCtx, onSubmit]);
|
|
121
|
+
useFormBottomActions({
|
|
122
|
+
onSubmit: handleSubmit,
|
|
123
|
+
onCancel,
|
|
124
|
+
submitLabel,
|
|
125
|
+
cancelLabel,
|
|
126
|
+
isLoading,
|
|
127
|
+
enabled: bottomBar && !hideActions,
|
|
128
|
+
});
|
|
103
129
|
const toggle = useCallback((id) => {
|
|
104
130
|
const isOpen = openIds.includes(id);
|
|
105
131
|
if (expandMode === "single") {
|
|
@@ -123,6 +149,6 @@ export function SectionForm({ sections, values, onChange, onSubmit, submitLabel
|
|
|
123
149
|
toggle(section.id); }, keepMounted: section.keepMounted, renderHeaderExtra: errorCount > 0
|
|
124
150
|
? () => (_jsxs(Badge, { variant: "danger", children: [errorCount, " ", errorCount === 1 ? "issue" : "issues"] }))
|
|
125
151
|
: undefined, children: section.render({ values, onChange: handleFieldChange, errors: fieldErrors }) }, section.id));
|
|
126
|
-
}), !hideActions && (_jsx(Row, { justify: "end", paddingY: "y-sm", children: _jsx(Button, { variant: "primary", type: "submit", onClick:
|
|
152
|
+
}), !hideActions && (_jsx(Row, { justify: "end", paddingY: "y-sm", children: _jsx(Button, { variant: "primary", type: "submit", onClick: handleSubmit, isLoading: isLoading, disabled: isLoading, children: submitLabel }) }))] }));
|
|
127
153
|
}
|
|
128
154
|
export default SectionForm;
|
|
@@ -11,6 +11,7 @@ import { ACTIONS } from "../../../_internal/shared/actions/action-registry";
|
|
|
11
11
|
import { createShipmentSchema } from "../schemas/validation";
|
|
12
12
|
import { useShipment } from "../hooks/useShipments";
|
|
13
13
|
import { formatCurrency } from "../../../utils/number.formatter";
|
|
14
|
+
import { RecordStatusTimeline } from "../../status-history/components/RecordStatusTimeline";
|
|
14
15
|
const STATUS_OPTIONS = [
|
|
15
16
|
{ label: "Planning", value: "planning" },
|
|
16
17
|
{ label: "Ordered", value: "ordered" },
|
|
@@ -77,7 +78,9 @@ export function AdminShipmentEditorView({ shipmentId, onSaved }) {
|
|
|
77
78
|
onSaved?.(saved.id);
|
|
78
79
|
},
|
|
79
80
|
});
|
|
80
|
-
return (_jsx(Form, { schema: createShipmentSchema, spacing: "md", children: ({ setFieldError }) => (_jsxs(_Fragment, { children: [_jsxs(Grid, { cols: 2, gap: "md", children: [_jsx(FieldInput, { name: "shipmentNumber", label: "Shipment Number", required: true, value: shipmentNumber, onChange: setShipmentNumber, placeholder: "SH-2026-0001" }), _jsx(FieldInput, { name: "supplierName", label: "Supplier Name", required: true, value: supplierName, onChange: setSupplierName }), _jsx(FieldInput, { name: "originCountry", label: "Origin Country", value: originCountry, onChange: setOriginCountry }), _jsx(FieldSelect, { name: "status", label: "Status", required: true, value: status, onChange: setStatus, options: STATUS_OPTIONS }), _jsx(FieldInput, { name: "trackingNumber", label: "Tracking Number", value: trackingNumber, onChange: setTrackingNumber }), _jsx(FieldInput, { name: "carrier", label: "Carrier", value: carrier, onChange: setCarrier }), _jsx(FieldInput, { name: "etaDate", label: "ETA Date", type: "date", value: etaDate, onChange: setEtaDate }), _jsx(FieldInput, { name: "laborHoursSpent", label: "Labor Hours Spent", type: "number", min: "0", value: laborHoursSpent, onChange: setLaborHoursSpent }), _jsx(FieldInput, { name: "customsTotal", label: "Customs Total (\u20B9)", type: "number", min: "0", value: customsTotalRupees, onChange: setCustomsTotalRupees, hint: "Split across lots by declared value" }), _jsx(FieldInput, { name: "shippingTotal", label: "Shipping Total (\u20B9)", type: "number", min: "0", value: shippingTotalRupees, onChange: setShippingTotalRupees, hint: "Split across lots by weight" })] }), _jsx(FieldTextarea, { name: "notes", label: "Notes", value: notes, onChange: setNotes, rows: 3 }), _jsx(FormErrorSummary, {}), _jsx(Button, { type: "button", isLoading: saveMutation.isPending, onClick: () => {
|
|
81
|
+
return (_jsx(Form, { schema: createShipmentSchema, spacing: "md", children: ({ setFieldError, markSubmitAttempted }) => (_jsxs(_Fragment, { children: [_jsxs(Grid, { cols: 2, gap: "md", children: [_jsx(FieldInput, { name: "shipmentNumber", label: "Shipment Number", required: true, value: shipmentNumber, onChange: setShipmentNumber, placeholder: "SH-2026-0001" }), _jsx(FieldInput, { name: "supplierName", label: "Supplier Name", required: true, value: supplierName, onChange: setSupplierName }), _jsx(FieldInput, { name: "originCountry", label: "Origin Country", value: originCountry, onChange: setOriginCountry }), _jsx(FieldSelect, { name: "status", label: "Status", required: true, value: status, onChange: setStatus, options: STATUS_OPTIONS }), _jsx(FieldInput, { name: "trackingNumber", label: "Tracking Number", value: trackingNumber, onChange: setTrackingNumber }), _jsx(FieldInput, { name: "carrier", label: "Carrier", value: carrier, onChange: setCarrier }), _jsx(FieldInput, { name: "etaDate", label: "ETA Date", type: "date", value: etaDate, onChange: setEtaDate }), _jsx(FieldInput, { name: "laborHoursSpent", label: "Labor Hours Spent", type: "number", min: "0", value: laborHoursSpent, onChange: setLaborHoursSpent }), _jsx(FieldInput, { name: "customsTotal", label: "Customs Total (\u20B9)", type: "number", min: "0", value: customsTotalRupees, onChange: setCustomsTotalRupees, hint: "Split across lots by declared value" }), _jsx(FieldInput, { name: "shippingTotal", label: "Shipping Total (\u20B9)", type: "number", min: "0", value: shippingTotalRupees, onChange: setShippingTotalRupees, hint: "Split across lots by weight" })] }), _jsx(FieldTextarea, { name: "notes", label: "Notes", value: notes, onChange: setNotes, rows: 3 }), _jsx(FormErrorSummary, {}), _jsx(Button, { type: "button", isLoading: saveMutation.isPending, onClick: () => {
|
|
82
|
+
// type="button" — no native submit, so unhide the summary here.
|
|
83
|
+
markSubmitAttempted();
|
|
81
84
|
const parsed = createShipmentSchema.safeParse({
|
|
82
85
|
shipmentNumber,
|
|
83
86
|
supplierName,
|
|
@@ -92,7 +95,7 @@ export function AdminShipmentEditorView({ shipmentId, onSaved }) {
|
|
|
92
95
|
return;
|
|
93
96
|
}
|
|
94
97
|
saveMutation.mutate();
|
|
95
|
-
}, children: isCreate ? "Create Shipment" : "Save Changes" }), !isCreate && shipmentId && (_jsx(ShipmentLotsSection, { shipmentId: shipmentId, lots: lots, onLotsChanged: refetchLots }))] })) }));
|
|
98
|
+
}, children: isCreate ? "Create Shipment" : "Save Changes" }), !isCreate && shipmentId && (_jsx(ShipmentLotsSection, { shipmentId: shipmentId, lots: lots, onLotsChanged: refetchLots })), !isCreate && (_jsx(RecordStatusTimeline, { entries: shipment?.statusHistory, truncatedCount: shipment?.statusHistoryTruncated }))] })) }));
|
|
96
99
|
}
|
|
97
100
|
function ShipmentLotsSection({ shipmentId, lots, onLotsChanged, }) {
|
|
98
101
|
const [showAddLot, setShowAddLot] = React.useState(false);
|
|
@@ -4,8 +4,26 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { BaseRepository, parseSieveDateValue, type SieveModel, type FirebaseSieveResult } from "../../../providers/db-firebase";
|
|
6
6
|
import { type ShipmentDocument, type ShipmentCreateInput, type ShipmentUpdateInput } from "../schemas/firestore";
|
|
7
|
+
import { type HistoryActor } from "../../../_internal/shared/history/index";
|
|
8
|
+
/** Who/why for a shipment write that lands in `statusHistory`. */
|
|
9
|
+
export interface ShipmentWriteContext {
|
|
10
|
+
actor?: HistoryActor;
|
|
11
|
+
trigger?: string;
|
|
12
|
+
reason?: string;
|
|
13
|
+
note?: string;
|
|
14
|
+
}
|
|
7
15
|
export declare class ShipmentsRepository extends BaseRepository<ShipmentDocument> {
|
|
8
16
|
constructor();
|
|
17
|
+
/**
|
|
18
|
+
* The admin edit path. Two route callers, no funnel before this.
|
|
19
|
+
*
|
|
20
|
+
* `prior` is optional and unused by the current caller — the admin PATCH
|
|
21
|
+
* route does NOT fetch the shipment first (its GET sibling does), so this
|
|
22
|
+
* takes one read. That is the honest cost here rather than a hidden one:
|
|
23
|
+
* without the before-value a slipped ETA would record as
|
|
24
|
+
* `undefined → newDate`, which cannot answer "how many times did this move".
|
|
25
|
+
*/
|
|
26
|
+
adminUpdate(shipmentId: string, patch: Partial<ShipmentDocument>, ctx?: ShipmentWriteContext, prior?: ShipmentDocument | null): Promise<ShipmentDocument>;
|
|
9
27
|
findByShipmentNumber(shipmentNumber: string): Promise<ShipmentDocument | null>;
|
|
10
28
|
private assertShipmentNumberAvailable;
|
|
11
29
|
create(input: ShipmentCreateInput): Promise<ShipmentDocument>;
|
|
@@ -4,11 +4,33 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { BaseRepository, prepareForFirestore, parseSieveDateValue, } from "../../../providers/db-firebase";
|
|
6
6
|
import { ConflictError } from "../../../errors";
|
|
7
|
-
import { SHIPMENT_COLLECTION, SHIPMENT_FIELDS, SHIPMENT_ITEM_COLLECTION, DEFAULT_SHIPMENT_TOTALS, createShipmentId, } from "../schemas/firestore";
|
|
7
|
+
import { SHIPMENT_COLLECTION, SHIPMENT_FIELDS, SHIPMENT_ITEM_COLLECTION, DEFAULT_SHIPMENT_TOTALS, createShipmentId, SHIPMENT_TRACKED_FIELDS, SHIPMENT_HISTORY_PII_FIELDS, } from "../schemas/firestore";
|
|
8
|
+
import { withHistory } from "../../../_internal/shared/history/index";
|
|
8
9
|
export class ShipmentsRepository extends BaseRepository {
|
|
9
10
|
constructor() {
|
|
10
11
|
super(SHIPMENT_COLLECTION);
|
|
11
12
|
}
|
|
13
|
+
/**
|
|
14
|
+
* The admin edit path. Two route callers, no funnel before this.
|
|
15
|
+
*
|
|
16
|
+
* `prior` is optional and unused by the current caller — the admin PATCH
|
|
17
|
+
* route does NOT fetch the shipment first (its GET sibling does), so this
|
|
18
|
+
* takes one read. That is the honest cost here rather than a hidden one:
|
|
19
|
+
* without the before-value a slipped ETA would record as
|
|
20
|
+
* `undefined → newDate`, which cannot answer "how many times did this move".
|
|
21
|
+
*/
|
|
22
|
+
async adminUpdate(shipmentId, patch, ctx, prior) {
|
|
23
|
+
const current = prior !== undefined ? prior : await this.findById(shipmentId);
|
|
24
|
+
const withEntry = withHistory(current, patch, {
|
|
25
|
+
tracked: SHIPMENT_TRACKED_FIELDS,
|
|
26
|
+
actor: ctx?.actor ?? { role: "system" },
|
|
27
|
+
trigger: ctx?.trigger ?? "adminUpdateShipment",
|
|
28
|
+
reason: ctx?.reason,
|
|
29
|
+
note: ctx?.note,
|
|
30
|
+
piiFields: SHIPMENT_HISTORY_PII_FIELDS,
|
|
31
|
+
});
|
|
32
|
+
return this.update(shipmentId, withEntry ?? patch);
|
|
33
|
+
}
|
|
12
34
|
async findByShipmentNumber(shipmentNumber) {
|
|
13
35
|
return this.findOneBy(SHIPMENT_FIELDS.SHIPMENT_NUMBER, shipmentNumber);
|
|
14
36
|
}
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
* never inline in a Vercel route or React component.
|
|
14
14
|
*/
|
|
15
15
|
import { type GenerateShipmentIdInput } from "../../../utils/id-generators";
|
|
16
|
+
import type { StatusChangeEntry } from "../../../_internal/shared/history/types";
|
|
16
17
|
import type { BaseDocument } from "../../../_internal/shared/types/base-document";
|
|
17
18
|
import type { MediaField } from "../../media/types";
|
|
18
19
|
import type { ListingType } from "../../products/types";
|
|
@@ -57,6 +58,9 @@ export interface ShipmentDocument extends BaseDocument {
|
|
|
57
58
|
totals: ShipmentTotals;
|
|
58
59
|
totalsComputedAt?: Date;
|
|
59
60
|
createdBy: string;
|
|
61
|
+
/** Who changed what, when, and why. See § "Status History" in CLAUDE.md. */
|
|
62
|
+
statusHistory?: StatusChangeEntry[];
|
|
63
|
+
statusHistoryTruncated?: number;
|
|
60
64
|
}
|
|
61
65
|
export interface ShipmentLot extends BaseDocument {
|
|
62
66
|
shipmentId: string;
|
|
@@ -148,3 +152,13 @@ export declare const shipmentQueryHelpers: {
|
|
|
148
152
|
readonly notCancelled: () => readonly ["status", "!=", ShipmentStatus];
|
|
149
153
|
};
|
|
150
154
|
export declare function createShipmentId(input: GenerateShipmentIdInput): string;
|
|
155
|
+
/**
|
|
156
|
+
* The fields whose changes earn a timeline entry.
|
|
157
|
+
*
|
|
158
|
+
* A procurement shipment's dates ARE its status for practical purposes — "ETA
|
|
159
|
+
* slipped twice before it landed" is the question this answers, and each slip
|
|
160
|
+
* overwrites the previous `etaDate`.
|
|
161
|
+
*/
|
|
162
|
+
export declare const SHIPMENT_TRACKED_FIELDS: readonly ["status", "etaDate", "receivedDate", "trackingNumber"];
|
|
163
|
+
/** PII on this document, for `withHistory`'s scrub. */
|
|
164
|
+
export declare const SHIPMENT_HISTORY_PII_FIELDS: readonly ["supplierEmail", "supplierPhone", "contactName"];
|
|
@@ -74,3 +74,22 @@ export const shipmentQueryHelpers = {
|
|
|
74
74
|
export function createShipmentId(input) {
|
|
75
75
|
return generateShipmentId(input);
|
|
76
76
|
}
|
|
77
|
+
/**
|
|
78
|
+
* The fields whose changes earn a timeline entry.
|
|
79
|
+
*
|
|
80
|
+
* A procurement shipment's dates ARE its status for practical purposes — "ETA
|
|
81
|
+
* slipped twice before it landed" is the question this answers, and each slip
|
|
82
|
+
* overwrites the previous `etaDate`.
|
|
83
|
+
*/
|
|
84
|
+
export const SHIPMENT_TRACKED_FIELDS = [
|
|
85
|
+
"status",
|
|
86
|
+
"etaDate",
|
|
87
|
+
"receivedDate",
|
|
88
|
+
"trackingNumber",
|
|
89
|
+
];
|
|
90
|
+
/** PII on this document, for `withHistory`'s scrub. */
|
|
91
|
+
export const SHIPMENT_HISTORY_PII_FIELDS = [
|
|
92
|
+
"supplierEmail",
|
|
93
|
+
"supplierPhone",
|
|
94
|
+
"contactName",
|
|
95
|
+
];
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { StatusChangeEntry } from "../../../_internal/shared/history/types";
|
|
2
|
+
export interface RecordStatusTimelineProps {
|
|
3
|
+
/** The record's `statusHistory`. Absent or empty renders `emptyLabel`. */
|
|
4
|
+
entries?: StatusChangeEntry[];
|
|
5
|
+
/** The record's `statusHistoryTruncated` — "earlier history trimmed". */
|
|
6
|
+
truncatedCount?: number;
|
|
7
|
+
title?: string;
|
|
8
|
+
emptyLabel?: string;
|
|
9
|
+
className?: string;
|
|
10
|
+
}
|
|
11
|
+
export declare function RecordStatusTimeline({ entries, truncatedCount, title, emptyLabel, className, }: RecordStatusTimelineProps): import("react").JSX.Element;
|
|
12
|
+
export default RecordStatusTimeline;
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import { StatusTimeline } from "./StatusTimeline";
|
|
4
|
+
/**
|
|
5
|
+
* The timeline for an entity with no fixed phase vocabulary.
|
|
6
|
+
*
|
|
7
|
+
* ## Why this exists rather than seven more wrappers
|
|
8
|
+
*
|
|
9
|
+
* `OrderStatusTimeline` and `OfferPhaseTimeline` each supply a KNOWN sequence
|
|
10
|
+
* of phases — placed → paid → shipped → delivered, made → countered →
|
|
11
|
+
* accepted → paid — and render the steps a record has not reached yet as
|
|
12
|
+
* upcoming. That shape earns a dedicated wrapper because the vocabulary is
|
|
13
|
+
* part of the product.
|
|
14
|
+
*
|
|
15
|
+
* Support tickets, payouts, stores, scammer profiles, bids, shipments,
|
|
16
|
+
* catalogue items and ads have no such sequence: a store goes active →
|
|
17
|
+
* suspended → active, a bid goes active → outbid → active → won, a payout
|
|
18
|
+
* retries. There is nothing to pre-render. So the timeline is simply the
|
|
19
|
+
* entries that happened, in order — and writing that eight times would be
|
|
20
|
+
* eight chances for the actor label, the em-dash rule or the negative tone to
|
|
21
|
+
* drift, which is the bug-fix multiplier the Duplication Framework names.
|
|
22
|
+
*
|
|
23
|
+
* ## Never fabricate a step
|
|
24
|
+
*
|
|
25
|
+
* A record written before its entity adopted history has no `statusHistory`
|
|
26
|
+
* at all. That renders `emptyLabel`, NOT a step derived from `updatedAt` —
|
|
27
|
+
* "last write of any kind" is not an event, and a made-up date makes the
|
|
28
|
+
* record lie. Same rule the offer timeline follows for `expired`.
|
|
29
|
+
*/
|
|
30
|
+
/**
|
|
31
|
+
* Statuses that render in the error tone.
|
|
32
|
+
*
|
|
33
|
+
* Deliberately a shared set rather than a per-entity prop: these words mean
|
|
34
|
+
* the same thing on every entity that uses them, and a per-entity list is one
|
|
35
|
+
* more thing to keep aligned. A value not listed renders neutral, which is
|
|
36
|
+
* the safe direction — a real transition shown in the wrong tone is a smaller
|
|
37
|
+
* defect than a normal one shown as a failure.
|
|
38
|
+
*/
|
|
39
|
+
const NEGATIVE_STATUSES = new Set([
|
|
40
|
+
"cancelled",
|
|
41
|
+
"canceled",
|
|
42
|
+
"rejected",
|
|
43
|
+
"failed",
|
|
44
|
+
"forfeited",
|
|
45
|
+
"suspended",
|
|
46
|
+
"removed",
|
|
47
|
+
"declined",
|
|
48
|
+
"expired",
|
|
49
|
+
"lost",
|
|
50
|
+
]);
|
|
51
|
+
/** Ended, but not a failure — the record simply moved on. */
|
|
52
|
+
const MUTED_STATUSES = new Set(["outbid", "closed", "superseded", "archived"]);
|
|
53
|
+
function humanise(value) {
|
|
54
|
+
if (value === null || value === undefined || value === "")
|
|
55
|
+
return "—";
|
|
56
|
+
if (typeof value === "boolean")
|
|
57
|
+
return value ? "yes" : "no";
|
|
58
|
+
const s = String(value);
|
|
59
|
+
return s.replace(/_/g, " ").replace(/^./, (c) => c.toUpperCase());
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* One entry → one step.
|
|
63
|
+
*
|
|
64
|
+
* `status` is the headline when it changed; otherwise the step is named after
|
|
65
|
+
* the fields that did move, so an entry recording only "ETA moved" still
|
|
66
|
+
* reads as a real event rather than a blank row.
|
|
67
|
+
*/
|
|
68
|
+
function toStep(entry, index) {
|
|
69
|
+
const statusChange = entry.changes["status"] ?? entry.changes["listingStatus"];
|
|
70
|
+
const to = statusChange?.to;
|
|
71
|
+
const label = statusChange
|
|
72
|
+
? humanise(to)
|
|
73
|
+
: Object.keys(entry.changes).map(humanise).join(", ") || "Updated";
|
|
74
|
+
const toKey = typeof to === "string" ? to.toLowerCase() : "";
|
|
75
|
+
return {
|
|
76
|
+
key: `${entry.at instanceof Date ? entry.at.toISOString() : String(entry.at)}-${index}`,
|
|
77
|
+
label,
|
|
78
|
+
date: entry.at instanceof Date ? entry.at.toISOString() : String(entry.at),
|
|
79
|
+
negative: NEGATIVE_STATUSES.has(toKey),
|
|
80
|
+
muted: MUTED_STATUSES.has(toKey),
|
|
81
|
+
actorRole: entry.actorRole,
|
|
82
|
+
reason: entry.reason,
|
|
83
|
+
note: entry.note,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
export function RecordStatusTimeline({ entries, truncatedCount, title = "History", emptyLabel = "No recorded history yet", className, }) {
|
|
87
|
+
const steps = (entries ?? []).map(toStep);
|
|
88
|
+
return (_jsx(StatusTimeline, { title: title, steps: steps, truncatedCount: truncatedCount, emptyLabel: emptyLabel, className: className }));
|
|
89
|
+
}
|
|
90
|
+
export default RecordStatusTimeline;
|
|
@@ -9,3 +9,5 @@
|
|
|
9
9
|
*/
|
|
10
10
|
export { StatusTimeline, stepsFromEntries } from "./components/StatusTimeline";
|
|
11
11
|
export type { StatusTimelineProps, TimelineEntry, TimelineStep } from "./components/StatusTimeline";
|
|
12
|
+
export { RecordStatusTimeline } from "./components/RecordStatusTimeline";
|
|
13
|
+
export type { RecordStatusTimelineProps } from "./components/RecordStatusTimeline";
|
|
@@ -6,6 +6,14 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import { BaseRepository, type SieveModel, type FirebaseSieveResult, type DocumentSnapshot } from "../../../providers/db-firebase";
|
|
8
8
|
import { StoreDocument } from "../schemas";
|
|
9
|
+
import { type HistoryActor } from "../../../_internal/shared/history/index";
|
|
10
|
+
/** Who/why for a store write that lands in `statusHistory`. */
|
|
11
|
+
export interface StoreWriteContext {
|
|
12
|
+
actor?: HistoryActor;
|
|
13
|
+
trigger?: string;
|
|
14
|
+
reason?: string;
|
|
15
|
+
note?: string;
|
|
16
|
+
}
|
|
9
17
|
export declare class StoreRepository extends BaseRepository<StoreDocument> {
|
|
10
18
|
constructor();
|
|
11
19
|
private encryptSecrets;
|
|
@@ -33,9 +41,29 @@ export declare class StoreRepository extends BaseRepository<StoreDocument> {
|
|
|
33
41
|
*/
|
|
34
42
|
updateStore(storeSlug: string, data: Partial<Omit<StoreDocument, "id" | "storeSlug" | "ownerId" | "createdAt">>): Promise<StoreDocument>;
|
|
35
43
|
/**
|
|
36
|
-
* Admin: approve or
|
|
44
|
+
* Admin: approve, reject or suspend a store — and the ONLY correct way to
|
|
45
|
+
* move `status`, because `isPublic` must move with it.
|
|
46
|
+
*
|
|
47
|
+
* `status` and `isPublic` are two distinct fields and public visibility
|
|
48
|
+
* gates on `isPublic`. The admin PATCH route wrote `status` directly and
|
|
49
|
+
* never touched `isPublic` until 2026-08-26, so approving a pending store
|
|
50
|
+
* left it `isPublic: false` — active, and invisible on every public
|
|
51
|
+
* surface, with nothing anywhere reporting a problem. Same two-field trap
|
|
52
|
+
* CLAUDE.md already records for `becomeSeller` / `createStore`.
|
|
53
|
+
*
|
|
54
|
+
* `extra` carries the other admin fields so a single save is a single
|
|
55
|
+
* write and a single timeline entry; `prior` keeps that entry read-free
|
|
56
|
+
* for the route, which has already fetched the store to 404 on a missing
|
|
57
|
+
* one (Rule #6).
|
|
58
|
+
*/
|
|
59
|
+
setStatus(storeSlug: string, status: StoreDocument["status"], extra?: Partial<StoreDocument>, ctx?: StoreWriteContext, prior?: StoreDocument | null): Promise<StoreDocument>;
|
|
60
|
+
/**
|
|
61
|
+
* Admin field edits that do NOT move `status` — verification, featuring,
|
|
62
|
+
* capabilities, the suspension reason on an already-suspended store.
|
|
37
63
|
*/
|
|
38
|
-
|
|
64
|
+
adminUpdate(storeSlug: string, patch: Partial<StoreDocument>, ctx?: StoreWriteContext, prior?: StoreDocument | null): Promise<StoreDocument>;
|
|
65
|
+
/** Append a timeline entry when the patch touches a tracked field. */
|
|
66
|
+
private withStoreHistory;
|
|
39
67
|
/**
|
|
40
68
|
* Paginated list of stores with Sieve filtering/sorting.
|
|
41
69
|
* Filters to active + public stores for the public listing.
|
|
@@ -6,10 +6,11 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import { BaseRepository, prepareForFirestore, applySieveToFirestore, } from "../../../providers/db-firebase";
|
|
8
8
|
import { increment } from "../../../contracts/field-ops";
|
|
9
|
-
import { STORE_COLLECTION, STORE_FIELDS, DEFAULT_STORE_DATA, StoreStatusValues, } from "../schemas";
|
|
9
|
+
import { STORE_COLLECTION, STORE_FIELDS, DEFAULT_STORE_DATA, StoreStatusValues, STORE_TRACKED_FIELDS, STORE_HISTORY_PII_FIELDS, } from "../schemas";
|
|
10
10
|
import { DatabaseError } from "../../../errors";
|
|
11
11
|
import { normalizeError } from "../../../errors/normalize";
|
|
12
12
|
import { encryptSecret, decryptSecret, } from "../../../security/settings-encryption";
|
|
13
|
+
import { withHistory } from "../../../_internal/shared/history/index";
|
|
13
14
|
export class StoreRepository extends BaseRepository {
|
|
14
15
|
constructor() {
|
|
15
16
|
super(STORE_COLLECTION);
|
|
@@ -97,16 +98,54 @@ export class StoreRepository extends BaseRepository {
|
|
|
97
98
|
});
|
|
98
99
|
}
|
|
99
100
|
/**
|
|
100
|
-
* Admin: approve or
|
|
101
|
+
* Admin: approve, reject or suspend a store — and the ONLY correct way to
|
|
102
|
+
* move `status`, because `isPublic` must move with it.
|
|
103
|
+
*
|
|
104
|
+
* `status` and `isPublic` are two distinct fields and public visibility
|
|
105
|
+
* gates on `isPublic`. The admin PATCH route wrote `status` directly and
|
|
106
|
+
* never touched `isPublic` until 2026-08-26, so approving a pending store
|
|
107
|
+
* left it `isPublic: false` — active, and invisible on every public
|
|
108
|
+
* surface, with nothing anywhere reporting a problem. Same two-field trap
|
|
109
|
+
* CLAUDE.md already records for `becomeSeller` / `createStore`.
|
|
110
|
+
*
|
|
111
|
+
* `extra` carries the other admin fields so a single save is a single
|
|
112
|
+
* write and a single timeline entry; `prior` keeps that entry read-free
|
|
113
|
+
* for the route, which has already fetched the store to 404 on a missing
|
|
114
|
+
* one (Rule #6).
|
|
101
115
|
*/
|
|
102
|
-
async setStatus(storeSlug, status) {
|
|
103
|
-
|
|
116
|
+
async setStatus(storeSlug, status, extra, ctx, prior) {
|
|
117
|
+
const current = prior !== undefined ? prior : await this.findById(storeSlug);
|
|
118
|
+
return this.update(storeSlug, this.withStoreHistory(current, {
|
|
104
119
|
status,
|
|
105
120
|
// Auto-set isPublic when approved
|
|
106
121
|
...(status === StoreStatusValues.ACTIVE && { isPublic: true }),
|
|
107
122
|
...(status !== StoreStatusValues.ACTIVE && { isPublic: false }),
|
|
123
|
+
...extra,
|
|
108
124
|
updatedAt: new Date(),
|
|
125
|
+
}, ctx, "setStatus"));
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Admin field edits that do NOT move `status` — verification, featuring,
|
|
129
|
+
* capabilities, the suspension reason on an already-suspended store.
|
|
130
|
+
*/
|
|
131
|
+
async adminUpdate(storeSlug, patch, ctx, prior) {
|
|
132
|
+
const current = prior !== undefined ? prior : await this.findById(storeSlug);
|
|
133
|
+
return this.update(storeSlug, this.withStoreHistory(current, { ...patch, updatedAt: new Date() }, ctx, "adminUpdateStore"));
|
|
134
|
+
}
|
|
135
|
+
/** Append a timeline entry when the patch touches a tracked field. */
|
|
136
|
+
withStoreHistory(current, patch, ctx, defaultTrigger) {
|
|
137
|
+
const withEntry = withHistory(current, patch, {
|
|
138
|
+
tracked: STORE_TRACKED_FIELDS,
|
|
139
|
+
actor: ctx?.actor ?? { role: "system" },
|
|
140
|
+
trigger: ctx?.trigger ?? defaultTrigger,
|
|
141
|
+
reason: ctx?.reason,
|
|
142
|
+
note: ctx?.note,
|
|
143
|
+
// `mapDoc` decrypts whatsappConfig.accessToken on EVERY read, so a
|
|
144
|
+
// live Meta credential is in memory whenever this diff runs, and
|
|
145
|
+
// `encryptPiiFields` never descends into arrays.
|
|
146
|
+
piiFields: STORE_HISTORY_PII_FIELDS,
|
|
109
147
|
});
|
|
148
|
+
return withEntry ?? patch;
|
|
110
149
|
}
|
|
111
150
|
/**
|
|
112
151
|
* Paginated list of stores with Sieve filtering/sorting.
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* Canonical Firestore document interfaces, collection names, field constants,
|
|
5
5
|
* and helpers for the stores feature.
|
|
6
6
|
*/
|
|
7
|
+
import type { StatusChangeEntry } from "../../../_internal/shared/history/types";
|
|
7
8
|
import type { StoreCapability } from "../../auth/permissions/constants";
|
|
8
9
|
import type { BaseDocument } from "../../../_internal/shared/types/base-document";
|
|
9
10
|
export type ShippingProviderType = "self-courier" | "store-pickup" | "custom";
|
|
@@ -139,7 +140,35 @@ export interface StoreDocument extends BaseDocument {
|
|
|
139
140
|
/** When the seller first connected their account */
|
|
140
141
|
connectedAt?: Date;
|
|
141
142
|
};
|
|
143
|
+
/**
|
|
144
|
+
* Who changed what, when, and why. See § "Status History" in CLAUDE.md.
|
|
145
|
+
*
|
|
146
|
+
* The question this answers for a seller is "why is my store suspended and
|
|
147
|
+
* who did it" — previously recoverable only from `adminAuditLog`, which is
|
|
148
|
+
* admin-only and which the seller can never see.
|
|
149
|
+
*/
|
|
150
|
+
statusHistory?: StatusChangeEntry[];
|
|
151
|
+
statusHistoryTruncated?: number;
|
|
142
152
|
}
|
|
153
|
+
/**
|
|
154
|
+
* The fields whose changes earn a timeline entry.
|
|
155
|
+
*
|
|
156
|
+
* `isPublic` is tracked alongside `status` deliberately: the two are supposed
|
|
157
|
+
* to move together (`setStatus` syncs them) and the admin PATCH route did NOT
|
|
158
|
+
* until 2026-08-26, so an approved store stayed invisible. A timeline that
|
|
159
|
+
* shows them diverging is how the next instance gets noticed.
|
|
160
|
+
*
|
|
161
|
+
* `adminNotes` is excluded — its own schema comment says it is never shown to
|
|
162
|
+
* the store owner or the public, and `statusHistory` rides on the store
|
|
163
|
+
* document the owner can read.
|
|
164
|
+
*/
|
|
165
|
+
export declare const STORE_TRACKED_FIELDS: readonly ["status", "isPublic", "isVerified", "isFeatured", "suspensionReason", "capabilities"];
|
|
166
|
+
/**
|
|
167
|
+
* PII on this document, for `withHistory`'s scrub. `whatsappConfig.accessToken`
|
|
168
|
+
* is a live Meta credential; `mapDoc` decrypts it on EVERY read, so it is
|
|
169
|
+
* present in memory whenever a diff runs.
|
|
170
|
+
*/
|
|
171
|
+
export declare const STORE_HISTORY_PII_FIELDS: readonly ["accessToken", "payoutDetails", "upiVpa", "accountNumber"];
|
|
143
172
|
export type { StoreCapability } from "../../auth/permissions/constants";
|
|
144
173
|
export declare const STORE_COLLECTION: "stores";
|
|
145
174
|
export declare const STORE_FIELDS: {
|
|
@@ -12,6 +12,37 @@ export const StoreStatusValues = {
|
|
|
12
12
|
SUSPENDED: "suspended",
|
|
13
13
|
REJECTED: "rejected",
|
|
14
14
|
};
|
|
15
|
+
/**
|
|
16
|
+
* The fields whose changes earn a timeline entry.
|
|
17
|
+
*
|
|
18
|
+
* `isPublic` is tracked alongside `status` deliberately: the two are supposed
|
|
19
|
+
* to move together (`setStatus` syncs them) and the admin PATCH route did NOT
|
|
20
|
+
* until 2026-08-26, so an approved store stayed invisible. A timeline that
|
|
21
|
+
* shows them diverging is how the next instance gets noticed.
|
|
22
|
+
*
|
|
23
|
+
* `adminNotes` is excluded — its own schema comment says it is never shown to
|
|
24
|
+
* the store owner or the public, and `statusHistory` rides on the store
|
|
25
|
+
* document the owner can read.
|
|
26
|
+
*/
|
|
27
|
+
export const STORE_TRACKED_FIELDS = [
|
|
28
|
+
"status",
|
|
29
|
+
"isPublic",
|
|
30
|
+
"isVerified",
|
|
31
|
+
"isFeatured",
|
|
32
|
+
"suspensionReason",
|
|
33
|
+
"capabilities",
|
|
34
|
+
];
|
|
35
|
+
/**
|
|
36
|
+
* PII on this document, for `withHistory`'s scrub. `whatsappConfig.accessToken`
|
|
37
|
+
* is a live Meta credential; `mapDoc` decrypts it on EVERY read, so it is
|
|
38
|
+
* present in memory whenever a diff runs.
|
|
39
|
+
*/
|
|
40
|
+
export const STORE_HISTORY_PII_FIELDS = [
|
|
41
|
+
"accessToken",
|
|
42
|
+
"payoutDetails",
|
|
43
|
+
"upiVpa",
|
|
44
|
+
"accountNumber",
|
|
45
|
+
];
|
|
15
46
|
export const STORE_COLLECTION = "stores";
|
|
16
47
|
export const STORE_FIELDS = {
|
|
17
48
|
ID: "id",
|