@mohasinac/appkit 2.7.32 → 2.7.34
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/faqs/index.d.ts +1 -0
- package/dist/_internal/server/features/faqs/index.js +1 -0
- package/dist/_internal/server/features/faqs/og.d.ts +9 -0
- package/dist/_internal/server/features/faqs/og.js +50 -0
- package/dist/_internal/server/features/reviews/index.d.ts +2 -0
- package/dist/_internal/server/features/reviews/index.js +2 -0
- package/dist/_internal/server/features/reviews/og-data.d.ts +1 -0
- package/dist/_internal/server/features/reviews/og-data.js +7 -0
- package/dist/_internal/server/features/reviews/og.d.ts +19 -0
- package/dist/_internal/server/features/reviews/og.js +70 -0
- package/dist/_internal/server/features/scams/index.d.ts +1 -0
- package/dist/_internal/server/features/scams/index.js +1 -0
- package/dist/_internal/server/features/scams/og.d.ts +18 -0
- package/dist/_internal/server/features/scams/og.js +59 -0
- 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/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/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/conversations-seed-data.js +52 -0
- 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/notifications-seed-data.js +10 -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/sessions-seed-data.js +35 -1
- package/dist/seed/support-tickets-seed-data.js +43 -0
- package/dist/seed/users-seed-data.js +1 -1
- package/dist/seed/wishlists-seed-data.js +5 -1
- package/dist/server-entry.d.ts +3 -1
- package/dist/server-entry.js +3 -1
- package/dist/server.d.ts +5 -0
- package/dist/server.js +5 -0
- package/dist/ui/components/Input.style.css +42 -4
- package/package.json +1 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { renderFaqOg, renderFaqOgImage, type FaqOgData } from "./og";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { renderFaqOg, renderFaqOgImage } from "./og";
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { ReactElement } from "react";
|
|
2
|
+
export interface FaqOgData {
|
|
3
|
+
categoryLabel: string;
|
|
4
|
+
siteName: string;
|
|
5
|
+
}
|
|
6
|
+
export declare function renderFaqOg(category: string, opts: {
|
|
7
|
+
siteName: string;
|
|
8
|
+
}): ReactElement;
|
|
9
|
+
export declare function renderFaqOgImage(data: FaqOgData): ReactElement;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
export function renderFaqOg(category, opts) {
|
|
3
|
+
const label = category
|
|
4
|
+
.replace(/-/g, " ")
|
|
5
|
+
.replace(/\b\w/g, (c) => c.toUpperCase());
|
|
6
|
+
return renderFaqOgImage({ categoryLabel: label, siteName: opts.siteName });
|
|
7
|
+
}
|
|
8
|
+
export function renderFaqOgImage(data) {
|
|
9
|
+
const { categoryLabel, siteName } = data;
|
|
10
|
+
return (_jsxs("div", { style: {
|
|
11
|
+
display: "flex",
|
|
12
|
+
width: "100%",
|
|
13
|
+
height: "100%",
|
|
14
|
+
background: "linear-gradient(135deg, #3570fc 0%, #1343de 100%)",
|
|
15
|
+
fontFamily: "sans-serif",
|
|
16
|
+
alignItems: "center",
|
|
17
|
+
justifyContent: "center",
|
|
18
|
+
padding: "80px",
|
|
19
|
+
position: "relative",
|
|
20
|
+
overflow: "hidden",
|
|
21
|
+
}, children: [_jsx("div", { style: {
|
|
22
|
+
position: "absolute",
|
|
23
|
+
inset: 0,
|
|
24
|
+
opacity: 0.06,
|
|
25
|
+
backgroundImage: "radial-gradient(circle at 25% 25%, white 1px, transparent 0), radial-gradient(circle at 75% 75%, white 1px, transparent 0)",
|
|
26
|
+
backgroundSize: "64px 64px",
|
|
27
|
+
} }), _jsxs("div", { style: {
|
|
28
|
+
position: "relative",
|
|
29
|
+
display: "flex",
|
|
30
|
+
flexDirection: "column",
|
|
31
|
+
alignItems: "center",
|
|
32
|
+
gap: 24,
|
|
33
|
+
textAlign: "center",
|
|
34
|
+
}, children: [_jsxs("div", { style: {
|
|
35
|
+
fontSize: 20,
|
|
36
|
+
color: "rgba(255,255,255,0.7)",
|
|
37
|
+
letterSpacing: 3,
|
|
38
|
+
textTransform: "uppercase",
|
|
39
|
+
}, children: [siteName, " \u00B7 Help Centre"] }), _jsx("div", { style: {
|
|
40
|
+
fontSize: 72,
|
|
41
|
+
fontWeight: 800,
|
|
42
|
+
color: "white",
|
|
43
|
+
lineHeight: 1.1,
|
|
44
|
+
letterSpacing: "-1px",
|
|
45
|
+
}, children: categoryLabel }), _jsx("div", { style: {
|
|
46
|
+
fontSize: 32,
|
|
47
|
+
color: "rgba(255,255,255,0.8)",
|
|
48
|
+
fontWeight: 400,
|
|
49
|
+
}, children: "Frequently Asked Questions" })] })] }));
|
|
50
|
+
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
1
|
export { getReviewsForProduct, getReviewsForStore, hasUserPurchasedProduct } from "./data";
|
|
2
|
+
export { getReviewById } from "./og-data";
|
|
3
|
+
export { renderReviewOg, renderReviewOgImage, type ReviewOgData } from "./og";
|
|
2
4
|
export { createReviewAction, replyToReviewAction, deleteReviewAction, markReviewHelpfulAction, } from "./actions";
|
|
3
5
|
export { REVIEWS_PAGE_SIZE, REVIEW_MAX_RATING, REVIEW_MIN_RATING, REVIEW_IMAGES_MAX } from "../../../shared/features/reviews/config";
|
|
@@ -1,3 +1,5 @@
|
|
|
1
1
|
export { getReviewsForProduct, getReviewsForStore, hasUserPurchasedProduct } from "./data";
|
|
2
|
+
export { getReviewById } from "./og-data";
|
|
3
|
+
export { renderReviewOg, renderReviewOgImage } from "./og";
|
|
2
4
|
export { createReviewAction, replyToReviewAction, deleteReviewAction, markReviewHelpfulAction, } from "./actions";
|
|
3
5
|
export { REVIEWS_PAGE_SIZE, REVIEW_MAX_RATING, REVIEW_MIN_RATING, REVIEW_IMAGES_MAX } from "../../../shared/features/reviews/config";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const getReviewById: (id: string) => Promise<import("../../../..").ReviewDocument | null>;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { ReactElement } from "react";
|
|
2
|
+
export interface ReviewOgData {
|
|
3
|
+
productTitle: string;
|
|
4
|
+
rating: number;
|
|
5
|
+
reviewerInitial: string;
|
|
6
|
+
reviewExcerpt?: string | null;
|
|
7
|
+
}
|
|
8
|
+
interface ReviewDocLike {
|
|
9
|
+
title?: string | null;
|
|
10
|
+
productName?: string | null;
|
|
11
|
+
rating?: number | null;
|
|
12
|
+
userName?: string | null;
|
|
13
|
+
body?: string | null;
|
|
14
|
+
}
|
|
15
|
+
export declare function renderReviewOg(doc: ReviewDocLike | null | undefined, opts: {
|
|
16
|
+
siteName: string;
|
|
17
|
+
}): ReactElement;
|
|
18
|
+
export declare function renderReviewOgImage(data: ReviewOgData, siteName: string): ReactElement;
|
|
19
|
+
export {};
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
function stars(n) {
|
|
3
|
+
return "★".repeat(Math.min(5, Math.max(1, Math.round(n))));
|
|
4
|
+
}
|
|
5
|
+
export function renderReviewOg(doc, opts) {
|
|
6
|
+
return renderReviewOgImage({
|
|
7
|
+
productTitle: doc?.productName ?? doc?.title ?? "Collectible",
|
|
8
|
+
rating: doc?.rating ?? 5,
|
|
9
|
+
reviewerInitial: (doc?.userName?.[0] ?? "?").toUpperCase(),
|
|
10
|
+
reviewExcerpt: doc?.body ? doc.body.slice(0, 120) : null,
|
|
11
|
+
}, opts.siteName);
|
|
12
|
+
}
|
|
13
|
+
export function renderReviewOgImage(data, siteName) {
|
|
14
|
+
const { productTitle, rating, reviewerInitial, reviewExcerpt } = data;
|
|
15
|
+
return (_jsxs("div", { style: {
|
|
16
|
+
display: "flex",
|
|
17
|
+
width: "100%",
|
|
18
|
+
height: "100%",
|
|
19
|
+
background: "#0f172a",
|
|
20
|
+
fontFamily: "sans-serif",
|
|
21
|
+
alignItems: "center",
|
|
22
|
+
justifyContent: "center",
|
|
23
|
+
padding: "60px",
|
|
24
|
+
position: "relative",
|
|
25
|
+
}, children: [_jsx("div", { style: {
|
|
26
|
+
position: "absolute",
|
|
27
|
+
inset: 0,
|
|
28
|
+
background: "linear-gradient(135deg, #0f172a 0%, #1e293b 100%)",
|
|
29
|
+
} }), _jsxs("div", { style: {
|
|
30
|
+
position: "relative",
|
|
31
|
+
display: "flex",
|
|
32
|
+
flexDirection: "column",
|
|
33
|
+
width: "100%",
|
|
34
|
+
maxWidth: 1080,
|
|
35
|
+
gap: 32,
|
|
36
|
+
}, children: [_jsxs("div", { style: { fontSize: 18, color: "#94a3b8", letterSpacing: 2, textTransform: "uppercase" }, children: [siteName, " \u00B7 Customer Review"] }), _jsxs("div", { style: {
|
|
37
|
+
display: "flex",
|
|
38
|
+
alignItems: "center",
|
|
39
|
+
gap: 24,
|
|
40
|
+
}, children: [_jsx("div", { style: {
|
|
41
|
+
width: 80,
|
|
42
|
+
height: 80,
|
|
43
|
+
borderRadius: 40,
|
|
44
|
+
background: "linear-gradient(135deg, #3570fc, #e91e8c)",
|
|
45
|
+
display: "flex",
|
|
46
|
+
alignItems: "center",
|
|
47
|
+
justifyContent: "center",
|
|
48
|
+
fontSize: 36,
|
|
49
|
+
fontWeight: 700,
|
|
50
|
+
color: "white",
|
|
51
|
+
flexShrink: 0,
|
|
52
|
+
}, children: reviewerInitial }), _jsxs("div", { style: { display: "flex", flexDirection: "column", gap: 4 }, children: [_jsx("div", { style: { fontSize: 32, color: "#f59e0b", letterSpacing: 2 }, children: stars(rating) }), _jsxs("div", { style: { fontSize: 20, color: "#64748b" }, children: [rating, "/5"] })] })] }), _jsx("div", { style: {
|
|
53
|
+
fontSize: 48,
|
|
54
|
+
fontWeight: 700,
|
|
55
|
+
color: "#f1f5f9",
|
|
56
|
+
lineHeight: 1.2,
|
|
57
|
+
display: "-webkit-box",
|
|
58
|
+
WebkitLineClamp: 2,
|
|
59
|
+
WebkitBoxOrient: "vertical",
|
|
60
|
+
overflow: "hidden",
|
|
61
|
+
}, children: productTitle }), reviewExcerpt && (_jsxs("div", { style: {
|
|
62
|
+
fontSize: 24,
|
|
63
|
+
color: "#94a3b8",
|
|
64
|
+
lineHeight: 1.5,
|
|
65
|
+
display: "-webkit-box",
|
|
66
|
+
WebkitLineClamp: 2,
|
|
67
|
+
WebkitBoxOrient: "vertical",
|
|
68
|
+
overflow: "hidden",
|
|
69
|
+
}, children: ["\u201C", reviewExcerpt, "\u201D"] }))] })] }));
|
|
70
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { ReactElement } from "react";
|
|
2
|
+
export interface ScamOgData {
|
|
3
|
+
displayName: string;
|
|
4
|
+
scamTypeLabel: string;
|
|
5
|
+
reportCount: number;
|
|
6
|
+
}
|
|
7
|
+
interface ScammerDocLike {
|
|
8
|
+
displayNames?: string[];
|
|
9
|
+
scamType?: string;
|
|
10
|
+
incidents?: unknown[];
|
|
11
|
+
totalReports?: number;
|
|
12
|
+
}
|
|
13
|
+
export declare function renderScamOg(doc: ScammerDocLike | null | undefined, opts: {
|
|
14
|
+
siteName: string;
|
|
15
|
+
scamTypeLabel?: string;
|
|
16
|
+
}): ReactElement;
|
|
17
|
+
export declare function renderScamOgImage(data: ScamOgData, siteName: string): ReactElement;
|
|
18
|
+
export {};
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
export function renderScamOg(doc, opts) {
|
|
3
|
+
return renderScamOgImage({
|
|
4
|
+
displayName: doc?.displayNames?.[0] ?? "Unknown",
|
|
5
|
+
scamTypeLabel: opts.scamTypeLabel ?? doc?.scamType ?? "Scammer",
|
|
6
|
+
reportCount: doc?.totalReports ?? 0,
|
|
7
|
+
}, opts.siteName);
|
|
8
|
+
}
|
|
9
|
+
export function renderScamOgImage(data, siteName) {
|
|
10
|
+
const { displayName, scamTypeLabel, reportCount } = data;
|
|
11
|
+
return (_jsxs("div", { style: {
|
|
12
|
+
display: "flex",
|
|
13
|
+
width: "100%",
|
|
14
|
+
height: "100%",
|
|
15
|
+
background: "#0f172a",
|
|
16
|
+
fontFamily: "sans-serif",
|
|
17
|
+
alignItems: "center",
|
|
18
|
+
padding: "60px 80px",
|
|
19
|
+
position: "relative",
|
|
20
|
+
overflow: "hidden",
|
|
21
|
+
}, children: [_jsx("div", { style: {
|
|
22
|
+
position: "absolute",
|
|
23
|
+
inset: 0,
|
|
24
|
+
background: "linear-gradient(135deg, #1e0505 0%, #0f172a 60%)",
|
|
25
|
+
} }), _jsxs("div", { style: {
|
|
26
|
+
position: "relative",
|
|
27
|
+
display: "flex",
|
|
28
|
+
flexDirection: "column",
|
|
29
|
+
gap: 28,
|
|
30
|
+
width: "100%",
|
|
31
|
+
}, children: [_jsxs("div", { style: {
|
|
32
|
+
display: "flex",
|
|
33
|
+
alignItems: "center",
|
|
34
|
+
gap: 12,
|
|
35
|
+
}, children: [_jsx("div", { style: {
|
|
36
|
+
background: "#dc2626",
|
|
37
|
+
color: "white",
|
|
38
|
+
padding: "6px 18px",
|
|
39
|
+
borderRadius: 6,
|
|
40
|
+
fontSize: 16,
|
|
41
|
+
fontWeight: 700,
|
|
42
|
+
letterSpacing: 2,
|
|
43
|
+
textTransform: "uppercase",
|
|
44
|
+
}, children: "\u26A0 Verified Scammer" }), _jsxs("div", { style: { fontSize: 18, color: "#94a3b8", letterSpacing: 1 }, children: [siteName, " \u00B7 Scam Registry"] })] }), _jsx("div", { style: {
|
|
45
|
+
fontSize: 72,
|
|
46
|
+
fontWeight: 800,
|
|
47
|
+
color: "#f1f5f9",
|
|
48
|
+
lineHeight: 1.1,
|
|
49
|
+
letterSpacing: "-1px",
|
|
50
|
+
}, children: displayName }), _jsxs("div", { style: { display: "flex", gap: 24, alignItems: "center" }, children: [_jsx("div", { style: {
|
|
51
|
+
background: "#1e293b",
|
|
52
|
+
border: "1px solid #334155",
|
|
53
|
+
borderRadius: 8,
|
|
54
|
+
padding: "10px 20px",
|
|
55
|
+
fontSize: 22,
|
|
56
|
+
color: "#f87171",
|
|
57
|
+
fontWeight: 600,
|
|
58
|
+
}, children: scamTypeLabel }), reportCount > 0 && (_jsxs("div", { style: { fontSize: 22, color: "#64748b" }, children: [reportCount, " ", reportCount === 1 ? "report" : "reports"] }))] }), _jsx("div", { style: { fontSize: 20, color: "#64748b", marginTop: 8 }, children: "Do not transact with this individual. Verified by the LetItRip community." })] })] }));
|
|
59
|
+
}
|
|
@@ -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",
|