@mohasinac/appkit 4.22.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 +2 -0
- package/dist/client.js +1 -0
- package/dist/constants/field-names.d.ts +12 -0
- package/dist/constants/field-names.js +12 -0
- package/dist/features/admin/actions/admin-actions.js +13 -3
- package/dist/features/admin/components/AdminBidsView.js +7 -1
- 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/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/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/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/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/shipments/components/AdminShipmentEditorView.js +2 -1
- 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 +54 -0
- package/dist/index.d.ts +4 -1
- package/dist/index.js +12 -1
- package/dist/schemas/registry.d.ts +32 -32
- package/dist/seed/bids-seed-data.js +8 -0
- package/package.json +1 -1
|
@@ -237,7 +237,10 @@ export async function finalizeLockedLines(items, orderId) {
|
|
|
237
237
|
}
|
|
238
238
|
if (item.bidId) {
|
|
239
239
|
try {
|
|
240
|
-
await bidRepository.attachOrder(item.bidId, orderId
|
|
240
|
+
await bidRepository.attachOrder(item.bidId, orderId, {
|
|
241
|
+
actor: { role: "system" },
|
|
242
|
+
trigger: "finalizeLockedLines",
|
|
243
|
+
});
|
|
241
244
|
}
|
|
242
245
|
catch (err) {
|
|
243
246
|
void normalizeError(err);
|
|
@@ -270,7 +273,14 @@ async function closeOutBuyoutLosers(productId, winningBidId) {
|
|
|
270
273
|
const losers = active.filter((e) => e.id !== winningBidId);
|
|
271
274
|
if (losers.length === 0)
|
|
272
275
|
return;
|
|
273
|
-
|
|
276
|
+
// Pass the {ref, data} pairs whole, not just the refs — `getActiveByProduct`
|
|
277
|
+
// already returned the documents, so the timeline gets a real before-value
|
|
278
|
+
// at zero read cost.
|
|
279
|
+
await bidRepository.markManyLost(losers, {
|
|
280
|
+
actor: { role: "system" },
|
|
281
|
+
trigger: "finalizeLockedLines:buyoutClosedAuction",
|
|
282
|
+
reason: "A Buy Now purchase ended this auction.",
|
|
283
|
+
});
|
|
274
284
|
serverLogger.info("finalizeLockedLines: buyout closed out losing bids", {
|
|
275
285
|
productId,
|
|
276
286
|
winningBidId,
|
|
@@ -27,7 +27,7 @@ import type { StoreDetail, StoreListItem } from "../../../../features/stores/typ
|
|
|
27
27
|
*/
|
|
28
28
|
export declare const PUBLIC_STORE_FIELDS: readonly ["id", "storeSlug", "ownerId", "storeName", "storeDescription", "storeCategory", "storeLogoURL", "storeBannerURL", "status", "isPublic", "stats", "bio", "location", "website", "socialLinks", "returnPolicy", "shippingPolicy", "isVacationMode", "vacationMessage", "createdAt"];
|
|
29
29
|
/** Everything else, with the reason it stays server-side. */
|
|
30
|
-
export declare const PRIVATE_STORE_FIELDS: readonly ["updatedAt", "whatsappConfig", "adminNotes", "isVerified", "suspensionReason", "capabilities", "customCommissionRate", "googleReviews", "shippingConfig", "emiEnabled", "isFeatured", "vacationReturnDate", "isTestData", "testDataExpiresAt"];
|
|
30
|
+
export declare const PRIVATE_STORE_FIELDS: readonly ["updatedAt", "whatsappConfig", "adminNotes", "statusHistory", "statusHistoryTruncated", "isVerified", "suspensionReason", "capabilities", "customCommissionRate", "googleReviews", "shippingConfig", "emiEnabled", "isFeatured", "vacationReturnDate", "isTestData", "testDataExpiresAt"];
|
|
31
31
|
/**
|
|
32
32
|
* Accepted input: a `StoreDocument` (the common case) or a loosely-typed
|
|
33
33
|
* Firestore row. The legacy top-level counters below predate `stats` and are
|
|
@@ -29,6 +29,11 @@ export const PRIVATE_STORE_FIELDS = [
|
|
|
29
29
|
"updatedAt", // bookkeeping; no public reader
|
|
30
30
|
"whatsappConfig", // Meta access token + WABA/catalog ids — secret
|
|
31
31
|
"adminNotes", // schema comment: never shown to the owner or the public
|
|
32
|
+
// Admin decisions with actor UIDs and suspension reasons. The store OWNER
|
|
33
|
+
// sees this on their own admin surfaces; an anonymous visitor to /stores has
|
|
34
|
+
// no business reading who suspended whom and why.
|
|
35
|
+
"statusHistory",
|
|
36
|
+
"statusHistoryTruncated",
|
|
32
37
|
"isVerified", // admin moderation state; surfaced via a separate badge feed if ever needed
|
|
33
38
|
"suspensionReason", // admin moderation note
|
|
34
39
|
"capabilities", // internal feature gating; server-enforced, never client-read
|
|
@@ -32,7 +32,11 @@ async function releasePendingBuyouts(ctx, product, holds) {
|
|
|
32
32
|
for (const hold of holds) {
|
|
33
33
|
try {
|
|
34
34
|
await cartRepository.removeItemsByBidId(hold.data.userId, hold.id);
|
|
35
|
-
await bidRepository.markCancelled(hold.id
|
|
35
|
+
await bidRepository.markCancelled(hold.id, {
|
|
36
|
+
actor: { role: "system" },
|
|
37
|
+
trigger: "auctionSettlement:lapsedBuyoutHold",
|
|
38
|
+
reason: "The auction ended before this Buy Now claim was paid for.",
|
|
39
|
+
});
|
|
36
40
|
await sendNotification({
|
|
37
41
|
userId: hold.data.userId,
|
|
38
42
|
type: "auction_ended",
|
|
@@ -84,7 +88,9 @@ async function settleAuction(ctx, product) {
|
|
|
84
88
|
const reserve = product.reservePrice;
|
|
85
89
|
if (typeof reserve === "number" && reserve > 0 && winnerEntry.data.bidAmount < reserve) {
|
|
86
90
|
const batch = ctx.db.batch();
|
|
87
|
-
|
|
91
|
+
// `data` comes from the same query as `ref`, so the timeline entry is
|
|
92
|
+
// free even on an auction that closed with hundreds of bids (Rule #6).
|
|
93
|
+
activeBids.forEach(({ ref, data }) => bidRepository.markLost(batch, ref, { actor: { role: "system" }, trigger: "auctionSettlement" }, data));
|
|
88
94
|
productRepository.updateStatusInBatch(batch, product.id, "archived");
|
|
89
95
|
await batch.commit();
|
|
90
96
|
const reserveStore = product.storeId
|
|
@@ -117,8 +123,8 @@ async function settleAuction(ctx, product) {
|
|
|
117
123
|
return;
|
|
118
124
|
}
|
|
119
125
|
const batch = ctx.db.batch();
|
|
120
|
-
bidRepository.markWon(batch, winnerEntry.ref);
|
|
121
|
-
loserEntries.forEach(({ ref }) => bidRepository.markLost(batch, ref));
|
|
126
|
+
bidRepository.markWon(batch, winnerEntry.ref, { actor: { role: "system" }, trigger: "auctionSettlement" }, winnerEntry.data);
|
|
127
|
+
loserEntries.forEach(({ ref, data }) => bidRepository.markLost(batch, ref, { actor: { role: "system" }, trigger: "auctionSettlement" }, data));
|
|
122
128
|
// Two-axis model: mark as sold without changing publish status.
|
|
123
129
|
batch.update(ctx.db.collection("products").doc(product.id), {
|
|
124
130
|
isSold: true,
|
|
@@ -155,7 +155,11 @@ async function lapseUnpaidAuctionWins(ctx) {
|
|
|
155
155
|
for (const { userId, item } of buyoutHolds) {
|
|
156
156
|
try {
|
|
157
157
|
await cartRepository.removeItemsByBidId(userId, item.bidId);
|
|
158
|
-
await bidRepository.markCancelled(item.bidId
|
|
158
|
+
await bidRepository.markCancelled(item.bidId, {
|
|
159
|
+
actor: { role: "system" },
|
|
160
|
+
trigger: "runOfferExpiry:buyoutLapsed",
|
|
161
|
+
reason: "The Buy Now checkout window closed without payment.",
|
|
162
|
+
});
|
|
159
163
|
await sendNotification({
|
|
160
164
|
userId,
|
|
161
165
|
type: "auction_ended",
|
|
@@ -211,7 +215,11 @@ async function lapseUnpaidAuctionWins(ctx) {
|
|
|
211
215
|
for (const { userId, item } of auctionLines) {
|
|
212
216
|
try {
|
|
213
217
|
await cartRepository.removeItemsByBidId(userId, item.bidId);
|
|
214
|
-
await bidRepository.markForfeited(item.bidId
|
|
218
|
+
await bidRepository.markForfeited(item.bidId, {
|
|
219
|
+
actor: { role: "system" },
|
|
220
|
+
trigger: "runOfferExpiry:winUnpaid",
|
|
221
|
+
reason: "The 48-hour checkout window closed without payment.",
|
|
222
|
+
});
|
|
215
223
|
await sendNotification({
|
|
216
224
|
userId,
|
|
217
225
|
type: "auction_ended",
|
|
@@ -9,7 +9,19 @@ import { payoutRepository } from "../../../../repositories";
|
|
|
9
9
|
const MAX_FAILURES = 3;
|
|
10
10
|
async function dispatch(ctx, entry) {
|
|
11
11
|
const { ref, data: payout } = entry;
|
|
12
|
-
|
|
12
|
+
/*
|
|
13
|
+
* The batch's running copy of the document, threaded through every write.
|
|
14
|
+
*
|
|
15
|
+
* Two reasons it is not just `payout`. (a) The snapshot came from the
|
|
16
|
+
* pending query, so history costs no extra read. (b) The sequence is
|
|
17
|
+
* pending → processing → failed; diffing the final write against the
|
|
18
|
+
* ORIGINAL snapshot would record "pending → failed" and lose the dispatch
|
|
19
|
+
* attempt that is the whole reason anyone opens the timeline.
|
|
20
|
+
*/
|
|
21
|
+
let current = payout;
|
|
22
|
+
const actor = { role: "system" };
|
|
23
|
+
await payoutRepository.markProcessing(ref, { actor }, current);
|
|
24
|
+
current = { ...current, status: "processing" };
|
|
13
25
|
try {
|
|
14
26
|
const keyId = ctx.env("RAZORPAY_KEY_ID") ?? "";
|
|
15
27
|
const keySecret = ctx.env("RAZORPAY_KEY_SECRET") ?? "";
|
|
@@ -67,7 +79,7 @@ async function dispatch(ctx, entry) {
|
|
|
67
79
|
throw new Error(`Razorpay ${response.status}: ${errBody}`);
|
|
68
80
|
}
|
|
69
81
|
const result = (await response.json());
|
|
70
|
-
await payoutRepository.recordSuccess(ref, result.id, result.status);
|
|
82
|
+
await payoutRepository.recordSuccess(ref, result.id, result.status, { actor }, current);
|
|
71
83
|
ctx.logger.info(`Payout ${payout.id} dispatched`, { razorpayId: result.id, razorpayStatus: result.status });
|
|
72
84
|
}
|
|
73
85
|
catch (error) {
|
|
@@ -75,7 +87,7 @@ async function dispatch(ctx, entry) {
|
|
|
75
87
|
const failureCount = (payout.failureCount ?? 0) + 1;
|
|
76
88
|
const isFinal = failureCount >= MAX_FAILURES;
|
|
77
89
|
const reason = error instanceof Error ? error.message : String(error);
|
|
78
|
-
await payoutRepository.recordFailure(ref, failureCount, reason, isFinal);
|
|
90
|
+
await payoutRepository.recordFailure(ref, failureCount, reason, isFinal, { actor }, current);
|
|
79
91
|
if (isFinal) {
|
|
80
92
|
ctx.logger.error(`Payout ${payout.id} permanently failed after ${MAX_FAILURES} attempts`, error, {
|
|
81
93
|
sellerId: payout.storeId,
|
|
@@ -41,7 +41,7 @@ export declare const productInputSchema: z.ZodObject<{
|
|
|
41
41
|
condition?: "new" | "used" | "refurbished" | "like_new" | "good" | "fair" | "poor" | "broken" | undefined;
|
|
42
42
|
insurance?: boolean | undefined;
|
|
43
43
|
insuranceCost?: number | undefined;
|
|
44
|
-
shippingPaidBy?: "
|
|
44
|
+
shippingPaidBy?: "buyer" | "seller" | undefined;
|
|
45
45
|
seoTitle?: string | undefined;
|
|
46
46
|
seoDescription?: string | undefined;
|
|
47
47
|
seoKeywords?: string[] | undefined;
|
|
@@ -65,7 +65,7 @@ export declare const productInputSchema: z.ZodObject<{
|
|
|
65
65
|
condition?: "new" | "used" | "refurbished" | "like_new" | "good" | "fair" | "poor" | "broken" | undefined;
|
|
66
66
|
insurance?: boolean | undefined;
|
|
67
67
|
insuranceCost?: number | undefined;
|
|
68
|
-
shippingPaidBy?: "
|
|
68
|
+
shippingPaidBy?: "buyer" | "seller" | undefined;
|
|
69
69
|
seoTitle?: string | undefined;
|
|
70
70
|
seoDescription?: string | undefined;
|
|
71
71
|
seoKeywords?: string[] | undefined;
|
|
@@ -114,7 +114,7 @@ export declare const productUpdateSchema: z.ZodObject<{
|
|
|
114
114
|
condition?: "new" | "used" | "refurbished" | "like_new" | "good" | "fair" | "poor" | "broken" | undefined;
|
|
115
115
|
insurance?: boolean | undefined;
|
|
116
116
|
insuranceCost?: number | undefined;
|
|
117
|
-
shippingPaidBy?: "
|
|
117
|
+
shippingPaidBy?: "buyer" | "seller" | undefined;
|
|
118
118
|
seoTitle?: string | undefined;
|
|
119
119
|
seoDescription?: string | undefined;
|
|
120
120
|
seoKeywords?: string[] | undefined;
|
|
@@ -138,7 +138,7 @@ export declare const productUpdateSchema: z.ZodObject<{
|
|
|
138
138
|
condition?: "new" | "used" | "refurbished" | "like_new" | "good" | "fair" | "poor" | "broken" | undefined;
|
|
139
139
|
insurance?: boolean | undefined;
|
|
140
140
|
insuranceCost?: number | undefined;
|
|
141
|
-
shippingPaidBy?: "
|
|
141
|
+
shippingPaidBy?: "buyer" | "seller" | undefined;
|
|
142
142
|
seoTitle?: string | undefined;
|
|
143
143
|
seoDescription?: string | undefined;
|
|
144
144
|
seoKeywords?: string[] | undefined;
|
|
@@ -201,7 +201,7 @@ export declare const auctionInputSchema: z.ZodObject<{
|
|
|
201
201
|
condition?: "new" | "used" | "refurbished" | "like_new" | "good" | "fair" | "poor" | "broken" | undefined;
|
|
202
202
|
insurance?: boolean | undefined;
|
|
203
203
|
insuranceCost?: number | undefined;
|
|
204
|
-
shippingPaidBy?: "
|
|
204
|
+
shippingPaidBy?: "buyer" | "seller" | undefined;
|
|
205
205
|
reservePrice?: number | undefined;
|
|
206
206
|
buyNowPrice?: number | undefined;
|
|
207
207
|
minBidIncrement?: number | undefined;
|
|
@@ -233,7 +233,7 @@ export declare const auctionInputSchema: z.ZodObject<{
|
|
|
233
233
|
condition?: "new" | "used" | "refurbished" | "like_new" | "good" | "fair" | "poor" | "broken" | undefined;
|
|
234
234
|
insurance?: boolean | undefined;
|
|
235
235
|
insuranceCost?: number | undefined;
|
|
236
|
-
shippingPaidBy?: "
|
|
236
|
+
shippingPaidBy?: "buyer" | "seller" | undefined;
|
|
237
237
|
reservePrice?: number | undefined;
|
|
238
238
|
buyNowPrice?: number | undefined;
|
|
239
239
|
minBidIncrement?: number | undefined;
|
|
@@ -299,7 +299,7 @@ export declare const preOrderInputSchema: z.ZodObject<{
|
|
|
299
299
|
condition?: "new" | "used" | "refurbished" | "like_new" | "good" | "fair" | "poor" | "broken" | undefined;
|
|
300
300
|
insurance?: boolean | undefined;
|
|
301
301
|
insuranceCost?: number | undefined;
|
|
302
|
-
shippingPaidBy?: "
|
|
302
|
+
shippingPaidBy?: "buyer" | "seller" | undefined;
|
|
303
303
|
preOrderDepositPercent?: number | undefined;
|
|
304
304
|
preOrderMaxQuantity?: number | undefined;
|
|
305
305
|
seoTitle?: string | undefined;
|
|
@@ -327,7 +327,7 @@ export declare const preOrderInputSchema: z.ZodObject<{
|
|
|
327
327
|
condition?: "new" | "used" | "refurbished" | "like_new" | "good" | "fair" | "poor" | "broken" | undefined;
|
|
328
328
|
insurance?: boolean | undefined;
|
|
329
329
|
insuranceCost?: number | undefined;
|
|
330
|
-
shippingPaidBy?: "
|
|
330
|
+
shippingPaidBy?: "buyer" | "seller" | undefined;
|
|
331
331
|
preOrderDepositPercent?: number | undefined;
|
|
332
332
|
preOrderMaxQuantity?: number | undefined;
|
|
333
333
|
preOrderProductionStatus?: "upcoming" | "in_production" | "ready_to_ship" | undefined;
|
|
@@ -75,7 +75,7 @@ export declare const createCouponSchema: z.ZodObject<{
|
|
|
75
75
|
}, "strip", z.ZodTypeAny, {
|
|
76
76
|
code: string;
|
|
77
77
|
type: "fixed" | "percentage" | "free_shipping" | "buy_x_get_y";
|
|
78
|
-
scope: "
|
|
78
|
+
scope: "seller" | "admin";
|
|
79
79
|
name: string;
|
|
80
80
|
discount: {
|
|
81
81
|
value: number;
|
|
@@ -99,7 +99,7 @@ export declare const createCouponSchema: z.ZodObject<{
|
|
|
99
99
|
}, {
|
|
100
100
|
code: string;
|
|
101
101
|
type: "fixed" | "percentage" | "free_shipping" | "buy_x_get_y";
|
|
102
|
-
scope: "
|
|
102
|
+
scope: "seller" | "admin";
|
|
103
103
|
name: string;
|
|
104
104
|
discount: {
|
|
105
105
|
value: number;
|
|
@@ -177,7 +177,7 @@ export declare const updateCouponSchema: z.ZodObject<Omit<{
|
|
|
177
177
|
}>>>;
|
|
178
178
|
}, "code">, "strip", z.ZodTypeAny, {
|
|
179
179
|
type?: "fixed" | "percentage" | "free_shipping" | "buy_x_get_y" | undefined;
|
|
180
|
-
scope?: "
|
|
180
|
+
scope?: "seller" | "admin" | undefined;
|
|
181
181
|
name?: string | undefined;
|
|
182
182
|
discount?: {
|
|
183
183
|
value: number;
|
|
@@ -200,7 +200,7 @@ export declare const updateCouponSchema: z.ZodObject<Omit<{
|
|
|
200
200
|
} | undefined;
|
|
201
201
|
}, {
|
|
202
202
|
type?: "fixed" | "percentage" | "free_shipping" | "buy_x_get_y" | undefined;
|
|
203
|
-
scope?: "
|
|
203
|
+
scope?: "seller" | "admin" | undefined;
|
|
204
204
|
name?: string | undefined;
|
|
205
205
|
discount?: {
|
|
206
206
|
value: number;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type { HistoryActorRole, HistoryActor, FieldChange, StatusChangeEntry, AppendHistoryResult, } from "./types";
|
|
1
|
+
export type { HistoryActorRole, HistoryActor, FieldChange, StatusChangeEntry, AppendHistoryResult, SerialisedStatusChangeEntry, } from "./types";
|
|
2
2
|
export { STATUS_HISTORY_MAX } from "./types";
|
|
3
3
|
export type { BuildHistoryEntryInput } from "./history";
|
|
4
4
|
export { diffTrackedFields, buildHistoryEntry, appendHistoryEntry, withHistory, } from "./history";
|
|
@@ -59,3 +59,19 @@ export interface AppendHistoryResult {
|
|
|
59
59
|
*/
|
|
60
60
|
truncatedCount: number;
|
|
61
61
|
}
|
|
62
|
+
/**
|
|
63
|
+
* A `StatusChangeEntry` after it has crossed the RSC boundary.
|
|
64
|
+
*
|
|
65
|
+
* Identical except `at` is an ISO string. Server Components must serialise
|
|
66
|
+
* before handing history to a `"use client"` component: every other date prop
|
|
67
|
+
* on those drawers is already a string, and mixing the two is how a
|
|
68
|
+
* `toLocaleString` on a string ends up rendering "Invalid Date".
|
|
69
|
+
*
|
|
70
|
+
* Declared once here rather than restated per page — two client files had
|
|
71
|
+
* hand-written copies, which is both a duplication and (because each spelled
|
|
72
|
+
* `changes` as `Record<string, { from: unknown; to: unknown }>`) an untyped
|
|
73
|
+
* seam `audit-unknown-leakage` correctly refused.
|
|
74
|
+
*/
|
|
75
|
+
export interface SerialisedStatusChangeEntry extends Omit<StatusChangeEntry, "at"> {
|
|
76
|
+
at: string;
|
|
77
|
+
}
|
package/dist/client.d.ts
CHANGED
|
@@ -164,6 +164,8 @@ export type { ColorPickerFieldProps } from "./ui/forms";
|
|
|
164
164
|
export { ColorPickerField } from "./ui/forms";
|
|
165
165
|
export type { FormErrorSummaryProps } from "./ui/forms";
|
|
166
166
|
export { FormErrorSummary } from "./ui/forms";
|
|
167
|
+
export { RecordStatusTimeline } from "./features/status-history/index";
|
|
168
|
+
export type { SerialisedStatusChangeEntry } from "./_internal/shared/history/types";
|
|
167
169
|
export type { FormErrorListProps } from "./ui/forms";
|
|
168
170
|
export { FormErrorList } from "./ui/forms";
|
|
169
171
|
export { useFormBottomActions } from "./features/layout/index";
|
package/dist/client.js
CHANGED
|
@@ -222,6 +222,7 @@ export { FieldCheckbox } from "./ui/forms";
|
|
|
222
222
|
export { FieldTextarea } from "./ui/forms";
|
|
223
223
|
export { ColorPickerField } from "./ui/forms";
|
|
224
224
|
export { FormErrorSummary } from "./ui/forms";
|
|
225
|
+
export { RecordStatusTimeline } from "./features/status-history/index";
|
|
225
226
|
export { FormErrorList } from "./ui/forms";
|
|
226
227
|
export { useFormBottomActions } from "./features/layout/index";
|
|
227
228
|
export { OverlayContext, useIsInsideOverlay } from "./ui/components/overlay-context";
|
|
@@ -341,6 +341,18 @@ export declare const AD_FIELDS: {
|
|
|
341
341
|
readonly PAUSED: "paused";
|
|
342
342
|
};
|
|
343
343
|
};
|
|
344
|
+
/**
|
|
345
|
+
* The ad fields whose changes earn a `statusHistory` entry.
|
|
346
|
+
*
|
|
347
|
+
* Ads live inside the `siteSettings` singleton and have no repository, so
|
|
348
|
+
* `withHistory` is called from the route directly — it is a pure function, so
|
|
349
|
+
* that is fine. This list lives here beside `AD_FIELDS` rather than in a
|
|
350
|
+
* schema module because ads have no schema module of their own.
|
|
351
|
+
*
|
|
352
|
+
* The scheduling dates are tracked alongside `status`: an ad silently
|
|
353
|
+
* rescheduled reads on the timeline as the same kind of event as one paused.
|
|
354
|
+
*/
|
|
355
|
+
export declare const AD_TRACKED_FIELDS: readonly ["status", "startAt", "endAt"];
|
|
344
356
|
export declare const EVENT_FIELDS: {
|
|
345
357
|
readonly ID: "id";
|
|
346
358
|
readonly TITLE: "title";
|
|
@@ -390,6 +390,18 @@ export const AD_FIELDS = {
|
|
|
390
390
|
PAUSED: "paused",
|
|
391
391
|
},
|
|
392
392
|
};
|
|
393
|
+
/**
|
|
394
|
+
* The ad fields whose changes earn a `statusHistory` entry.
|
|
395
|
+
*
|
|
396
|
+
* Ads live inside the `siteSettings` singleton and have no repository, so
|
|
397
|
+
* `withHistory` is called from the route directly — it is a pure function, so
|
|
398
|
+
* that is fine. This list lives here beside `AD_FIELDS` rather than in a
|
|
399
|
+
* schema module because ads have no schema module of their own.
|
|
400
|
+
*
|
|
401
|
+
* The scheduling dates are tracked alongside `status`: an ad silently
|
|
402
|
+
* rescheduled reads on the timeline as the same kind of event as one paused.
|
|
403
|
+
*/
|
|
404
|
+
export const AD_TRACKED_FIELDS = ["status", "startAt", "endAt"];
|
|
393
405
|
// ============================================================================
|
|
394
406
|
// EVENT FIELDS
|
|
395
407
|
// ============================================================================
|
|
@@ -72,8 +72,11 @@ export async function adminUpdatePayout(adminId, id, input) {
|
|
|
72
72
|
throw new NotFoundError("Payout not found");
|
|
73
73
|
}
|
|
74
74
|
const updateData = {
|
|
75
|
-
...(input.status !== undefined ? { status: input.status } : {}),
|
|
76
75
|
...(input.adminNote !== undefined ? { adminNote: input.adminNote } : {}),
|
|
76
|
+
// The payment reference. Absent from this pick until 2026-08-26, so every
|
|
77
|
+
// payout marked paid was stored WITHOUT the UTR the modal required — see
|
|
78
|
+
// PAYOUT_ADMIN_UPDATEABLE_FIELDS for the full trace.
|
|
79
|
+
...(input.transactionId !== undefined ? { transactionId: input.transactionId } : {}),
|
|
77
80
|
...(input.processedAt
|
|
78
81
|
? {
|
|
79
82
|
processedAt: input.processedAt instanceof Date
|
|
@@ -82,12 +85,19 @@ export async function adminUpdatePayout(adminId, id, input) {
|
|
|
82
85
|
}
|
|
83
86
|
: {}),
|
|
84
87
|
};
|
|
85
|
-
|
|
88
|
+
/*
|
|
89
|
+
* Through `updateStatus`, not a bare `update`, so the change lands on the
|
|
90
|
+
* payout's own timeline — `existing` is threaded in as `prior`, so that
|
|
91
|
+
* costs no second read of a document this function already holds.
|
|
92
|
+
*/
|
|
93
|
+
const updated = input.status !== undefined
|
|
94
|
+
? await payoutRepository.updateStatus(id, input.status, updateData, { actor: { role: "admin", uid: adminId }, trigger: "adminUpdatePayout" }, existing)
|
|
95
|
+
: await payoutRepository.update(id, updateData);
|
|
86
96
|
serverLogger.info("adminUpdatePayout", {
|
|
87
97
|
adminId,
|
|
88
98
|
payoutId: id,
|
|
89
99
|
});
|
|
90
|
-
if (
|
|
100
|
+
if (input.status === "paid") {
|
|
91
101
|
void recordAdminAction({
|
|
92
102
|
actorUid: adminId,
|
|
93
103
|
action: AdminAuditActionValues.PAYOUT_MARK_PAID,
|
|
@@ -14,6 +14,7 @@ import { buildBidDetailFields, bidStatusBadge } from "../../auctions/components/
|
|
|
14
14
|
import { toRecordArray, toRelativeDate, toCurrency, toStringValue, } from "../hooks/useAdminListingData";
|
|
15
15
|
import { DataListingView } from "./DataListingView";
|
|
16
16
|
import { apiClient } from "../../../http";
|
|
17
|
+
import { RecordStatusTimeline } from "../../status-history/components/RecordStatusTimeline";
|
|
17
18
|
export function AdminBidsView({ children, ...props }) {
|
|
18
19
|
const queryClient = useQueryClient();
|
|
19
20
|
const { showToast } = useToast();
|
|
@@ -98,7 +99,12 @@ export function AdminBidsView({ children, ...props }) {
|
|
|
98
99
|
const detailBadge = detail ? bidStatusBadge(detail.status) : null;
|
|
99
100
|
return (_jsxs(_Fragment, { children: [_jsx(DataListingView, { config: config }), _jsx(RecordDetailModal, { isOpen: detail !== null, onClose: () => setDetail(null), title: toStringValue(detail?.detail?.productTitle, "Bid"), badges: detailBadge ? [{ label: detailBadge.label }] : undefined, fields: detail
|
|
100
101
|
? buildBidDetailFields(detail.detail, "admin")
|
|
101
|
-
: undefined
|
|
102
|
+
: undefined, extra:
|
|
103
|
+
// A bid moves through more states than any record a buyer owns, and
|
|
104
|
+
// every one of them happened inside a settlement batch with nothing
|
|
105
|
+
// recording it until W18. Absent history renders the empty label,
|
|
106
|
+
// never a step invented from `updatedAt`.
|
|
107
|
+
_jsx(RecordStatusTimeline, { entries: detail?.detail?.statusHistory, truncatedCount: detail?.detail?.statusHistoryTruncated }) }), _jsx(ConfirmDeleteModal, { isOpen: cancelOpen, onClose: () => {
|
|
102
108
|
setCancelOpen(false);
|
|
103
109
|
setSelectedRow(null);
|
|
104
110
|
}, onConfirm: () => {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import React from "react";
|
|
2
|
+
import type { StatusChangeEntry } from "../../../_internal/shared/history/types";
|
|
2
3
|
import { type ScammerStatus } from "../../scams/schemas/firestore";
|
|
3
4
|
export interface AdminScammerEditorViewProps {
|
|
4
5
|
open: boolean;
|
|
@@ -11,7 +12,10 @@ export interface AdminScammerEditorViewProps {
|
|
|
11
12
|
upiIds?: string[];
|
|
12
13
|
currentStatus?: ScammerStatus;
|
|
13
14
|
verificationNote?: string;
|
|
15
|
+
/** The profile's own decision history. Absent on profiles predating W18. */
|
|
16
|
+
statusHistory?: StatusChangeEntry[];
|
|
17
|
+
statusHistoryTruncated?: number;
|
|
14
18
|
reportedBy?: string;
|
|
15
19
|
reportedByAnon?: boolean;
|
|
16
20
|
}
|
|
17
|
-
export declare function AdminScammerEditorView({ open, onClose, scammerId, displayNames, scamType, description, phones, upiIds, currentStatus, verificationNote, reportedBy, reportedByAnon, }: AdminScammerEditorViewProps): React.JSX.Element;
|
|
21
|
+
export declare function AdminScammerEditorView({ open, onClose, scammerId, displayNames, scamType, description, phones, upiIds, currentStatus, verificationNote, statusHistory, statusHistoryTruncated, reportedBy, reportedByAnon, }: AdminScammerEditorViewProps): React.JSX.Element;
|
|
@@ -6,6 +6,7 @@ import { useQueryClient } from "@tanstack/react-query";
|
|
|
6
6
|
import { Button, Code, Div, FormActions, HorizontalRule, Label, Row, Select, SideDrawer, Span, Stack, Text, Textarea, useToast } from "../../../ui";
|
|
7
7
|
import { apiClient } from "../../../http";
|
|
8
8
|
import { ADMIN_ENDPOINTS } from "../../../constants/api-endpoints";
|
|
9
|
+
import { RecordStatusTimeline } from "../../status-history/components/RecordStatusTimeline";
|
|
9
10
|
import { ScammerStatusValues, } from "../../scams/schemas/firestore";
|
|
10
11
|
import { SCAMMER_STATUS_BADGE, SCAMMER_STATUS_LABEL, SCAMMER_STATUS_OPTIONS, } from "../../scams/constants/scammer-status-display";
|
|
11
12
|
const __P = {
|
|
@@ -13,7 +14,7 @@ const __P = {
|
|
|
13
14
|
p4: "p-[var(--appkit-space-4)]",
|
|
14
15
|
};
|
|
15
16
|
const CLS_SECTION_LABEL = "text-[length:var(--appkit-text-xs)] font-medium text-[var(--appkit-color-text-muted)] uppercase tracking-wide";
|
|
16
|
-
export function AdminScammerEditorView({ open, onClose, scammerId, displayNames = [], scamType, description, phones = [], upiIds = [], currentStatus, verificationNote, reportedBy, reportedByAnon, }) {
|
|
17
|
+
export function AdminScammerEditorView({ open, onClose, scammerId, displayNames = [], scamType, description, phones = [], upiIds = [], currentStatus, verificationNote, statusHistory, statusHistoryTruncated, reportedBy, reportedByAnon, }) {
|
|
17
18
|
const queryClient = useQueryClient();
|
|
18
19
|
const { showToast } = useToast();
|
|
19
20
|
const [status, setStatus] = React.useState(currentStatus ?? "pending_review");
|
|
@@ -53,5 +54,5 @@ export function AdminScammerEditorView({ open, onClose, scammerId, displayNames
|
|
|
53
54
|
showToast(err?.message ?? "Failed to delete profile.", "error");
|
|
54
55
|
},
|
|
55
56
|
});
|
|
56
|
-
return (_jsx(SideDrawer, { isOpen: open, onClose: onClose, title: displayNames.length > 0 ? displayNames[0] : "Scammer Profile", children: _jsxs(Stack, { className: `${__P.p4}`, gap: "md", children: [_jsxs(Row, { align: "center", gap: "sm", children: [_jsx(Span, { size: "xs", weight: "medium", className: `inline-flex ${SCAMMER_STATUS_BADGE[currentStatus ?? ScammerStatusValues.PENDING_REVIEW]}`, rounded: "full", padding: "pill-sm-tall", children: SCAMMER_STATUS_LABEL[currentStatus ?? ScammerStatusValues.PENDING_REVIEW] }), scamType && (_jsx(Span, { size: "xs", rounded: "full", padding: "pill-sm-tall", surface: "subtle", color: "muted", children: scamType.replace(/_/g, " ") }))] }), displayNames.length > 0 && (_jsxs(Div, { children: [_jsx(Text, { className: "mb-1 tracking-wide", color: "muted", size: "xs", weight: "semibold", transform: "uppercase", children: "Names / Aliases" }), _jsx(Row, { wrap: true, gap: "xs", children: displayNames.map((name, i) => (_jsx(Span, { size: "xs", rounded: "full", surface: "subtle", padding: "pill-sm", children: name }, i))) })] })), (phones.length > 0 || upiIds.length > 0) && (_jsxs(Div, { className: `${__P.p3}`, rounded: "lg", surface: "muted", border: "default", children: [phones.length > 0 && (_jsxs(Div, { className: "mb-2", children: [_jsx(Text, { className: CLS_SECTION_LABEL, children: "Phone numbers" }), _jsx(Row, { wrap: true, gap: "xs", children: phones.map((p, i) => (_jsx(Code, { size: "xs", rounded: "default", padding: "sm", surface: "subtle", children: p }, i))) })] })), upiIds.length > 0 && (_jsxs(_Fragment, { children: [_jsx(Text, { className: CLS_SECTION_LABEL, children: "UPI IDs" }), _jsx(Row, { wrap: true, gap: "xs", children: upiIds.map((u, i) => (_jsx(Code, { size: "xs", rounded: "default", padding: "sm", surface: "subtle", children: u }, i))) })] }))] })), description && (_jsxs(Div, { className: `${__P.p3}`, rounded: "lg", surface: "muted", border: "default", children: [_jsx(Text, { className: CLS_SECTION_LABEL, children: "Description" }), _jsx(Text, { className: "whitespace-pre-wrap", color: "primary", size: "sm", children: description })] })), _jsxs(Div, { textSize: "xs", color: "muted", children: ["Reported by:", " ", _jsx(Span, { weight: "medium", color: "muted", children: reportedByAnon ? "Anonymous" : (reportedBy ?? "Unknown") })] }), _jsx(HorizontalRule, { tone: "subtle", spacing: "comfortable" }), _jsx(Select, { label: "Status", options: SCAMMER_STATUS_OPTIONS, value: status, onValueChange: setStatus }), _jsxs(Stack, { gap: "xs", children: [_jsx(Label, { className: "uppercase tracking-wide", color: "muted", size: "xs", weight: "semibold", children: "Verification note (internal)" }), _jsx(Textarea, { value: note, onChange: (e) => setNote(e.target.value), rows: 3, placeholder: "e.g. Verified via 3 independent reports\u2026" })] }), _jsxs(FormActions, { align: "right", children: [_jsx(Button, { type: "button", variant: "danger", size: "sm", disabled: !scammerId, isLoading: deleteMutation.isPending, onClick: () => deleteMutation.mutate(), children: "Delete" }), _jsx(Button, { type: "button", variant: "secondary", onClick: onClose, children: "Cancel" }), _jsx(Button, { type: "button", isLoading: updateMutation.isPending, disabled: !scammerId || updateMutation.isPending, onClick: () => updateMutation.mutate(), children: "Save changes" })] })] }) }));
|
|
57
|
+
return (_jsx(SideDrawer, { isOpen: open, onClose: onClose, title: displayNames.length > 0 ? displayNames[0] : "Scammer Profile", children: _jsxs(Stack, { className: `${__P.p4}`, gap: "md", children: [_jsx(RecordStatusTimeline, { title: "Decision history", entries: statusHistory, truncatedCount: statusHistoryTruncated }), _jsxs(Row, { align: "center", gap: "sm", children: [_jsx(Span, { size: "xs", weight: "medium", className: `inline-flex ${SCAMMER_STATUS_BADGE[currentStatus ?? ScammerStatusValues.PENDING_REVIEW]}`, rounded: "full", padding: "pill-sm-tall", children: SCAMMER_STATUS_LABEL[currentStatus ?? ScammerStatusValues.PENDING_REVIEW] }), scamType && (_jsx(Span, { size: "xs", rounded: "full", padding: "pill-sm-tall", surface: "subtle", color: "muted", children: scamType.replace(/_/g, " ") }))] }), displayNames.length > 0 && (_jsxs(Div, { children: [_jsx(Text, { className: "mb-1 tracking-wide", color: "muted", size: "xs", weight: "semibold", transform: "uppercase", children: "Names / Aliases" }), _jsx(Row, { wrap: true, gap: "xs", children: displayNames.map((name, i) => (_jsx(Span, { size: "xs", rounded: "full", surface: "subtle", padding: "pill-sm", children: name }, i))) })] })), (phones.length > 0 || upiIds.length > 0) && (_jsxs(Div, { className: `${__P.p3}`, rounded: "lg", surface: "muted", border: "default", children: [phones.length > 0 && (_jsxs(Div, { className: "mb-2", children: [_jsx(Text, { className: CLS_SECTION_LABEL, children: "Phone numbers" }), _jsx(Row, { wrap: true, gap: "xs", children: phones.map((p, i) => (_jsx(Code, { size: "xs", rounded: "default", padding: "sm", surface: "subtle", children: p }, i))) })] })), upiIds.length > 0 && (_jsxs(_Fragment, { children: [_jsx(Text, { className: CLS_SECTION_LABEL, children: "UPI IDs" }), _jsx(Row, { wrap: true, gap: "xs", children: upiIds.map((u, i) => (_jsx(Code, { size: "xs", rounded: "default", padding: "sm", surface: "subtle", children: u }, i))) })] }))] })), description && (_jsxs(Div, { className: `${__P.p3}`, rounded: "lg", surface: "muted", border: "default", children: [_jsx(Text, { className: CLS_SECTION_LABEL, children: "Description" }), _jsx(Text, { className: "whitespace-pre-wrap", color: "primary", size: "sm", children: description })] })), _jsxs(Div, { textSize: "xs", color: "muted", children: ["Reported by:", " ", _jsx(Span, { weight: "medium", color: "muted", children: reportedByAnon ? "Anonymous" : (reportedBy ?? "Unknown") })] }), _jsx(HorizontalRule, { tone: "subtle", spacing: "comfortable" }), _jsx(Select, { label: "Status", options: SCAMMER_STATUS_OPTIONS, value: status, onValueChange: setStatus }), _jsxs(Stack, { gap: "xs", children: [_jsx(Label, { className: "uppercase tracking-wide", color: "muted", size: "xs", weight: "semibold", children: "Verification note (internal)" }), _jsx(Textarea, { value: note, onChange: (e) => setNote(e.target.value), rows: 3, placeholder: "e.g. Verified via 3 independent reports\u2026" })] }), _jsxs(FormActions, { align: "right", children: [_jsx(Button, { type: "button", variant: "danger", size: "sm", disabled: !scammerId, isLoading: deleteMutation.isPending, onClick: () => deleteMutation.mutate(), children: "Delete" }), _jsx(Button, { type: "button", variant: "secondary", onClick: onClose, children: "Cancel" }), _jsx(Button, { type: "button", isLoading: updateMutation.isPending, disabled: !scammerId || updateMutation.isPending, onClick: () => updateMutation.mutate(), children: "Save changes" })] })] }) }));
|
|
57
58
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import React from "react";
|
|
2
|
+
import type { StatusChangeEntry } from "../../../_internal/shared/history/types";
|
|
2
3
|
interface TicketMessageClient {
|
|
3
4
|
id?: string;
|
|
4
5
|
authorId?: string;
|
|
@@ -28,6 +29,9 @@ export interface AdminSupportTicketDetailViewProps {
|
|
|
28
29
|
orderId?: string;
|
|
29
30
|
/** ST-6 — current relatedParties snapshot from the ticket document. */
|
|
30
31
|
relatedParties?: RelatedPartiesClient;
|
|
32
|
+
/** The ticket's `statusHistory`. Absent on tickets predating W18. */
|
|
33
|
+
statusHistory?: StatusChangeEntry[];
|
|
34
|
+
statusHistoryTruncated?: number;
|
|
31
35
|
}
|
|
32
|
-
export declare function AdminSupportTicketDetailView({ open, onClose, ticketId, subject, userDisplayName, category, currentStatus, currentPriority, description, messages, internalNotes, orderId, relatedParties, }: AdminSupportTicketDetailViewProps): React.JSX.Element;
|
|
36
|
+
export declare function AdminSupportTicketDetailView({ open, onClose, ticketId, subject, userDisplayName, category, currentStatus, currentPriority, description, messages, internalNotes, orderId, relatedParties, statusHistory, statusHistoryTruncated, }: AdminSupportTicketDetailViewProps): React.JSX.Element;
|
|
33
37
|
export {};
|
|
@@ -6,6 +6,7 @@ import { useQuery, useQueryClient } from "@tanstack/react-query";
|
|
|
6
6
|
import { Button, Div, FormActions, HorizontalRule, Input, Label, Row, Select, SideDrawer, Span, Stack, Text, Textarea, Toggle, useToast } from "../../../ui";
|
|
7
7
|
import { apiClient } from "../../../http";
|
|
8
8
|
import { ADMIN_ENDPOINTS, SUPPORT_ENDPOINTS } from "../../../constants/api-endpoints";
|
|
9
|
+
import { RecordStatusTimeline } from "../../status-history/components/RecordStatusTimeline";
|
|
9
10
|
const __P = {
|
|
10
11
|
p3: "p-[var(--appkit-space-3)]",
|
|
11
12
|
p4: "p-[var(--appkit-space-4)]",
|
|
@@ -38,7 +39,7 @@ const ROLE_LABEL = {
|
|
|
38
39
|
support: "Support",
|
|
39
40
|
admin: "Admin",
|
|
40
41
|
};
|
|
41
|
-
export function AdminSupportTicketDetailView({ open, onClose, ticketId, subject, userDisplayName, category, currentStatus, currentPriority, description, messages = [], internalNotes, orderId, relatedParties, }) {
|
|
42
|
+
export function AdminSupportTicketDetailView({ open, onClose, ticketId, subject, userDisplayName, category, currentStatus, currentPriority, description, messages = [], internalNotes, orderId, relatedParties, statusHistory, statusHistoryTruncated, }) {
|
|
42
43
|
const queryClient = useQueryClient();
|
|
43
44
|
const { showToast } = useToast();
|
|
44
45
|
const [status, setStatus] = React.useState(currentStatus ?? "open");
|
|
@@ -259,7 +260,7 @@ export function AdminSupportTicketDetailView({ open, onClose, ticketId, subject,
|
|
|
259
260
|
showToast(err?.message ?? "Failed to update order items.", "error");
|
|
260
261
|
},
|
|
261
262
|
});
|
|
262
|
-
return (_jsx(SideDrawer, { isOpen: open, onClose: onClose, title: subject ?? "Support Ticket", children: _jsxs(Stack, { className: `${__P.p4}`, gap: "md", children: [_jsxs(Row, { align: "center", gap: "sm", wrap: true, children: [_jsx(Span, { size: "xs", weight: "medium", className: `inline-flex ${STATUS_COLOR[currentStatus ?? "open"] ?? STATUS_COLOR.open}`, rounded: "full", padding: "pill-sm-tall", children: (currentStatus ?? "open").replace(/_/g, " ") }), _jsx(Span, { size: "xs", rounded: "full", padding: "pill-sm-tall", surface: "subtle", color: "muted", children: category }), orderId && (_jsxs(Span, { size: "xs", rounded: "full", padding: "pill-sm-tall", surface: "subtle", color: "muted", children: ["Order: ", orderId] }))] }), _jsxs(Div, { textSize: "sm", color: "muted", children: ["From: ", _jsx(Span, { weight: "medium", color: "primary", children: userDisplayName })] }), description && (_jsxs(Div, { className: `${__P.p3}`, rounded: "lg", surface: "muted", border: "default", children: [_jsx(Text, { className: "mb-1 tracking-wide", color: "muted", size: "xs", weight: "semibold", transform: "uppercase", children: "Description" }), _jsx(Text, { className: "whitespace-pre-wrap", color: "primary", size: "sm", children: description })] })), messages.length > 0 && (_jsxs(Stack, { gap: "sm", children: [_jsxs(Text, { className: "tracking-wide", color: "muted", size: "xs", weight: "semibold", transform: "uppercase", children: ["Messages (", messages.length, ")"] }), _jsx(Stack, { className: `max-h-64 ${__O.yAuto}`, gap: "sm", children: messages.map((msg, i) => (_jsxs(Div, { textSize: "sm", surface: msg.authorRole === "user" ? "default" : "info-surface", className: msg.authorRole === "user" ? "border border-[var(--appkit-color-border)]" : "border border-[var(--appkit-color-info)]", rounded: "lg", padding: "sm", children: [_jsxs(Row, { color: "muted", textSize: "xs", className: "mb-1", align: "center", gap: "sm", children: [_jsx(Span, { weight: "medium", color: "muted", children: ROLE_LABEL[msg.authorRole ?? "user"] ?? msg.authorRole }), msg.createdAt && (_jsx(Span, { children: new Date(msg.createdAt).toLocaleString() }))] }), _jsx(Text, { className: "whitespace-pre-wrap", color: "primary", children: msg.body })] }, msg.id ?? i))) })] })), _jsxs(Stack, { gap: "xs", children: [_jsx(Label, { className: "uppercase tracking-wide", color: "muted", size: "xs", weight: "semibold", children: "Reply to user" }), _jsx(Textarea, { value: replyBody, onChange: (e) => setReplyBody(e.target.value), rows: 3, placeholder: "Type a reply\u2026" }), _jsx(Button, { type: "button", variant: "primary", size: "sm", isLoading: replyMutation.isPending, disabled: !replyBody.trim() || !ticketId || replyMutation.isPending, onClick: () => replyMutation.mutate(), children: "Send reply" })] }), _jsx(HorizontalRule, { tone: "subtle", spacing: "comfortable" }), _jsx(Select, { label: "Status", options: STATUS_OPTIONS, value: status, onValueChange: setStatus }), _jsx(Select, { label: "Priority", options: PRIORITY_OPTIONS, value: priority, onValueChange: setPriority }), _jsxs(Stack, { gap: "xs", children: [_jsx(Label, { className: "uppercase tracking-wide", color: "muted", size: "xs", weight: "semibold", children: "Internal notes (staff only)" }), _jsx(Textarea, { value: notes, onChange: (e) => setNotes(e.target.value), rows: 3, placeholder: "Notes visible only to admins and employees\u2026" })] }), _jsxs(Stack, { padding: "sm", gap: "sm", rounded: "lg", border: "default", children: [_jsx(Text, { className: "tracking-wide", color: "muted", size: "xs", weight: "semibold", transform: "uppercase", children: "Linked parties" }), _jsx(Text, { size: "xs", color: "muted", children: "Tag the buyer / store / order / product / bid this ticket concerns." }), [
|
|
263
|
+
return (_jsx(SideDrawer, { isOpen: open, onClose: onClose, title: subject ?? "Support Ticket", children: _jsxs(Stack, { className: `${__P.p4}`, gap: "md", children: [_jsxs(Row, { align: "center", gap: "sm", wrap: true, children: [_jsx(Span, { size: "xs", weight: "medium", className: `inline-flex ${STATUS_COLOR[currentStatus ?? "open"] ?? STATUS_COLOR.open}`, rounded: "full", padding: "pill-sm-tall", children: (currentStatus ?? "open").replace(/_/g, " ") }), _jsx(Span, { size: "xs", rounded: "full", padding: "pill-sm-tall", surface: "subtle", color: "muted", children: category }), orderId && (_jsxs(Span, { size: "xs", rounded: "full", padding: "pill-sm-tall", surface: "subtle", color: "muted", children: ["Order: ", orderId] }))] }), _jsxs(Div, { textSize: "sm", color: "muted", children: ["From: ", _jsx(Span, { weight: "medium", color: "primary", children: userDisplayName })] }), description && (_jsxs(Div, { className: `${__P.p3}`, rounded: "lg", surface: "muted", border: "default", children: [_jsx(Text, { className: "mb-1 tracking-wide", color: "muted", size: "xs", weight: "semibold", transform: "uppercase", children: "Description" }), _jsx(Text, { className: "whitespace-pre-wrap", color: "primary", size: "sm", children: description })] })), messages.length > 0 && (_jsxs(Stack, { gap: "sm", children: [_jsxs(Text, { className: "tracking-wide", color: "muted", size: "xs", weight: "semibold", transform: "uppercase", children: ["Messages (", messages.length, ")"] }), _jsx(Stack, { className: `max-h-64 ${__O.yAuto}`, gap: "sm", children: messages.map((msg, i) => (_jsxs(Div, { textSize: "sm", surface: msg.authorRole === "user" ? "default" : "info-surface", className: msg.authorRole === "user" ? "border border-[var(--appkit-color-border)]" : "border border-[var(--appkit-color-info)]", rounded: "lg", padding: "sm", children: [_jsxs(Row, { color: "muted", textSize: "xs", className: "mb-1", align: "center", gap: "sm", children: [_jsx(Span, { weight: "medium", color: "muted", children: ROLE_LABEL[msg.authorRole ?? "user"] ?? msg.authorRole }), msg.createdAt && (_jsx(Span, { children: new Date(msg.createdAt).toLocaleString() }))] }), _jsx(Text, { className: "whitespace-pre-wrap", color: "primary", children: msg.body })] }, msg.id ?? i))) })] })), _jsxs(Stack, { gap: "xs", children: [_jsx(Label, { className: "uppercase tracking-wide", color: "muted", size: "xs", weight: "semibold", children: "Reply to user" }), _jsx(Textarea, { value: replyBody, onChange: (e) => setReplyBody(e.target.value), rows: 3, placeholder: "Type a reply\u2026" }), _jsx(Button, { type: "button", variant: "primary", size: "sm", isLoading: replyMutation.isPending, disabled: !replyBody.trim() || !ticketId || replyMutation.isPending, onClick: () => replyMutation.mutate(), children: "Send reply" })] }), _jsx(HorizontalRule, { tone: "subtle", spacing: "comfortable" }), _jsx(RecordStatusTimeline, { entries: statusHistory, truncatedCount: statusHistoryTruncated }), _jsx(HorizontalRule, { tone: "subtle", spacing: "comfortable" }), _jsx(Select, { label: "Status", options: STATUS_OPTIONS, value: status, onValueChange: setStatus }), _jsx(Select, { label: "Priority", options: PRIORITY_OPTIONS, value: priority, onValueChange: setPriority }), _jsxs(Stack, { gap: "xs", children: [_jsx(Label, { className: "uppercase tracking-wide", color: "muted", size: "xs", weight: "semibold", children: "Internal notes (staff only)" }), _jsx(Textarea, { value: notes, onChange: (e) => setNotes(e.target.value), rows: 3, placeholder: "Notes visible only to admins and employees\u2026" })] }), _jsxs(Stack, { padding: "sm", gap: "sm", rounded: "lg", border: "default", children: [_jsx(Text, { className: "tracking-wide", color: "muted", size: "xs", weight: "semibold", transform: "uppercase", children: "Linked parties" }), _jsx(Text, { size: "xs", color: "muted", children: "Tag the buyer / store / order / product / bid this ticket concerns." }), [
|
|
263
264
|
["userId", "User slug"],
|
|
264
265
|
["storeId", "Store slug"],
|
|
265
266
|
["orderId", "Order ID"],
|
|
@@ -7,6 +7,14 @@ import { BaseRepository, parseSieveDateValue } from "../../../providers/db-fireb
|
|
|
7
7
|
import type { DocumentReference, WriteBatch } from "firebase-admin/firestore";
|
|
8
8
|
import type { SieveModel, FirebaseSieveResult } from "../../../providers/db-firebase";
|
|
9
9
|
import type { BidDocument, BidCreateInput, BidUpdateInput, BidAdminUpdateInput, BidStatus } from "../schemas";
|
|
10
|
+
import { type HistoryActor } from "../../../_internal/shared/history/index";
|
|
11
|
+
/** Who/why for a bid write that lands in `statusHistory`. */
|
|
12
|
+
export interface BidWriteContext {
|
|
13
|
+
actor?: HistoryActor;
|
|
14
|
+
trigger?: string;
|
|
15
|
+
reason?: string;
|
|
16
|
+
note?: string;
|
|
17
|
+
}
|
|
10
18
|
export declare class BidRepository extends BaseRepository<BidDocument> {
|
|
11
19
|
constructor();
|
|
12
20
|
/** Override mapDoc to auto-decrypt PII on every bid read */
|
|
@@ -49,7 +57,7 @@ export declare class BidRepository extends BaseRepository<BidDocument> {
|
|
|
49
57
|
ref: DocumentReference;
|
|
50
58
|
data: BidDocument;
|
|
51
59
|
} | null>;
|
|
52
|
-
markWon(batch: WriteBatch, ref: DocumentReference): void;
|
|
60
|
+
markWon(batch: WriteBatch, ref: DocumentReference, ctx?: BidWriteContext, prior?: BidDocument | null): void;
|
|
53
61
|
/**
|
|
54
62
|
* Link a won bid to the order it eventually became.
|
|
55
63
|
*
|
|
@@ -58,29 +66,34 @@ export declare class BidRepository extends BaseRepository<BidDocument> {
|
|
|
58
66
|
* /user/bids could not link anywhere, and nothing could tell a settled win
|
|
59
67
|
* from an unpaid one.
|
|
60
68
|
*/
|
|
61
|
-
attachOrder(bidId: string, orderId: string): Promise<void>;
|
|
69
|
+
attachOrder(bidId: string, orderId: string, ctx?: BidWriteContext, prior?: BidDocument | null): Promise<void>;
|
|
62
70
|
/**
|
|
63
71
|
* The winner never paid within their checkout window. Distinct from "lost"
|
|
64
72
|
* (outbid) — this bidder won and then defaulted, which a seller may want to
|
|
65
73
|
* act on.
|
|
66
74
|
*/
|
|
67
|
-
markForfeited(bidId: string): Promise<void>;
|
|
68
|
-
markLost(batch: WriteBatch, ref: DocumentReference): void;
|
|
75
|
+
markForfeited(bidId: string, ctx?: BidWriteContext, prior?: BidDocument | null): Promise<void>;
|
|
76
|
+
markLost(batch: WriteBatch, ref: DocumentReference, ctx?: BidWriteContext, prior?: BidDocument | null): void;
|
|
69
77
|
/**
|
|
70
78
|
* Batched "everyone else lost", for callers that are not already inside a
|
|
71
79
|
* batch of their own — a Buy Now purchase ends the auction outside the
|
|
72
80
|
* settlement job, and the losing bids can be any number, so this writes them
|
|
73
81
|
* in Firestore-batch chunks rather than one round-trip per bid.
|
|
74
82
|
*/
|
|
75
|
-
markManyLost(
|
|
83
|
+
markManyLost(entries: readonly (DocumentReference | {
|
|
84
|
+
ref: DocumentReference;
|
|
85
|
+
data?: BidDocument;
|
|
86
|
+
})[], ctx?: BidWriteContext): Promise<void>;
|
|
76
87
|
/**
|
|
77
88
|
* A pending Buy Now claim that never became a sale. Not `forfeited` — that
|
|
78
89
|
* status means "won, then defaulted", and a lapsed claim never won anything;
|
|
79
90
|
* the auction it was placed against carried on untouched.
|
|
80
91
|
*/
|
|
81
|
-
markCancelled(bidId: string): Promise<void>;
|
|
82
|
-
markOutbid(batch: WriteBatch, ref: DocumentReference): void;
|
|
83
|
-
markWinning(batch: WriteBatch, ref: DocumentReference): void;
|
|
92
|
+
markCancelled(bidId: string, ctx?: BidWriteContext, prior?: BidDocument | null): Promise<void>;
|
|
93
|
+
markOutbid(batch: WriteBatch, ref: DocumentReference, ctx?: BidWriteContext, prior?: BidDocument | null): void;
|
|
94
|
+
markWinning(batch: WriteBatch, ref: DocumentReference, ctx?: BidWriteContext, prior?: BidDocument | null): void;
|
|
95
|
+
/** Append a timeline entry when the patch touches a tracked field. */
|
|
96
|
+
private withBidHistory;
|
|
84
97
|
/**
|
|
85
98
|
* Find current winning bid for a product
|
|
86
99
|
*/
|