@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
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
import { BaseRepository } from "../../../providers/db-firebase";
|
|
2
2
|
import type { FirebaseSieveFields, FirebaseSieveResult, SieveModel } from "../../../providers/db-firebase";
|
|
3
3
|
import { type SupportTicketDocument, type SupportTicketCreateInput, type SupportTicketUpdateInput, type TicketMessage, type TicketStatus } from "../schemas/firestore";
|
|
4
|
+
import { type HistoryActor } from "../../../_internal/shared/history/index";
|
|
5
|
+
/** Who/why for a write that lands in `statusHistory`. */
|
|
6
|
+
export interface TicketWriteContext {
|
|
7
|
+
actor?: HistoryActor;
|
|
8
|
+
trigger?: string;
|
|
9
|
+
reason?: string;
|
|
10
|
+
note?: string;
|
|
11
|
+
}
|
|
4
12
|
export declare class SupportRepository extends BaseRepository<SupportTicketDocument> {
|
|
5
13
|
static readonly SIEVE_FIELDS: FirebaseSieveFields;
|
|
6
14
|
constructor();
|
|
@@ -10,9 +18,45 @@ export declare class SupportRepository extends BaseRepository<SupportTicketDocum
|
|
|
10
18
|
countActiveTickets(userId: string): Promise<number>;
|
|
11
19
|
getActiveOrderTicket(userId: string, orderId: string): Promise<SupportTicketDocument | null>;
|
|
12
20
|
getActiveCategoryTicket(userId: string, category: string): Promise<SupportTicketDocument | null>;
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
21
|
+
/**
|
|
22
|
+
* The single status/assignment write path.
|
|
23
|
+
*
|
|
24
|
+
* `prior` is not an optimisation detail — the caller that matters (the admin
|
|
25
|
+
* PATCH route) already fetches the ticket to 404 on a missing one, so
|
|
26
|
+
* threading it through means history costs ZERO extra Firestore reads
|
|
27
|
+
* (Rule #6). Omit it and this falls back to a read, which is correct but
|
|
28
|
+
* avoidable.
|
|
29
|
+
*/
|
|
30
|
+
updateTicketStatus(ticketId: string, update: SupportTicketUpdateInput, ctx?: TicketWriteContext, prior?: SupportTicketDocument | null): Promise<SupportTicketDocument>;
|
|
31
|
+
/**
|
|
32
|
+
* Stamp `resolvedAt` / `closedAt` on the transition INTO that status.
|
|
33
|
+
*
|
|
34
|
+
* Both fields were declared, constant-named and listed in the update input,
|
|
35
|
+
* and no code path had ever written either (verified 2026-08-26) — so every
|
|
36
|
+
* resolved ticket in the collection has no resolution time.
|
|
37
|
+
*
|
|
38
|
+
* Guarded on the transition, not on the status: re-saving a priority change
|
|
39
|
+
* on an already-resolved ticket must not move its resolution time forward.
|
|
40
|
+
* Never fabricated for a ticket resolved before this existed — that one
|
|
41
|
+
* renders an em-dash, the same rule the offer timeline follows.
|
|
42
|
+
*/
|
|
43
|
+
private resolutionStamps;
|
|
44
|
+
/** Append a timeline entry when the patch touches a tracked field. */
|
|
45
|
+
private withTicketHistory;
|
|
46
|
+
/**
|
|
47
|
+
* Append a reply, optionally moving the ticket's status.
|
|
48
|
+
*
|
|
49
|
+
* The message itself is NOT history — the thread is already the record of
|
|
50
|
+
* what was said. Only an accompanying status change earns an entry, and
|
|
51
|
+
* only then is a read taken, so an ordinary reply still costs one write and
|
|
52
|
+
* no read.
|
|
53
|
+
*
|
|
54
|
+
* `messages` stays an `arrayUnion` (concurrent replies must not clobber one
|
|
55
|
+
* another); `statusHistory` cannot, because `arrayUnion` has no way to
|
|
56
|
+
* enforce the FIFO cap — so it is folded in memory and written whole.
|
|
57
|
+
*/
|
|
58
|
+
addMessage(ticketId: string, message: TicketMessage, newStatus?: TicketStatus, ctx?: TicketWriteContext, prior?: SupportTicketDocument | null): Promise<void>;
|
|
59
|
+
assignTicket(ticketId: string, assignedTo: string, assignedToName: string, ctx?: TicketWriteContext, prior?: SupportTicketDocument | null): Promise<SupportTicketDocument>;
|
|
16
60
|
listAll(model: SieveModel): Promise<FirebaseSieveResult<SupportTicketDocument>>;
|
|
17
61
|
}
|
|
18
62
|
export declare const supportRepository: SupportRepository;
|
|
@@ -3,7 +3,8 @@ import { BaseRepository, prepareForFirestore, parseSieveDateValue, } from "../..
|
|
|
3
3
|
import { normalizeError } from "../../../errors/normalize";
|
|
4
4
|
import { DatabaseError } from "../../../errors";
|
|
5
5
|
import { SUPPORT_TICKET_FIELDS } from "../../../constants/field-names";
|
|
6
|
-
import { SUPPORT_TICKET_COLLECTION, ACTIVE_TICKET_STATUSES, } from "../schemas/firestore";
|
|
6
|
+
import { SUPPORT_TICKET_COLLECTION, SUPPORT_TICKET_PII_FIELDS, SUPPORT_TICKET_TRACKED_FIELDS, TicketStatusValues, ACTIVE_TICKET_STATUSES, } from "../schemas/firestore";
|
|
7
|
+
import { withHistory } from "../../../_internal/shared/history/index";
|
|
7
8
|
export class SupportRepository extends BaseRepository {
|
|
8
9
|
constructor() {
|
|
9
10
|
super(SUPPORT_TICKET_COLLECTION);
|
|
@@ -73,13 +74,73 @@ export class SupportRepository extends BaseRepository {
|
|
|
73
74
|
const doc = snap.docs[0];
|
|
74
75
|
return { id: doc.id, ...doc.data() };
|
|
75
76
|
}
|
|
76
|
-
|
|
77
|
-
|
|
77
|
+
/**
|
|
78
|
+
* The single status/assignment write path.
|
|
79
|
+
*
|
|
80
|
+
* `prior` is not an optimisation detail — the caller that matters (the admin
|
|
81
|
+
* PATCH route) already fetches the ticket to 404 on a missing one, so
|
|
82
|
+
* threading it through means history costs ZERO extra Firestore reads
|
|
83
|
+
* (Rule #6). Omit it and this falls back to a read, which is correct but
|
|
84
|
+
* avoidable.
|
|
85
|
+
*/
|
|
86
|
+
async updateTicketStatus(ticketId, update, ctx, prior) {
|
|
87
|
+
const current = prior !== undefined ? prior : await this.getTicketById(ticketId);
|
|
88
|
+
const patch = {
|
|
78
89
|
...update,
|
|
90
|
+
...this.resolutionStamps(current, update.status),
|
|
79
91
|
updatedAt: new Date(),
|
|
92
|
+
};
|
|
93
|
+
return this.update(ticketId, this.withTicketHistory(current, patch, ctx, "updateTicketStatus"));
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Stamp `resolvedAt` / `closedAt` on the transition INTO that status.
|
|
97
|
+
*
|
|
98
|
+
* Both fields were declared, constant-named and listed in the update input,
|
|
99
|
+
* and no code path had ever written either (verified 2026-08-26) — so every
|
|
100
|
+
* resolved ticket in the collection has no resolution time.
|
|
101
|
+
*
|
|
102
|
+
* Guarded on the transition, not on the status: re-saving a priority change
|
|
103
|
+
* on an already-resolved ticket must not move its resolution time forward.
|
|
104
|
+
* Never fabricated for a ticket resolved before this existed — that one
|
|
105
|
+
* renders an em-dash, the same rule the offer timeline follows.
|
|
106
|
+
*/
|
|
107
|
+
resolutionStamps(current, next) {
|
|
108
|
+
if (!next || next === current?.status)
|
|
109
|
+
return {};
|
|
110
|
+
const now = new Date();
|
|
111
|
+
if (next === TicketStatusValues.RESOLVED && !current?.resolvedAt)
|
|
112
|
+
return { resolvedAt: now };
|
|
113
|
+
if (next === TicketStatusValues.CLOSED && !current?.closedAt)
|
|
114
|
+
return { closedAt: now };
|
|
115
|
+
return {};
|
|
116
|
+
}
|
|
117
|
+
/** Append a timeline entry when the patch touches a tracked field. */
|
|
118
|
+
withTicketHistory(current, patch, ctx, defaultTrigger) {
|
|
119
|
+
const withEntry = withHistory(current, patch, {
|
|
120
|
+
tracked: SUPPORT_TICKET_TRACKED_FIELDS,
|
|
121
|
+
actor: ctx?.actor ?? { role: "system" },
|
|
122
|
+
trigger: ctx?.trigger ?? defaultTrigger,
|
|
123
|
+
reason: ctx?.reason,
|
|
124
|
+
note: ctx?.note,
|
|
125
|
+
// `encryptPiiFields` never descends into arrays, so a PII-named value
|
|
126
|
+
// reaching `changes` would persist in PLAINTEXT inside statusHistory.
|
|
127
|
+
piiFields: SUPPORT_TICKET_PII_FIELDS,
|
|
80
128
|
});
|
|
129
|
+
return withEntry ?? patch;
|
|
81
130
|
}
|
|
82
|
-
|
|
131
|
+
/**
|
|
132
|
+
* Append a reply, optionally moving the ticket's status.
|
|
133
|
+
*
|
|
134
|
+
* The message itself is NOT history — the thread is already the record of
|
|
135
|
+
* what was said. Only an accompanying status change earns an entry, and
|
|
136
|
+
* only then is a read taken, so an ordinary reply still costs one write and
|
|
137
|
+
* no read.
|
|
138
|
+
*
|
|
139
|
+
* `messages` stays an `arrayUnion` (concurrent replies must not clobber one
|
|
140
|
+
* another); `statusHistory` cannot, because `arrayUnion` has no way to
|
|
141
|
+
* enforce the FIFO cap — so it is folded in memory and written whole.
|
|
142
|
+
*/
|
|
143
|
+
async addMessage(ticketId, message, newStatus, ctx, prior) {
|
|
83
144
|
try {
|
|
84
145
|
const ref = this.db
|
|
85
146
|
.collection(SUPPORT_TICKET_COLLECTION)
|
|
@@ -88,8 +149,10 @@ export class SupportRepository extends BaseRepository {
|
|
|
88
149
|
messages: firebaseFieldOps.arrayUnion(prepareForFirestore(message)),
|
|
89
150
|
updatedAt: new Date(),
|
|
90
151
|
};
|
|
91
|
-
if (newStatus)
|
|
92
|
-
|
|
152
|
+
if (newStatus) {
|
|
153
|
+
const current = prior !== undefined ? prior : await this.getTicketById(ticketId);
|
|
154
|
+
Object.assign(updateData, this.withTicketHistory(current, { status: newStatus, ...this.resolutionStamps(current, newStatus) }, ctx, "addMessage"));
|
|
155
|
+
}
|
|
93
156
|
await ref.update(updateData);
|
|
94
157
|
}
|
|
95
158
|
catch (err) {
|
|
@@ -97,12 +160,9 @@ export class SupportRepository extends BaseRepository {
|
|
|
97
160
|
throw new DatabaseError("Failed to add message to ticket", err);
|
|
98
161
|
}
|
|
99
162
|
}
|
|
100
|
-
async assignTicket(ticketId, assignedTo, assignedToName) {
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
assignedToName,
|
|
104
|
-
updatedAt: new Date(),
|
|
105
|
-
});
|
|
163
|
+
async assignTicket(ticketId, assignedTo, assignedToName, ctx, prior) {
|
|
164
|
+
const current = prior !== undefined ? prior : await this.getTicketById(ticketId);
|
|
165
|
+
return this.update(ticketId, this.withTicketHistory(current, { assignedTo, assignedToName, updatedAt: new Date() }, ctx, "assignTicket"));
|
|
106
166
|
}
|
|
107
167
|
async listAll(model) {
|
|
108
168
|
return this.sieveQuery(model, SupportRepository.SIEVE_FIELDS, { defaultPageSize: 25, maxPageSize: 100 });
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
* - No new ticket in the same category while existing one is waiting_on_user
|
|
16
16
|
*/
|
|
17
17
|
import type { BaseDocument } from "../../../_internal/shared/types/base-document";
|
|
18
|
+
import type { StatusChangeEntry } from "../../../_internal/shared/history/types";
|
|
18
19
|
export declare const TicketCategoryValues: {
|
|
19
20
|
readonly ORDER_ISSUE: "order_issue";
|
|
20
21
|
readonly BILLING_PAYMENT: "billing_payment";
|
|
@@ -101,11 +102,47 @@ export interface SupportTicketDocument extends BaseDocument {
|
|
|
101
102
|
internalNotes?: string;
|
|
102
103
|
/** Threaded conversation. Ordered by createdAt asc. */
|
|
103
104
|
messages: TicketMessage[];
|
|
105
|
+
/**
|
|
106
|
+
* Stamped by the repository the first time `status` becomes `resolved` /
|
|
107
|
+
* `closed`. Both were declared, given `SUPPORT_TICKET_FIELDS` constants and
|
|
108
|
+
* listed in `SupportTicketUpdateInput` — and **nothing ever wrote either**
|
|
109
|
+
* (verified 2026-08-26), so no resolved ticket in the collection carries a
|
|
110
|
+
* resolution time and "how long do tickets take" has never been answerable.
|
|
111
|
+
*
|
|
112
|
+
* Server-stamped, never client-sent: the same discipline the reports route
|
|
113
|
+
* adopted after a client-sent `resolvedAt` arrived as a JSON string into a
|
|
114
|
+
* `Date` field and split the collection into two shapes.
|
|
115
|
+
*/
|
|
104
116
|
resolvedAt?: Date;
|
|
105
117
|
closedAt?: Date;
|
|
118
|
+
/**
|
|
119
|
+
* Who changed what, when, and why. See § "Status History" in CLAUDE.md.
|
|
120
|
+
* Append-only, capped at STATUS_HISTORY_MAX, oldest evicted first.
|
|
121
|
+
*/
|
|
122
|
+
statusHistory?: StatusChangeEntry[];
|
|
123
|
+
/** How many entries fell off the front. Never imply "this is all of it". */
|
|
124
|
+
statusHistoryTruncated?: number;
|
|
106
125
|
}
|
|
126
|
+
/**
|
|
127
|
+
* The fields whose changes earn a timeline entry.
|
|
128
|
+
*
|
|
129
|
+
* Explicit, not a whole-document diff: a diff would record the `updatedAt`
|
|
130
|
+
* bump on every write and exhaust the 50-entry cap within days. `messages` is
|
|
131
|
+
* excluded on purpose — the thread is already the conversation, and mirroring
|
|
132
|
+
* every reply into history would bury the handful of transitions anyone reads.
|
|
133
|
+
*/
|
|
134
|
+
export declare const SUPPORT_TICKET_TRACKED_FIELDS: readonly ["status", "priority", "assignedTo", "resolvedAt", "closedAt"];
|
|
135
|
+
/**
|
|
136
|
+
* PII on this document, for `withHistory`'s scrub.
|
|
137
|
+
*
|
|
138
|
+
* `assignedToName` is a staff display name denormalised for the admin table —
|
|
139
|
+
* not the ticket author's identity — but it is still a person's name reaching
|
|
140
|
+
* an array `encryptPiiFields` cannot descend into, so history carries the UID
|
|
141
|
+
* (`assignedTo`) and never the name.
|
|
142
|
+
*/
|
|
143
|
+
export declare const SUPPORT_TICKET_PII_FIELDS: readonly ["userEmail", "userDisplayName", "assignedToName"];
|
|
107
144
|
export type SupportTicketCreateInput = Pick<SupportTicketDocument, "userId" | "userEmail" | "userDisplayName" | "category" | "subject" | "description" | "orderId">;
|
|
108
|
-
export type SupportTicketUpdateInput = Partial<Pick<SupportTicketDocument, "status" | "priority" | "assignedTo" | "assignedToName" | "internalNotes" | "
|
|
145
|
+
export type SupportTicketUpdateInput = Partial<Pick<SupportTicketDocument, "status" | "priority" | "assignedTo" | "assignedToName" | "internalNotes" | "relatedParties">>;
|
|
109
146
|
export declare const SUPPORT_TICKET_COLLECTION: "supportTickets";
|
|
110
147
|
export declare const SUPPORT_TICKET_ID_PREFIX: "ticket-";
|
|
111
148
|
export declare const SUPPORT_TICKET_INDEXED_FIELDS: readonly ["userId", "status", "category", "priority", "assignedTo", "orderId", "createdAt", "updatedAt"];
|
|
@@ -63,6 +63,40 @@ export const ELIGIBLE_ORDER_STATUSES_FOR_TICKET = [
|
|
|
63
63
|
"SHIPPED",
|
|
64
64
|
"RETURN_REQUESTED",
|
|
65
65
|
];
|
|
66
|
+
/**
|
|
67
|
+
* The fields whose changes earn a timeline entry.
|
|
68
|
+
*
|
|
69
|
+
* Explicit, not a whole-document diff: a diff would record the `updatedAt`
|
|
70
|
+
* bump on every write and exhaust the 50-entry cap within days. `messages` is
|
|
71
|
+
* excluded on purpose — the thread is already the conversation, and mirroring
|
|
72
|
+
* every reply into history would bury the handful of transitions anyone reads.
|
|
73
|
+
*/
|
|
74
|
+
export const SUPPORT_TICKET_TRACKED_FIELDS = [
|
|
75
|
+
"status",
|
|
76
|
+
"priority",
|
|
77
|
+
"assignedTo",
|
|
78
|
+
"resolvedAt",
|
|
79
|
+
"closedAt",
|
|
80
|
+
];
|
|
81
|
+
/**
|
|
82
|
+
* PII on this document, for `withHistory`'s scrub.
|
|
83
|
+
*
|
|
84
|
+
* `assignedToName` is a staff display name denormalised for the admin table —
|
|
85
|
+
* not the ticket author's identity — but it is still a person's name reaching
|
|
86
|
+
* an array `encryptPiiFields` cannot descend into, so history carries the UID
|
|
87
|
+
* (`assignedTo`) and never the name.
|
|
88
|
+
*/
|
|
89
|
+
export const SUPPORT_TICKET_PII_FIELDS = [
|
|
90
|
+
"userEmail",
|
|
91
|
+
"userDisplayName",
|
|
92
|
+
"assignedToName",
|
|
93
|
+
];
|
|
94
|
+
/*
|
|
95
|
+
* `resolvedAt` / `closedAt` are deliberately NOT in the update input. They are
|
|
96
|
+
* events, and the server is the only thing that knows when one happened —
|
|
97
|
+
* letting a client send them is how the reports collection ended up holding
|
|
98
|
+
* both Timestamps and ISO strings in one field.
|
|
99
|
+
*/
|
|
66
100
|
// ============================================================================
|
|
67
101
|
// COLLECTION CONSTANTS
|
|
68
102
|
// ============================================================================
|
|
@@ -182,6 +182,7 @@ export declare const supportTicketFirestoreSchema: z.ZodObject<{
|
|
|
182
182
|
closedAt?: string | Date | z.objectOutputType<{
|
|
183
183
|
toDate: z.ZodFunction<z.ZodTuple<[], z.ZodUnknown>, z.ZodDate>;
|
|
184
184
|
}, z.ZodTypeAny, "passthrough"> | undefined;
|
|
185
|
+
assignedToName?: string | undefined;
|
|
185
186
|
relatedParties?: {
|
|
186
187
|
storeId?: string | undefined;
|
|
187
188
|
userId?: string | undefined;
|
|
@@ -189,7 +190,6 @@ export declare const supportTicketFirestoreSchema: z.ZodObject<{
|
|
|
189
190
|
productId?: string | undefined;
|
|
190
191
|
bidId?: string | undefined;
|
|
191
192
|
} | undefined;
|
|
192
|
-
assignedToName?: string | undefined;
|
|
193
193
|
internalNotes?: string | undefined;
|
|
194
194
|
}, {
|
|
195
195
|
status: "closed" | "open" | "resolved" | "in_progress" | "waiting_on_user";
|
|
@@ -225,6 +225,7 @@ export declare const supportTicketFirestoreSchema: z.ZodObject<{
|
|
|
225
225
|
closedAt?: string | Date | z.objectInputType<{
|
|
226
226
|
toDate: z.ZodFunction<z.ZodTuple<[], z.ZodUnknown>, z.ZodDate>;
|
|
227
227
|
}, z.ZodTypeAny, "passthrough"> | undefined;
|
|
228
|
+
assignedToName?: string | undefined;
|
|
228
229
|
relatedParties?: {
|
|
229
230
|
storeId?: string | undefined;
|
|
230
231
|
userId?: string | undefined;
|
|
@@ -232,7 +233,6 @@ export declare const supportTicketFirestoreSchema: z.ZodObject<{
|
|
|
232
233
|
productId?: string | undefined;
|
|
233
234
|
bidId?: string | undefined;
|
|
234
235
|
} | undefined;
|
|
235
|
-
assignedToName?: string | undefined;
|
|
236
236
|
internalNotes?: string | undefined;
|
|
237
237
|
}>;
|
|
238
238
|
export type SupportTicketFromSchema = z.infer<typeof supportTicketFirestoreSchema>;
|
|
@@ -1156,6 +1156,96 @@ const rawTesterChecklistItems = [
|
|
|
1156
1156
|
description: "The fix had to cover the update path as well as create — a create-only fix would mean the listing saved correctly and then lost its per-type block the first time anyone edited anything else (Root Cause #39).",
|
|
1157
1157
|
href: "/store/classified",
|
|
1158
1158
|
},
|
|
1159
|
+
{
|
|
1160
|
+
key: "ticket-resolution-timestamp-stamped",
|
|
1161
|
+
label: "Resolving a support ticket records WHEN it was resolved",
|
|
1162
|
+
description: "`resolvedAt` and `closedAt` were declared on the document, had field-name constants, were listed in the update input — and nothing had ever written either (verified 2026-08-26), so no resolved ticket carried a resolution time. Move a ticket to Resolved, reopen it, and confirm the timeline shows a real date on that step. Re-save an unrelated field afterwards: the resolution time must NOT move forward.",
|
|
1163
|
+
href: "/admin/support-tickets",
|
|
1164
|
+
},
|
|
1165
|
+
{
|
|
1166
|
+
key: "payout-utr-actually-saves",
|
|
1167
|
+
label: "The UTR entered when marking a payout paid is still there on reload",
|
|
1168
|
+
description: "The mark-paid modal REQUIRES a transaction reference, the route declared it, its own test asserted it reached the action — and the action picked only status/adminNote/processedAt, so the UTR was dropped while the 200 echoed it back as if stored. Mark a payout paid with a reference, reload the payout, and confirm 'Transaction ref' shows it.",
|
|
1169
|
+
href: "/admin/payouts",
|
|
1170
|
+
},
|
|
1171
|
+
{
|
|
1172
|
+
key: "store-approval-makes-it-public",
|
|
1173
|
+
label: "Approving a pending store actually makes it visible on /stores",
|
|
1174
|
+
description: "`status` and `isPublic` are two distinct fields and public visibility gates on `isPublic`. The admin PATCH wrote `status` directly and never touched `isPublic`, so an approved store was active AND invisible with no error anywhere. Approve `store-blader-bazaar`, then open /stores and confirm it appears.",
|
|
1175
|
+
href: "/admin/stores",
|
|
1176
|
+
},
|
|
1177
|
+
{
|
|
1178
|
+
key: "store-timeline-shows-who-suspended",
|
|
1179
|
+
label: "A suspended store's page shows who suspended it, when, and why",
|
|
1180
|
+
description: "Previously recoverable only from /admin/audit-log, which a store owner can never see. Suspend a store with a reason, open its admin detail page, and confirm the History block names the admin, the time, and the reason.",
|
|
1181
|
+
href: "/admin/stores",
|
|
1182
|
+
},
|
|
1183
|
+
{
|
|
1184
|
+
key: "payout-failure-reasons-survive-retries",
|
|
1185
|
+
label: "A payout that failed twice shows BOTH reasons, not just the last",
|
|
1186
|
+
description: "`lastFailureReason` is overwritten by every retry, so the timeline is the only place earlier attempts survive. Also confirm the four dispatch fields (razorpayPayoutId, razorpayStatus, failureCount, lastFailureReason) render — they were written by the batch and UNDECLARED on the document until 2026-08-26, so nothing could safely surface them.",
|
|
1187
|
+
href: "/admin/payouts",
|
|
1188
|
+
},
|
|
1189
|
+
{
|
|
1190
|
+
key: "bid-timeline-explains-forfeited",
|
|
1191
|
+
label: "A forfeited bid says why it was forfeited",
|
|
1192
|
+
description: "Open a forfeited bid from /admin/bids. The History block must read 'Forfeited' with the system actor and the reason 'The 48-hour checkout window closed without payment.' A bid moves through more states than any record a buyer owns and every one of them happened inside a settlement batch with nothing recording it.",
|
|
1193
|
+
href: "/admin/bids",
|
|
1194
|
+
},
|
|
1195
|
+
{
|
|
1196
|
+
key: "catalogue-rejection-reason-survives",
|
|
1197
|
+
label: "A catalogue item rejected twice keeps both reasons",
|
|
1198
|
+
description: "`rejectionReason` holds only the last decision. Reject a submission, have the owner resubmit, reject again with a different reason — the approval modal's History block must show both, each with its own date.",
|
|
1199
|
+
href: "/admin/catalogue-approvals",
|
|
1200
|
+
},
|
|
1201
|
+
{
|
|
1202
|
+
key: "history-absent-renders-empty-not-invented",
|
|
1203
|
+
label: "A record created BEFORE history existed shows 'no recorded history', not a made-up date",
|
|
1204
|
+
description: "Never fabricate a timestamp. Open any store, payout or ticket that predates this feature: the History block must say there is none, NOT derive a step from `updatedAt` — 'last write of any kind' is not an event, and a made-up date makes the record lie.",
|
|
1205
|
+
href: "/admin/stores",
|
|
1206
|
+
},
|
|
1207
|
+
{
|
|
1208
|
+
key: "history-carries-no-pii",
|
|
1209
|
+
label: "No name or email appears anywhere in a record's History block",
|
|
1210
|
+
description: "`encryptPiiFields` is a flat top-level loop that never descends into arrays, so a PII value inside statusHistory would be stored in PLAINTEXT. Every entry shows a ROLE (Admin/System/Buyer/Store) and never a name. Check a payout (sellerEmail/upiId), a bid (userName/userEmail) and a scammer profile (built entirely of identifying details).",
|
|
1211
|
+
href: "/admin/payouts",
|
|
1212
|
+
},
|
|
1213
|
+
{
|
|
1214
|
+
key: "form-errors-wait-for-first-submit",
|
|
1215
|
+
label: "Opening the address form shows NO errors until you press Save",
|
|
1216
|
+
description: "Live bug fixed 2026-08-26: every schema-driven form greeted the user with a list of its own requirements as errors before they had typed anything. Two causes compounded — <FormErrorSummary> was deliberately un-gated, and ~20 views run validate(draft) in a mount effect, so an untouched empty draft failed every .min(1) on first paint. Open /user/addresses/new: the summary block must be absent. Press Save with the form empty: NOW it lists the missing fields.",
|
|
1217
|
+
href: "/user/addresses/new",
|
|
1218
|
+
},
|
|
1219
|
+
{
|
|
1220
|
+
key: "form-errors-stay-live-after-first-submit",
|
|
1221
|
+
label: "After a failed Save, the error list SHRINKS as you fill fields in",
|
|
1222
|
+
description: "The fix gates the DISPLAY, not the computation — the live validation underneath is what makes the list useful. Press Save on an empty address form, then fill Full name. The summary must drop that entry immediately, without a second Save. If the list is frozen, the on-mount validate() was removed instead of gated, which is the wrong fix.",
|
|
1223
|
+
href: "/user/addresses/new",
|
|
1224
|
+
},
|
|
1225
|
+
{
|
|
1226
|
+
key: "form-mobile-bar-pinned",
|
|
1227
|
+
label: "On a phone, Save and Cancel are pinned above the tab bar on the address form",
|
|
1228
|
+
description: "Added 2026-08-26 (W21). Narrow the window below 1024px, or use a real phone. Save/Cancel should sit in the same fixed bar the cart uses for 'Proceed to checkout' — above the bottom tab bar, not scrolled off the end of a long form. On desktop the bar is hidden and the inline Save/Cancel row at the bottom of the form is the control.",
|
|
1229
|
+
href: "/user/addresses/new",
|
|
1230
|
+
},
|
|
1231
|
+
{
|
|
1232
|
+
key: "form-mobile-error-sheet",
|
|
1233
|
+
label: "On a phone, a failed Save opens a 'Fix N issues' sheet you can tap through",
|
|
1234
|
+
description: "Below 1024px, press Save on an empty address form. A 'Fix 6 issues' row must appear above the buttons with the list already open. Collapse it, press Save again — it must RE-open (the count is a counter, not a boolean, precisely so a second attempt is a new event). Tapping an entry on a sectioned form jumps to that field.",
|
|
1235
|
+
href: "/user/addresses/new",
|
|
1236
|
+
},
|
|
1237
|
+
{
|
|
1238
|
+
key: "form-bar-restores-listing-bulk-bar",
|
|
1239
|
+
label: "Closing a form drawer over a listing brings the listing's bulk bar back",
|
|
1240
|
+
description: "Regression test for the singleton clobber: there is ONE bottom bar per route and DataListingView claims it on ~70 admin screens. Select a few rows on an admin listing so the bulk bar appears, open a row's editor drawer, then close it — the bulk bar must still be there with the selection intact. Before the claim stack landed, the drawer overwrote it and blanked it on close.",
|
|
1241
|
+
href: "/admin/products",
|
|
1242
|
+
},
|
|
1243
|
+
{
|
|
1244
|
+
key: "form-bar-absent-inside-a-modal",
|
|
1245
|
+
label: "A form inside a modal or drawer does NOT get a second bar at the bottom of the screen",
|
|
1246
|
+
description: "An overlay owns its own footer, and a viewport-fixed bar would render behind the backdrop, below the dialog it belongs to. Open any drawer-based editor on a phone-width window and confirm the only Save/Cancel controls are the ones inside the drawer.",
|
|
1247
|
+
href: "/admin/coupons",
|
|
1248
|
+
},
|
|
1159
1249
|
{
|
|
1160
1250
|
key: "address-routes-normalised",
|
|
1161
1251
|
label: "Adding and editing an address works, and the OLD urls still resolve",
|
package/dist/index.d.ts
CHANGED
|
@@ -2120,6 +2120,10 @@ export { SidebarLayout } from "./features/layout/index";
|
|
|
2120
2120
|
export { TitleBar } from "./features/layout/index";
|
|
2121
2121
|
export { TitleBarLayout } from "./features/layout/index";
|
|
2122
2122
|
export { useBottomActions } from "./features/layout/index";
|
|
2123
|
+
export { RecordStatusTimeline } from "./features/status-history/index";
|
|
2124
|
+
export { useFormBottomActions } from "./features/layout/index";
|
|
2125
|
+
export { FormErrorList } from "./ui/forms";
|
|
2126
|
+
export { OverlayContext, useIsInsideOverlay } from "./ui/components/overlay-context";
|
|
2123
2127
|
export { useBottomActionsContext } from "./features/layout/index";
|
|
2124
2128
|
export { useDashboardNav } from "./features/layout/index";
|
|
2125
2129
|
export type { AppLayoutShellProps } from "./features/layout/index";
|
|
@@ -3233,7 +3237,7 @@ export { getListingRule, getCategoryRule, pickOrderType, getSplitKey, runSyncPre
|
|
|
3233
3237
|
export type { ListingCheckoutRule, CategoryCheckoutRule, CartItemProductPair, OrderItemInput, RefundPolicy, } from "./_internal/shared/checkout/rules";
|
|
3234
3238
|
export { MEGABYTE, MAX_IMAGE_BYTES, MAX_PDF_BYTES, MAX_VIDEO_BYTES, MAX_LABEL, MAX_BYTES, ALLOWED_IMAGE_MIMES, ALLOWED_VIDEO_MIMES, ALLOWED_DOC_MIMES, ALLOWED_MIMES, ALLOWED_TYPES_LABEL, MIME_TO_EXT, PDF_MAGIC, VIDEO_CONVERSION_HINTS, classifyMime, isAllowedMime, maxBytesFor, getConversionHint, } from "./_internal/shared/media/limits";
|
|
3235
3239
|
export type { MediaKind, AllowedImageMime, AllowedVideoMime, AllowedDocMime, AllowedMime, } from "./_internal/shared/media/limits";
|
|
3236
|
-
export { PRODUCT_FIELDS, PRODUCT_STATUS_TRANSITIONS, ORDER_FIELDS, REVIEW_FIELDS, BID_FIELDS, AD_FIELDS, BLOG_FIELDS, BRAND_FIELDS, WISHLIST_FIELDS, HISTORY_FIELDS, COUPON_USAGE_FIELDS, CONVERSATION_FIELDS, CAROUSEL_FIELDS, FAQ_FIELDS, HOMEPAGE_SECTION_FIELDS, SITE_SETTINGS_FIELDS, COMMON_FIELDS, OAUTH_STATE_VALUES, SCHEMA_DEFAULTS, } from "./constants/field-names";
|
|
3240
|
+
export { PRODUCT_FIELDS, PRODUCT_STATUS_TRANSITIONS, ORDER_FIELDS, REVIEW_FIELDS, BID_FIELDS, AD_FIELDS, AD_TRACKED_FIELDS, BLOG_FIELDS, BRAND_FIELDS, WISHLIST_FIELDS, HISTORY_FIELDS, COUPON_USAGE_FIELDS, CONVERSATION_FIELDS, CAROUSEL_FIELDS, FAQ_FIELDS, HOMEPAGE_SECTION_FIELDS, SITE_SETTINGS_FIELDS, COMMON_FIELDS, OAUTH_STATE_VALUES, SCHEMA_DEFAULTS, } from "./constants/field-names";
|
|
3237
3241
|
export { SORT_DIR, sortBy } from "./constants/sort";
|
|
3238
3242
|
export type { SortDirection } from "./constants/sort";
|
|
3239
3243
|
export { TABLE_KEYS, VIEW_MODE } from "./constants/table-keys";
|
|
@@ -3294,3 +3298,5 @@ export { adminUserUpdateSchema, whatsappSettingsSchema, lotteryPullSchema, } fro
|
|
|
3294
3298
|
export { siteSettingsFormSchema } from "./features/admin/schemas/site-settings-form";
|
|
3295
3299
|
export { createReviewSchema as reviewSubmitSchema } from "./_internal/shared/features/reviews/schema";
|
|
3296
3300
|
export { wishlistAddSchema } from "./schemas/api-routes";
|
|
3301
|
+
export { withHistory, buildHistoryEntry, appendHistoryEntry, diffTrackedFields, STATUS_HISTORY_MAX, } from "./_internal/shared/history/index";
|
|
3302
|
+
export type { StatusChangeEntry, SerialisedStatusChangeEntry, HistoryActor, HistoryActorRole, FieldChange, } from "./_internal/shared/history/index";
|
package/dist/index.js
CHANGED
|
@@ -3934,6 +3934,11 @@ export { TitleBarLayout } from "./features/layout/index";
|
|
|
3934
3934
|
// [CLIENT-SSR]-Runs in both SSR and browser â€" React component or hook that does not depend on browser-only APIs.
|
|
3935
3935
|
// useBottomActions - React hook for use bottom actions.
|
|
3936
3936
|
export { useBottomActions } from "./features/layout/index";
|
|
3937
|
+
export { RecordStatusTimeline } from "./features/status-history/index";
|
|
3938
|
+
// useFormBottomActions - pins a form's Save/Cancel row + error sheet to the mobile bottom-chrome tier.
|
|
3939
|
+
export { useFormBottomActions } from "./features/layout/index";
|
|
3940
|
+
export { FormErrorList } from "./ui/forms";
|
|
3941
|
+
export { OverlayContext, useIsInsideOverlay } from "./ui/components/overlay-context";
|
|
3937
3942
|
// [CLIENT-SSR]-Runs in both SSR and browser â€" React component or hook that does not depend on browser-only APIs.
|
|
3938
3943
|
// useBottomActionsContext - React hook for use bottom actions context.
|
|
3939
3944
|
export { useBottomActionsContext } from "./features/layout/index";
|
|
@@ -5637,7 +5642,7 @@ export { MEGABYTE, MAX_IMAGE_BYTES, MAX_PDF_BYTES, MAX_VIDEO_BYTES, MAX_LABEL, M
|
|
|
5637
5642
|
// NOTE: STORE_FIELDS, CATEGORY_FIELDS, EVENT_FIELDS, EVENT_ENTRY_FIELDS, PAYOUT_FIELDS,
|
|
5638
5643
|
// COUPON_FIELDS, CART_FIELDS, USER_FIELDS, ADDRESS_FIELDS, NOTIFICATION_FIELDS,
|
|
5639
5644
|
// SESSION_FIELDS, SCAMMER_FIELDS are already exported from feature barrels above.
|
|
5640
|
-
export { PRODUCT_FIELDS, PRODUCT_STATUS_TRANSITIONS, ORDER_FIELDS, REVIEW_FIELDS, BID_FIELDS, AD_FIELDS, BLOG_FIELDS, BRAND_FIELDS, WISHLIST_FIELDS, HISTORY_FIELDS, COUPON_USAGE_FIELDS, CONVERSATION_FIELDS, CAROUSEL_FIELDS, FAQ_FIELDS, HOMEPAGE_SECTION_FIELDS, SITE_SETTINGS_FIELDS, COMMON_FIELDS, OAUTH_STATE_VALUES, SCHEMA_DEFAULTS, } from "./constants/field-names";
|
|
5645
|
+
export { PRODUCT_FIELDS, PRODUCT_STATUS_TRANSITIONS, ORDER_FIELDS, REVIEW_FIELDS, BID_FIELDS, AD_FIELDS, AD_TRACKED_FIELDS, BLOG_FIELDS, BRAND_FIELDS, WISHLIST_FIELDS, HISTORY_FIELDS, COUPON_USAGE_FIELDS, CONVERSATION_FIELDS, CAROUSEL_FIELDS, FAQ_FIELDS, HOMEPAGE_SECTION_FIELDS, SITE_SETTINGS_FIELDS, COMMON_FIELDS, OAUTH_STATE_VALUES, SCHEMA_DEFAULTS, } from "./constants/field-names";
|
|
5641
5646
|
// ── Sort helpers ──────────────────────────────────────────────────────────────
|
|
5642
5647
|
export { SORT_DIR, sortBy } from "./constants/sort";
|
|
5643
5648
|
// ── URL table keys + view mode ────────────────────────────────────────────────
|
|
@@ -5711,3 +5716,13 @@ export { siteSettingsFormSchema } from "./features/admin/schemas/site-settings-f
|
|
|
5711
5716
|
*/
|
|
5712
5717
|
export { createReviewSchema as reviewSubmitSchema } from "./_internal/shared/features/reviews/schema";
|
|
5713
5718
|
export { wishlistAddSchema } from "./schemas/api-routes";
|
|
5719
|
+
/*
|
|
5720
|
+
* Status history — the generic primitive behind every entity timeline.
|
|
5721
|
+
*
|
|
5722
|
+
* `withHistory` is exported because ads have no repository of their own: they
|
|
5723
|
+
* live inside the `siteSettings` singleton, so their route calls it directly.
|
|
5724
|
+
* Every other adopter funnels through its repository's write primitives and
|
|
5725
|
+
* should keep doing so — a call site reaching for this outside a repository
|
|
5726
|
+
* is usually a missing funnel, not a reason to export more of it.
|
|
5727
|
+
*/
|
|
5728
|
+
export { withHistory, buildHistoryEntry, appendHistoryEntry, diffTrackedFields, STATUS_HISTORY_MAX, } from "./_internal/shared/history/index";
|