@mohasinac/appkit 2.7.33 → 2.7.35
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/jobs/core/auctionSettlement.js +14 -14
- package/dist/_internal/server/jobs/core/offerExpiry.js +3 -2
- package/dist/_internal/server/jobs/core/onBidPlaced.js +14 -12
- package/dist/_internal/server/jobs/core/onOrderStatusChange.js +6 -71
- package/dist/_internal/server/jobs/core/onSupportTicketCreate.js +6 -8
- package/dist/_internal/server/jobs/core/onSupportTicketUpdate.js +6 -7
- package/dist/_internal/server/jobs/core/pendingOrderTimeout.js +11 -10
- package/dist/_internal/server/jobs/core/prizeRevealExpiry.js +2 -2
- package/dist/_internal/server/jobs/core/prizeRevealOpen.js +2 -2
- package/dist/_internal/server/jobs/core/prizeRevealReminder.js +2 -2
- package/dist/_internal/shared/actions/action-registry.js +48 -0
- package/dist/client.d.ts +4 -0
- package/dist/client.js +2 -0
- package/dist/features/admin/actions/notification-actions.js +18 -8
- package/dist/features/admin/schemas/firestore.d.ts +6 -2
- package/dist/features/admin/schemas/firestore.js +4 -0
- package/dist/features/homepage/components/FeaturedAuctionsSection.d.ts +4 -1
- package/dist/features/homepage/components/FeaturedAuctionsSection.js +2 -2
- package/dist/features/homepage/components/FeaturedPreOrdersSection.d.ts +4 -1
- package/dist/features/homepage/components/FeaturedPreOrdersSection.js +2 -2
- package/dist/features/homepage/components/FeaturedProductsSection.d.ts +3 -6
- package/dist/features/homepage/components/FeaturedProductsSection.js +3 -47
- package/dist/features/homepage/lib/section-renderer.js +3 -3
- package/dist/features/orders/actions/refund-actions.js +2 -2
- package/dist/features/seller/actions/offer-actions.js +5 -5
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/seed/actions/demo-seed-actions.d.ts +1 -1
- package/dist/seed/index.d.ts +1 -0
- package/dist/seed/index.js +1 -0
- package/dist/seed/manifest.js +5 -0
- package/dist/seed/offers-seed-data.d.ts +10 -0
- package/dist/seed/offers-seed-data.js +264 -0
- package/dist/seed/orders-seed-data.js +116 -2
- package/dist/seed/support-tickets-seed-data.js +43 -0
- package/dist/server.d.ts +1 -0
- package/dist/server.js +1 -0
- package/dist/tailwind-utilities.css +1 -1
- package/dist/ui/components/DateInput.d.ts +27 -0
- package/dist/ui/components/DateInput.js +11 -0
- package/dist/ui/components/FormField.d.ts +2 -1
- package/dist/ui/components/FormField.js +6 -4
- package/dist/ui/components/FormField.style.css +13 -0
- package/dist/ui/components/HorizontalScroller.js +25 -30
- package/dist/ui/components/Input.style.css +42 -4
- package/dist/ui/components/OtpInput.d.ts +14 -0
- package/dist/ui/components/OtpInput.js +55 -0
- package/dist/ui/components/OtpInput.style.css +13 -0
- package/dist/ui/components/Select.style.css +23 -6
- package/dist/ui/components/Textarea.style.css +31 -4
- package/dist/ui/components/index.style.css +1 -0
- package/dist/ui/index.d.ts +4 -0
- package/dist/ui/index.js +2 -0
- package/package.json +1 -1
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { bidRepository,
|
|
1
|
+
import { bidRepository, orderRepository, productRepository, } from "../../../../repositories";
|
|
2
|
+
import { sendNotification } from "../../../../features/admin/actions/notification-actions";
|
|
2
3
|
import { ProductStatusValues } from "../../../../features/products/schemas/firestore";
|
|
3
4
|
import { AUCTION_MESSAGES } from "../handlers/messages";
|
|
4
5
|
async function settleAuction(ctx, product) {
|
|
@@ -25,7 +26,9 @@ async function settleAuction(ctx, product) {
|
|
|
25
26
|
auctionProductId: product.id,
|
|
26
27
|
});
|
|
27
28
|
productRepository.updateStatusInBatch(batch, product.id, ProductStatusValues.SOLD);
|
|
28
|
-
|
|
29
|
+
await batch.commit();
|
|
30
|
+
// Fan-out notifications after batch commits.
|
|
31
|
+
await sendNotification({
|
|
29
32
|
userId: winnerEntry.data.userId,
|
|
30
33
|
type: "bid_won",
|
|
31
34
|
priority: "high",
|
|
@@ -34,18 +37,15 @@ async function settleAuction(ctx, product) {
|
|
|
34
37
|
relatedId: product.id,
|
|
35
38
|
relatedType: "product",
|
|
36
39
|
});
|
|
37
|
-
loserEntries.slice(0, 50).
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
});
|
|
47
|
-
});
|
|
48
|
-
await batch.commit();
|
|
40
|
+
await Promise.allSettled(loserEntries.slice(0, 50).map(({ data: bid }) => sendNotification({
|
|
41
|
+
userId: bid.userId,
|
|
42
|
+
type: "bid_lost",
|
|
43
|
+
priority: "normal",
|
|
44
|
+
title: AUCTION_MESSAGES.LOST_TITLE,
|
|
45
|
+
message: AUCTION_MESSAGES.LOST_MESSAGE(product.title),
|
|
46
|
+
relatedId: product.id,
|
|
47
|
+
relatedType: "product",
|
|
48
|
+
})));
|
|
49
49
|
ctx.logger.info(`Settled auction ${product.id}`, {
|
|
50
50
|
winner: winnerEntry.data.userId,
|
|
51
51
|
winningBid: winnerEntry.data.bidAmount,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { offerRepository
|
|
1
|
+
import { offerRepository } from "../../../../repositories";
|
|
2
|
+
import { sendNotification } from "../../../../features/admin/actions/notification-actions";
|
|
2
3
|
export async function runOfferExpiry(ctx) {
|
|
3
4
|
ctx.logger.info("Starting offer expiry sweep");
|
|
4
5
|
let expiredOffers;
|
|
@@ -18,7 +19,7 @@ export async function runOfferExpiry(ctx) {
|
|
|
18
19
|
for (const offer of expiredOffers) {
|
|
19
20
|
try {
|
|
20
21
|
expiredIds.push(offer.id);
|
|
21
|
-
await
|
|
22
|
+
await sendNotification({
|
|
22
23
|
userId: offer.buyerUid,
|
|
23
24
|
type: "offer_expired",
|
|
24
25
|
priority: "normal",
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { bidRepository,
|
|
1
|
+
import { bidRepository, productRepository, } from "../../../../repositories";
|
|
2
2
|
import { decryptPii } from "../../../../security/index";
|
|
3
|
+
import { sendNotification } from "../../../../features/admin/actions/notification-actions";
|
|
3
4
|
import { getAdminRealtimeDb } from "../../../../providers/db-firebase";
|
|
4
5
|
import { BID_MESSAGES } from "../handlers/messages";
|
|
5
6
|
const BIDS_COLLECTION = "bids";
|
|
@@ -15,33 +16,34 @@ export async function handleBidPlaced(input, ctx) {
|
|
|
15
16
|
if (prevWinning && prevWinning.data.userId !== newBid.userId) {
|
|
16
17
|
outbidUserId = prevWinning.data.userId;
|
|
17
18
|
bidRepository.markOutbid(batch, prevWinning.ref);
|
|
18
|
-
|
|
19
|
+
}
|
|
20
|
+
productRepository.incrementBidCountInBatch(batch, newBid.productId, newBid.bidAmount);
|
|
21
|
+
await batch.commit();
|
|
22
|
+
if (outbidUserId) {
|
|
23
|
+
const outbidMessage = BID_MESSAGES.OUTBID_MESSAGE(newBid.productTitle, newBid.currency, newBid.bidAmount);
|
|
24
|
+
await sendNotification({
|
|
19
25
|
userId: outbidUserId,
|
|
20
26
|
type: "bid_outbid",
|
|
21
27
|
priority: "high",
|
|
22
28
|
title: BID_MESSAGES.OUTBID_TITLE,
|
|
23
|
-
message:
|
|
29
|
+
message: outbidMessage,
|
|
24
30
|
relatedId: newBid.productId,
|
|
25
31
|
relatedType: "product",
|
|
26
32
|
});
|
|
27
|
-
|
|
28
|
-
productRepository.incrementBidCountInBatch(batch, newBid.productId, newBid.bidAmount);
|
|
29
|
-
await batch.commit();
|
|
30
|
-
try {
|
|
31
|
-
if (outbidUserId) {
|
|
33
|
+
try {
|
|
32
34
|
await getAdminRealtimeDb()
|
|
33
35
|
.ref(`notifications/${outbidUserId}`)
|
|
34
36
|
.push({
|
|
35
37
|
type: "bid_outbid",
|
|
36
38
|
title: BID_MESSAGES.OUTBID_TITLE,
|
|
37
|
-
message:
|
|
39
|
+
message: outbidMessage,
|
|
38
40
|
timestamp: Date.now(),
|
|
39
41
|
read: false,
|
|
40
42
|
});
|
|
41
43
|
}
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
44
|
+
catch (rtdbError) {
|
|
45
|
+
ctx.logger.error("Realtime DB push failed (non-fatal)", rtdbError);
|
|
46
|
+
}
|
|
45
47
|
}
|
|
46
48
|
ctx.logger.info(`Bid placed on ${newBid.productId}`, {
|
|
47
49
|
bidId,
|
|
@@ -1,87 +1,33 @@
|
|
|
1
|
-
import { notificationRepository } from "../../../../repositories";
|
|
2
|
-
import { decryptPii } from "../../../../security/index";
|
|
3
1
|
import { getAdminRealtimeDb } from "../../../../providers/db-firebase";
|
|
4
|
-
import {
|
|
2
|
+
import { decryptPii } from "../../../../security/index";
|
|
3
|
+
import { sendNotification } from "../../../../features/admin/actions/notification-actions";
|
|
4
|
+
import { ORDER_MESSAGES } from "../handlers/messages";
|
|
5
5
|
const STATUS_CONFIG = {
|
|
6
6
|
confirmed: {
|
|
7
7
|
type: "order_confirmed",
|
|
8
8
|
title: ORDER_MESSAGES.CONFIRMED_TITLE,
|
|
9
9
|
message: (o) => ORDER_MESSAGES.CONFIRMED_MESSAGE(o.productTitle),
|
|
10
10
|
priority: "normal",
|
|
11
|
-
sendEmail: true,
|
|
12
11
|
},
|
|
13
12
|
shipped: {
|
|
14
13
|
type: "order_shipped",
|
|
15
14
|
title: ORDER_MESSAGES.SHIPPED_TITLE,
|
|
16
15
|
message: (o) => ORDER_MESSAGES.SHIPPED_MESSAGE(o.productTitle, o.trackingNumber),
|
|
17
16
|
priority: "high",
|
|
18
|
-
sendEmail: true,
|
|
19
17
|
},
|
|
20
18
|
delivered: {
|
|
21
19
|
type: "order_delivered",
|
|
22
20
|
title: ORDER_MESSAGES.DELIVERED_TITLE,
|
|
23
21
|
message: (o) => ORDER_MESSAGES.DELIVERED_MESSAGE(o.productTitle),
|
|
24
22
|
priority: "normal",
|
|
25
|
-
sendEmail: true,
|
|
26
23
|
},
|
|
27
24
|
cancelled: {
|
|
28
25
|
type: "order_cancelled",
|
|
29
26
|
title: ORDER_MESSAGES.CANCELLED_TITLE,
|
|
30
27
|
message: (o) => `Your order for "${o.productTitle}" has been cancelled.`,
|
|
31
28
|
priority: "normal",
|
|
32
|
-
sendEmail: false,
|
|
33
29
|
},
|
|
34
30
|
};
|
|
35
|
-
async function sendResendEmail(params) {
|
|
36
|
-
const subject = params.status === "confirmed"
|
|
37
|
-
? EMAIL_SUBJECTS.ORDER_CONFIRMED(params.productTitle)
|
|
38
|
-
: params.status === "shipped"
|
|
39
|
-
? EMAIL_SUBJECTS.ORDER_SHIPPED(params.productTitle)
|
|
40
|
-
: params.status === "delivered"
|
|
41
|
-
? EMAIL_SUBJECTS.ORDER_DELIVERED(params.productTitle)
|
|
42
|
-
: EMAIL_SUBJECTS.ORDER_UPDATE_FALLBACK(params.productTitle);
|
|
43
|
-
const response = await fetch("https://api.resend.com/emails", {
|
|
44
|
-
method: "POST",
|
|
45
|
-
headers: {
|
|
46
|
-
Authorization: `Bearer ${params.apiKey}`,
|
|
47
|
-
"Content-Type": "application/json",
|
|
48
|
-
},
|
|
49
|
-
body: JSON.stringify({
|
|
50
|
-
from: params.fromAddress,
|
|
51
|
-
to: [params.to],
|
|
52
|
-
subject,
|
|
53
|
-
html: `<p>Hi,</p><p>Your order for <strong>${params.productTitle}</strong> is now <strong>${params.status}</strong>.</p>${params.trackingNumber ? `<p>Tracking number: ${params.trackingNumber}</p>` : ""}<p>Thanks,<br/>${params.brandName} Team</p>`,
|
|
54
|
-
}),
|
|
55
|
-
});
|
|
56
|
-
if (!response.ok) {
|
|
57
|
-
const body = await response.text();
|
|
58
|
-
throw new Error(JOB_ERROR_MESSAGES.RESEND_API_ERROR(response.status, body));
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
async function sendStatusChangeEmail(ctx, userEmail, newStatus, orderId, after, apiKey) {
|
|
62
|
-
try {
|
|
63
|
-
const fromAddress = ctx.env("ORDER_EMAIL_FROM") ?? ctx.env("RESEND_FROM_ADDRESS") ?? "";
|
|
64
|
-
const brandName = ctx.env("APPKIT_BRAND_NAME") ?? "";
|
|
65
|
-
if (!fromAddress || !brandName) {
|
|
66
|
-
ctx.logger.error("ORDER_EMAIL_FROM / APPKIT_BRAND_NAME not configured — skipping order status email", null, { orderId });
|
|
67
|
-
}
|
|
68
|
-
else {
|
|
69
|
-
await sendResendEmail({
|
|
70
|
-
to: userEmail,
|
|
71
|
-
status: newStatus,
|
|
72
|
-
orderId,
|
|
73
|
-
productTitle: after.productTitle,
|
|
74
|
-
trackingNumber: after.trackingNumber,
|
|
75
|
-
apiKey,
|
|
76
|
-
fromAddress,
|
|
77
|
-
brandName,
|
|
78
|
-
});
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
catch (emailError) {
|
|
82
|
-
ctx.logger.error("Email send failed (non-fatal)", emailError, { orderId });
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
31
|
export async function handleOrderStatusChange(input, ctx) {
|
|
86
32
|
const { orderId, before, after } = input;
|
|
87
33
|
if (!before || !after)
|
|
@@ -100,7 +46,7 @@ export async function handleOrderStatusChange(input, ctx) {
|
|
|
100
46
|
productTitle: after.productTitle,
|
|
101
47
|
trackingNumber: after.trackingNumber,
|
|
102
48
|
});
|
|
103
|
-
await
|
|
49
|
+
await sendNotification({
|
|
104
50
|
userId: after.userId,
|
|
105
51
|
type: config.type,
|
|
106
52
|
priority: config.priority,
|
|
@@ -108,6 +54,7 @@ export async function handleOrderStatusChange(input, ctx) {
|
|
|
108
54
|
message: messageText,
|
|
109
55
|
relatedId: orderId,
|
|
110
56
|
relatedType: "order",
|
|
57
|
+
userEmail,
|
|
111
58
|
});
|
|
112
59
|
try {
|
|
113
60
|
await getAdminRealtimeDb().ref(`notifications/${after.userId}`).push({
|
|
@@ -121,19 +68,7 @@ export async function handleOrderStatusChange(input, ctx) {
|
|
|
121
68
|
catch (rtdbError) {
|
|
122
69
|
ctx.logger.error("Realtime DB push failed (non-fatal)", rtdbError);
|
|
123
70
|
}
|
|
124
|
-
|
|
125
|
-
const apiKey = ctx.env("RESEND_API_KEY") ?? "";
|
|
126
|
-
if (!apiKey) {
|
|
127
|
-
ctx.logger.error(JOB_ERROR_MESSAGES.RESEND_KEY_MISSING, null);
|
|
128
|
-
}
|
|
129
|
-
else {
|
|
130
|
-
await sendStatusChangeEmail(ctx, userEmail, newStatus, orderId, after, apiKey);
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
ctx.logger.info(`Order ${orderId} status → ${newStatus}`, {
|
|
134
|
-
userId: after.userId,
|
|
135
|
-
emailSent: config.sendEmail,
|
|
136
|
-
});
|
|
71
|
+
ctx.logger.info(`Order ${orderId} status → ${newStatus}`, { userId: after.userId });
|
|
137
72
|
}
|
|
138
73
|
catch (error) {
|
|
139
74
|
ctx.logger.error("Error handling order status change", error, { orderId });
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { sendNotification } from "../../../../features/admin/actions/notification-actions";
|
|
2
2
|
export async function handleSupportTicketCreate(input, ctx) {
|
|
3
3
|
const { ticketId, ticket } = input;
|
|
4
4
|
const userId = ticket.userId;
|
|
@@ -6,17 +6,15 @@ export async function handleSupportTicketCreate(input, ctx) {
|
|
|
6
6
|
ctx.logger.warn("onSupportTicketCreate: no userId on ticket", { ticketId });
|
|
7
7
|
return;
|
|
8
8
|
}
|
|
9
|
-
// Confirm to the user their ticket was received
|
|
10
9
|
try {
|
|
11
|
-
await
|
|
10
|
+
await sendNotification({
|
|
12
11
|
userId,
|
|
13
12
|
type: "account_action",
|
|
13
|
+
priority: "normal",
|
|
14
14
|
title: "Support ticket received",
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
entityType: "support_ticket",
|
|
19
|
-
createdAt: new Date(),
|
|
15
|
+
message: `We received your support request: "${ticket.subject ?? "your ticket"}". We'll get back to you soon.`,
|
|
16
|
+
relatedId: ticketId,
|
|
17
|
+
relatedType: "support_ticket",
|
|
20
18
|
});
|
|
21
19
|
}
|
|
22
20
|
catch (err) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { sendNotification } from "../../../../features/admin/actions/notification-actions";
|
|
2
2
|
const USER_NOTIFY_STATUSES = new Set(["resolved", "closed", "waiting_on_user", "in_progress"]);
|
|
3
3
|
const STATUS_MESSAGES = {
|
|
4
4
|
resolved: {
|
|
@@ -32,15 +32,14 @@ export async function handleSupportTicketUpdate(input, ctx) {
|
|
|
32
32
|
if (!msg)
|
|
33
33
|
return;
|
|
34
34
|
try {
|
|
35
|
-
await
|
|
35
|
+
await sendNotification({
|
|
36
36
|
userId,
|
|
37
37
|
type: "account_action",
|
|
38
|
+
priority: "normal",
|
|
38
39
|
title: msg.title,
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
entityType: "support_ticket",
|
|
43
|
-
createdAt: new Date(),
|
|
40
|
+
message: msg.body(subject),
|
|
41
|
+
relatedId: ticketId,
|
|
42
|
+
relatedType: "support_ticket",
|
|
44
43
|
});
|
|
45
44
|
}
|
|
46
45
|
catch (err) {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { orderRepository } from "../../../../repositories";
|
|
2
|
+
import { sendNotification } from "../../../../features/admin/actions/notification-actions";
|
|
2
3
|
import { ORDER_MESSAGES } from "../handlers/messages";
|
|
3
4
|
const DEFAULT_TIMEOUT_HOURS = 24;
|
|
4
5
|
export async function runPendingOrderTimeout(ctx) {
|
|
@@ -12,16 +13,16 @@ export async function runPendingOrderTimeout(ctx) {
|
|
|
12
13
|
const batch = ctx.db.batch();
|
|
13
14
|
for (const entry of timedOut) {
|
|
14
15
|
orderRepository.cancelInBatch(batch, entry.ref);
|
|
15
|
-
notificationRepository.createInBatch(batch, {
|
|
16
|
-
userId: entry.data.userId,
|
|
17
|
-
type: "order_cancelled",
|
|
18
|
-
priority: "normal",
|
|
19
|
-
title: ORDER_MESSAGES.CANCELLED_TITLE,
|
|
20
|
-
message: ORDER_MESSAGES.CANCELLED_TIMEOUT_MESSAGE(entry.data.productTitle, timeoutHours),
|
|
21
|
-
relatedId: entry.data.id,
|
|
22
|
-
relatedType: "order",
|
|
23
|
-
});
|
|
24
16
|
}
|
|
25
17
|
await batch.commit();
|
|
18
|
+
await Promise.allSettled(timedOut.map((entry) => sendNotification({
|
|
19
|
+
userId: entry.data.userId,
|
|
20
|
+
type: "order_cancelled",
|
|
21
|
+
priority: "normal",
|
|
22
|
+
title: ORDER_MESSAGES.CANCELLED_TITLE,
|
|
23
|
+
message: ORDER_MESSAGES.CANCELLED_TIMEOUT_MESSAGE(entry.data.productTitle, timeoutHours),
|
|
24
|
+
relatedId: entry.data.id,
|
|
25
|
+
relatedType: "order",
|
|
26
|
+
})));
|
|
26
27
|
ctx.logger.info("Pending order timeout complete", { cancelled: timedOut.length });
|
|
27
28
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { sendNotification } from "../../../../features/admin/actions/notification-actions";
|
|
2
2
|
import { ORDER_FIELDS, PRODUCT_FIELDS, COMMON_FIELDS } from "../../../../constants/field-names";
|
|
3
3
|
const ORDER_COLLECTION = "orders";
|
|
4
4
|
export async function runPrizeRevealExpiry(ctx) {
|
|
@@ -34,7 +34,7 @@ export async function runPrizeRevealExpiry(ctx) {
|
|
|
34
34
|
});
|
|
35
35
|
if (order.userId) {
|
|
36
36
|
try {
|
|
37
|
-
await
|
|
37
|
+
await sendNotification({
|
|
38
38
|
userId: order.userId,
|
|
39
39
|
type: "prize_reveal_expired",
|
|
40
40
|
priority: "high",
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { sendNotification } from "../../../../features/admin/actions/notification-actions";
|
|
2
2
|
import { PRODUCT_FIELDS, ORDER_FIELDS, COMMON_FIELDS } from "../../../../constants/field-names";
|
|
3
3
|
const PRODUCT_COLLECTION = "products";
|
|
4
4
|
const ORDER_COLLECTION = "orders";
|
|
@@ -38,7 +38,7 @@ export async function runPrizeRevealOpen(ctx) {
|
|
|
38
38
|
if (!order.userId || order.prizeWon)
|
|
39
39
|
continue;
|
|
40
40
|
try {
|
|
41
|
-
await
|
|
41
|
+
await sendNotification({
|
|
42
42
|
userId: order.userId,
|
|
43
43
|
type: "prize_reveal_ready",
|
|
44
44
|
priority: "high",
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { sendNotification } from "../../../../features/admin/actions/notification-actions";
|
|
2
2
|
import { ORDER_FIELDS, PRODUCT_FIELDS } from "../../../../constants/field-names";
|
|
3
3
|
const ORDER_COLLECTION = "orders";
|
|
4
4
|
const ONE_DAY_MS = 86400000;
|
|
@@ -23,7 +23,7 @@ export async function runPrizeRevealReminder(ctx) {
|
|
|
23
23
|
if (!order.userId || order.prizeWon || !order.prizeDrawProductId)
|
|
24
24
|
continue;
|
|
25
25
|
try {
|
|
26
|
-
await
|
|
26
|
+
await sendNotification({
|
|
27
27
|
userId: order.userId,
|
|
28
28
|
type: "prize_reveal_reminder",
|
|
29
29
|
priority: "normal",
|
|
@@ -620,6 +620,54 @@ export const ACTIONS = {
|
|
|
620
620
|
description: "Submit the order with the chosen payment method.",
|
|
621
621
|
kind: "primary",
|
|
622
622
|
},
|
|
623
|
+
"continue-to-verification": {
|
|
624
|
+
id: "checkout.continue-to-verification",
|
|
625
|
+
label: "Continue to Verification",
|
|
626
|
+
description: "Advance from address selection to identity verification step.",
|
|
627
|
+
kind: "primary",
|
|
628
|
+
},
|
|
629
|
+
"send-otp": {
|
|
630
|
+
id: "checkout.send-otp",
|
|
631
|
+
label: "Send verification code",
|
|
632
|
+
description: "Send a one-time code to the buyer's registered email to verify identity before checkout.",
|
|
633
|
+
kind: "primary",
|
|
634
|
+
},
|
|
635
|
+
"verify-otp": {
|
|
636
|
+
id: "checkout.verify-otp",
|
|
637
|
+
label: "Verify & Continue",
|
|
638
|
+
description: "Submit the one-time code and proceed to payment selection.",
|
|
639
|
+
kind: "primary",
|
|
640
|
+
},
|
|
641
|
+
"resend-otp": {
|
|
642
|
+
id: "checkout.resend-otp",
|
|
643
|
+
label: "Resend code",
|
|
644
|
+
description: "Re-send the verification code to the buyer's registered email.",
|
|
645
|
+
kind: "ghost",
|
|
646
|
+
},
|
|
647
|
+
"pay-online": {
|
|
648
|
+
id: "checkout.pay-online",
|
|
649
|
+
label: "Pay Online (Razorpay)",
|
|
650
|
+
description: "Initiate an online payment via Razorpay UPI/Card/NetBanking.",
|
|
651
|
+
kind: "primary",
|
|
652
|
+
},
|
|
653
|
+
"pay-cod": {
|
|
654
|
+
id: "checkout.pay-cod",
|
|
655
|
+
label: "Cash on Delivery",
|
|
656
|
+
description: "Place the order for cash-on-delivery payment.",
|
|
657
|
+
kind: "secondary",
|
|
658
|
+
},
|
|
659
|
+
"admin-bypass": {
|
|
660
|
+
id: "checkout.admin-bypass",
|
|
661
|
+
label: "Skip Verification — Admin Bypass",
|
|
662
|
+
description: "Admin test mode: skip identity verification and place a test order without payment.",
|
|
663
|
+
kind: "secondary",
|
|
664
|
+
},
|
|
665
|
+
"admin-bypass-payment": {
|
|
666
|
+
id: "checkout.admin-bypass-payment",
|
|
667
|
+
label: "No Payment — Admin Bypass Order",
|
|
668
|
+
description: "Admin test mode: place a real order record without charging any payment.",
|
|
669
|
+
kind: "secondary",
|
|
670
|
+
},
|
|
623
671
|
},
|
|
624
672
|
NAV: {
|
|
625
673
|
"sign-in": {
|
package/dist/client.d.ts
CHANGED
|
@@ -53,6 +53,10 @@ export { Badge } from "./ui/components/Badge";
|
|
|
53
53
|
export { Button } from "./ui/components/Button";
|
|
54
54
|
export { Checkbox } from "./ui/components/Checkbox";
|
|
55
55
|
export { Input } from "./ui/components/Input";
|
|
56
|
+
export { OtpInput } from "./ui/components/OtpInput";
|
|
57
|
+
export type { OtpInputProps } from "./ui/components/OtpInput";
|
|
58
|
+
export { DateInput, DateRangeInput } from "./ui/components/DateInput";
|
|
59
|
+
export type { DateInputProps, DateRangeInputProps } from "./ui/components/DateInput";
|
|
56
60
|
export { Select } from "./ui/components/Select";
|
|
57
61
|
export type { SelectOption, SelectProps } from "./ui/components/Select";
|
|
58
62
|
export { Heading } from "./ui/components/Typography";
|
package/dist/client.js
CHANGED
|
@@ -121,6 +121,8 @@ export { Badge } from "./ui/components/Badge";
|
|
|
121
121
|
export { Button } from "./ui/components/Button";
|
|
122
122
|
export { Checkbox } from "./ui/components/Checkbox";
|
|
123
123
|
export { Input } from "./ui/components/Input";
|
|
124
|
+
export { OtpInput } from "./ui/components/OtpInput";
|
|
125
|
+
export { DateInput, DateRangeInput } from "./ui/components/DateInput";
|
|
124
126
|
export { Select } from "./ui/components/Select";
|
|
125
127
|
export { Heading } from "./ui/components/Typography";
|
|
126
128
|
export { Label, Text } from "./ui/components/Typography";
|
|
@@ -4,6 +4,7 @@ import { userRepository } from "../../auth/repository/user.repository";
|
|
|
4
4
|
import { createResendProvider } from "../../../providers/email-resend/provider";
|
|
5
5
|
import { sendWhatsAppBusinessMessage } from "../../whatsapp-bot/helpers/whatsapp";
|
|
6
6
|
import { serverLogger } from "../../../monitoring";
|
|
7
|
+
import { decryptPii } from "../../../security/index";
|
|
7
8
|
import { DEFAULT_NOTIFICATION_CHANNELS, meetsMinPriority, } from "../schemas/firestore";
|
|
8
9
|
export async function markNotificationRead(id) {
|
|
9
10
|
await notificationRepository.markAsRead(id);
|
|
@@ -53,14 +54,20 @@ export async function sendNotification(input) {
|
|
|
53
54
|
// Load user notification preferences (best-effort — if missing, all channels allowed).
|
|
54
55
|
let userChannelPrefs = {};
|
|
55
56
|
let userTypePrefs = {};
|
|
57
|
+
let fetchedUserDoc;
|
|
56
58
|
try {
|
|
57
|
-
|
|
58
|
-
userChannelPrefs =
|
|
59
|
-
userTypePrefs =
|
|
59
|
+
fetchedUserDoc = await userRepository.findById(input.userId);
|
|
60
|
+
userChannelPrefs = fetchedUserDoc?.notificationPreferences?.channels ?? {};
|
|
61
|
+
userTypePrefs = fetchedUserDoc?.notificationPreferences?.types ?? {};
|
|
60
62
|
}
|
|
61
63
|
catch {
|
|
62
64
|
// non-fatal: fall through with empty prefs (all enabled)
|
|
63
65
|
}
|
|
66
|
+
// If caller didn't supply userEmail, try to decrypt it from the already-fetched user doc.
|
|
67
|
+
const resolvedEmail = userEmail ??
|
|
68
|
+
(fetchedUserDoc?.email ? decryptPii(fetchedUserDoc.email) : undefined);
|
|
69
|
+
const resolvedPhone = userPhone ??
|
|
70
|
+
(fetchedUserDoc?.phoneNumber ? decryptPii(fetchedUserDoc.phoneNumber) : undefined);
|
|
64
71
|
// Map NotificationType to a NotificationTypePrefs key.
|
|
65
72
|
const typeToPrefsKey = {
|
|
66
73
|
order_placed: "orderUpdates", order_confirmed: "orderUpdates",
|
|
@@ -69,11 +76,14 @@ export async function sendNotification(input) {
|
|
|
69
76
|
bid_placed: "bids", bid_outbid: "bids", bid_won: "bids", bid_lost: "bids",
|
|
70
77
|
review_approved: "reviews", review_replied: "reviews",
|
|
71
78
|
promotion: "promotions",
|
|
72
|
-
system: "system", welcome: "system",
|
|
79
|
+
system: "system", welcome: "system", account_action: "system",
|
|
73
80
|
offer_received: "offers", offer_responded: "offers",
|
|
74
81
|
offer_expired: "offers", offer_counter_accepted: "offers",
|
|
75
82
|
refund_initiated: "orderUpdates",
|
|
76
83
|
product_available: "system",
|
|
84
|
+
prize_reveal_ready: "orderUpdates",
|
|
85
|
+
prize_reveal_expired: "orderUpdates",
|
|
86
|
+
prize_reveal_reminder: "orderUpdates",
|
|
77
87
|
};
|
|
78
88
|
const typeKey = typeToPrefsKey[type];
|
|
79
89
|
// If the user has explicitly disabled this notification type, skip all external channels.
|
|
@@ -84,7 +94,7 @@ export async function sendNotification(input) {
|
|
|
84
94
|
let emailStatus = "skipped";
|
|
85
95
|
if (channels.email.enabled &&
|
|
86
96
|
userChannelPrefs.email !== false &&
|
|
87
|
-
|
|
97
|
+
resolvedEmail &&
|
|
88
98
|
meetsMinPriority(priority, channels.email.minPriority) &&
|
|
89
99
|
(!channels.email.types?.length || channels.email.types.includes(type))) {
|
|
90
100
|
const apiKey = creds.resendApiKey?.trim() ?? "";
|
|
@@ -94,7 +104,7 @@ export async function sendNotification(input) {
|
|
|
94
104
|
try {
|
|
95
105
|
const emailProvider = createResendProvider({ apiKey, fromEmail, fromName });
|
|
96
106
|
await emailProvider.send({
|
|
97
|
-
to:
|
|
107
|
+
to: resolvedEmail,
|
|
98
108
|
subject: title,
|
|
99
109
|
html: emailHtml ?? `<p>${message}</p>`,
|
|
100
110
|
text: message,
|
|
@@ -111,7 +121,7 @@ export async function sendNotification(input) {
|
|
|
111
121
|
let whatsappStatus = "skipped";
|
|
112
122
|
if (channels.whatsapp.enabled &&
|
|
113
123
|
userChannelPrefs.whatsapp !== false &&
|
|
114
|
-
|
|
124
|
+
resolvedPhone &&
|
|
115
125
|
meetsMinPriority(priority, channels.whatsapp.minPriority) &&
|
|
116
126
|
(!channels.whatsapp.types?.length || channels.whatsapp.types.includes(type))) {
|
|
117
127
|
const phoneNumberId = creds.whatsappPhoneNumberId?.trim() ?? "";
|
|
@@ -119,7 +129,7 @@ export async function sendNotification(input) {
|
|
|
119
129
|
if (phoneNumberId && accessToken) {
|
|
120
130
|
try {
|
|
121
131
|
const ok = await sendWhatsAppBusinessMessage({
|
|
122
|
-
toPhone:
|
|
132
|
+
toPhone: resolvedPhone,
|
|
123
133
|
message: `*${title}*\n${message}`,
|
|
124
134
|
phoneNumberId,
|
|
125
135
|
accessToken,
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Admin Feature Firestore Document Types & Constants
|
|
3
3
|
* Covers: notifications, chat rooms, site settings
|
|
4
4
|
*/
|
|
5
|
-
export type NotificationType = "order_placed" | "order_confirmed" | "order_shipped" | "order_delivered" | "order_cancelled" | "bid_placed" | "bid_outbid" | "bid_won" | "bid_lost" | "review_approved" | "review_replied" | "product_available" | "promotion" | "system" | "welcome" | "offer_received" | "offer_responded" | "offer_expired" | "offer_counter_accepted" | "refund_initiated";
|
|
5
|
+
export type NotificationType = "order_placed" | "order_confirmed" | "order_shipped" | "order_delivered" | "order_cancelled" | "bid_placed" | "bid_outbid" | "bid_won" | "bid_lost" | "review_approved" | "review_replied" | "product_available" | "promotion" | "system" | "welcome" | "account_action" | "offer_received" | "offer_responded" | "offer_expired" | "offer_counter_accepted" | "refund_initiated" | "prize_reveal_ready" | "prize_reveal_expired" | "prize_reveal_reminder";
|
|
6
6
|
export type NotificationPriority = "low" | "normal" | "high";
|
|
7
7
|
export interface NotificationDocument {
|
|
8
8
|
id: string;
|
|
@@ -17,7 +17,7 @@ export interface NotificationDocument {
|
|
|
17
17
|
isRead: boolean;
|
|
18
18
|
readAt?: Date;
|
|
19
19
|
relatedId?: string;
|
|
20
|
-
relatedType?: "order" | "product" | "bid" | "review" | "blog" | "user" | "offer";
|
|
20
|
+
relatedType?: "order" | "product" | "bid" | "review" | "blog" | "user" | "offer" | "support_ticket";
|
|
21
21
|
createdAt: Date;
|
|
22
22
|
updatedAt: Date;
|
|
23
23
|
}
|
|
@@ -60,6 +60,10 @@ export declare const NOTIFICATION_FIELDS: {
|
|
|
60
60
|
readonly OFFER_EXPIRED: NotificationType;
|
|
61
61
|
readonly OFFER_COUNTER_ACCEPTED: NotificationType;
|
|
62
62
|
readonly REFUND_INITIATED: NotificationType;
|
|
63
|
+
readonly ACCOUNT_ACTION: NotificationType;
|
|
64
|
+
readonly PRIZE_REVEAL_READY: NotificationType;
|
|
65
|
+
readonly PRIZE_REVEAL_EXPIRED: NotificationType;
|
|
66
|
+
readonly PRIZE_REVEAL_REMINDER: NotificationType;
|
|
63
67
|
};
|
|
64
68
|
readonly PRIORITY_VALUES: {
|
|
65
69
|
readonly LOW: NotificationPriority;
|
|
@@ -46,6 +46,10 @@ export const NOTIFICATION_FIELDS = {
|
|
|
46
46
|
OFFER_EXPIRED: "offer_expired",
|
|
47
47
|
OFFER_COUNTER_ACCEPTED: "offer_counter_accepted",
|
|
48
48
|
REFUND_INITIATED: "refund_initiated",
|
|
49
|
+
ACCOUNT_ACTION: "account_action",
|
|
50
|
+
PRIZE_REVEAL_READY: "prize_reveal_ready",
|
|
51
|
+
PRIZE_REVEAL_EXPIRED: "prize_reveal_expired",
|
|
52
|
+
PRIZE_REVEAL_REMINDER: "prize_reveal_reminder",
|
|
49
53
|
},
|
|
50
54
|
PRIORITY_VALUES: {
|
|
51
55
|
LOW: "low",
|
|
@@ -7,5 +7,8 @@ export interface FeaturedAuctionsSectionProps {
|
|
|
7
7
|
className?: string;
|
|
8
8
|
filterByBrand?: string;
|
|
9
9
|
initialItems?: ProductItem[];
|
|
10
|
+
rows?: number;
|
|
11
|
+
autoScroll?: boolean;
|
|
12
|
+
scrollInterval?: number;
|
|
10
13
|
}
|
|
11
|
-
export declare function FeaturedAuctionsSection({ title, description, viewMoreHref, viewMoreLabel, className, filterByBrand, initialItems, }: FeaturedAuctionsSectionProps): import("react/jsx-runtime").JSX.Element;
|
|
14
|
+
export declare function FeaturedAuctionsSection({ title, description, viewMoreHref, viewMoreLabel, className, filterByBrand, initialItems, rows, autoScroll, scrollInterval, }: FeaturedAuctionsSectionProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -4,7 +4,7 @@ import { THEME_CONSTANTS } from "../../../tokens";
|
|
|
4
4
|
import { SectionCarousel } from "./SectionCarousel";
|
|
5
5
|
import { useFeaturedAuctions } from "../hooks/useFeaturedAuctions";
|
|
6
6
|
import { MarketplaceAuctionCard } from "../../auctions/components/MarketplaceAuctionCard";
|
|
7
|
-
export function FeaturedAuctionsSection({ title = "Live Auctions", description, viewMoreHref, viewMoreLabel = "View all auctions →", className = "", filterByBrand, initialItems, }) {
|
|
7
|
+
export function FeaturedAuctionsSection({ title = "Live Auctions", description, viewMoreHref, viewMoreLabel = "View all auctions →", className = "", filterByBrand, initialItems, rows = 1, autoScroll = false, scrollInterval = 5000, }) {
|
|
8
8
|
const { data: items = [], isLoading } = useFeaturedAuctions({ filterByBrand, initialData: initialItems });
|
|
9
|
-
return (_jsx(SectionCarousel, { title: title, description: description, pillLabel: "Live Auctions", headingVariant: "editorial", viewMoreHref: viewMoreHref, viewMoreLabel: viewMoreLabel, items: items, isLoading: isLoading, skeletonCount: 4, perView: THEME_CONSTANTS.carousel.perView.standard, gap: 16, keyExtractor: (product) => product.id, renderItem: (product) => (_jsx(MarketplaceAuctionCard, { product: product })), className: className }));
|
|
9
|
+
return (_jsx(SectionCarousel, { title: title, description: description, pillLabel: "Live Auctions", headingVariant: "editorial", viewMoreHref: viewMoreHref, viewMoreLabel: viewMoreLabel, items: items, isLoading: isLoading, skeletonCount: 4, perView: THEME_CONSTANTS.carousel.perView.standard, gap: 16, rows: Math.min(Math.max(rows, 1), 4), autoScroll: autoScroll, autoScrollInterval: scrollInterval, keyExtractor: (product) => product.id, renderItem: (product) => (_jsx(MarketplaceAuctionCard, { product: product })), className: className }));
|
|
10
10
|
}
|
|
@@ -7,5 +7,8 @@ export interface FeaturedPreOrdersSectionProps {
|
|
|
7
7
|
className?: string;
|
|
8
8
|
filterByBrand?: string;
|
|
9
9
|
initialItems?: ProductItem[];
|
|
10
|
+
rows?: number;
|
|
11
|
+
autoScroll?: boolean;
|
|
12
|
+
scrollInterval?: number;
|
|
10
13
|
}
|
|
11
|
-
export declare function FeaturedPreOrdersSection({ title, description, viewMoreHref, viewMoreLabel, className, filterByBrand, initialItems, }: FeaturedPreOrdersSectionProps): import("react/jsx-runtime").JSX.Element;
|
|
14
|
+
export declare function FeaturedPreOrdersSection({ title, description, viewMoreHref, viewMoreLabel, className, filterByBrand, initialItems, rows, autoScroll, scrollInterval, }: FeaturedPreOrdersSectionProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -4,7 +4,7 @@ import { THEME_CONSTANTS } from "../../../tokens";
|
|
|
4
4
|
import { SectionCarousel } from "./SectionCarousel";
|
|
5
5
|
import { useFeaturedPreOrders } from "../hooks/useFeaturedPreOrders";
|
|
6
6
|
import { MarketplacePreorderCard } from "../../pre-orders/components/MarketplacePreorderCard";
|
|
7
|
-
export function FeaturedPreOrdersSection({ title = "Reserve Before It Ships", description, viewMoreHref, viewMoreLabel = "View all pre-orders →", className = "", filterByBrand, initialItems, }) {
|
|
7
|
+
export function FeaturedPreOrdersSection({ title = "Reserve Before It Ships", description, viewMoreHref, viewMoreLabel = "View all pre-orders →", className = "", filterByBrand, initialItems, rows = 1, autoScroll = false, scrollInterval = 5000, }) {
|
|
8
8
|
const { data: items = [], isLoading } = useFeaturedPreOrders({ filterByBrand, initialData: initialItems });
|
|
9
|
-
return (_jsx(SectionCarousel, { title: title, description: description, pillLabel: "Pre-Order Incoming", headingVariant: "editorial", viewMoreHref: viewMoreHref, viewMoreLabel: viewMoreLabel, items: items, isLoading: isLoading, skeletonCount: 4, perView: THEME_CONSTANTS.carousel.perView.standard, gap: 16, keyExtractor: (product) => product.id, renderItem: (product) => (_jsx(MarketplacePreorderCard, { product: product })), className: className }));
|
|
9
|
+
return (_jsx(SectionCarousel, { title: title, description: description, pillLabel: "Pre-Order Incoming", headingVariant: "editorial", viewMoreHref: viewMoreHref, viewMoreLabel: viewMoreLabel, items: items, isLoading: isLoading, skeletonCount: 4, perView: THEME_CONSTANTS.carousel.perView.standard, gap: 16, rows: Math.min(Math.max(rows, 1), 4), autoScroll: autoScroll, autoScrollInterval: scrollInterval, keyExtractor: (product) => product.id, renderItem: (product) => (_jsx(MarketplacePreorderCard, { product: product })), className: className }));
|
|
10
10
|
}
|