@mohasinac/appkit 2.7.33 → 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.
Files changed (29) hide show
  1. package/dist/_internal/server/jobs/core/auctionSettlement.js +14 -14
  2. package/dist/_internal/server/jobs/core/offerExpiry.js +3 -2
  3. package/dist/_internal/server/jobs/core/onBidPlaced.js +14 -12
  4. package/dist/_internal/server/jobs/core/onOrderStatusChange.js +6 -71
  5. package/dist/_internal/server/jobs/core/onSupportTicketCreate.js +6 -8
  6. package/dist/_internal/server/jobs/core/onSupportTicketUpdate.js +6 -7
  7. package/dist/_internal/server/jobs/core/pendingOrderTimeout.js +11 -10
  8. package/dist/_internal/server/jobs/core/prizeRevealExpiry.js +2 -2
  9. package/dist/_internal/server/jobs/core/prizeRevealOpen.js +2 -2
  10. package/dist/_internal/server/jobs/core/prizeRevealReminder.js +2 -2
  11. package/dist/features/admin/actions/notification-actions.js +18 -8
  12. package/dist/features/admin/schemas/firestore.d.ts +6 -2
  13. package/dist/features/admin/schemas/firestore.js +4 -0
  14. package/dist/features/orders/actions/refund-actions.js +2 -2
  15. package/dist/features/seller/actions/offer-actions.js +5 -5
  16. package/dist/index.d.ts +1 -0
  17. package/dist/index.js +1 -0
  18. package/dist/seed/actions/demo-seed-actions.d.ts +1 -1
  19. package/dist/seed/index.d.ts +1 -0
  20. package/dist/seed/index.js +1 -0
  21. package/dist/seed/manifest.js +5 -0
  22. package/dist/seed/offers-seed-data.d.ts +10 -0
  23. package/dist/seed/offers-seed-data.js +264 -0
  24. package/dist/seed/orders-seed-data.js +116 -2
  25. package/dist/seed/support-tickets-seed-data.js +43 -0
  26. package/dist/server.d.ts +1 -0
  27. package/dist/server.js +1 -0
  28. package/dist/ui/components/Input.style.css +42 -4
  29. package/package.json +1 -1
@@ -1,4 +1,5 @@
1
- import { bidRepository, notificationRepository, orderRepository, productRepository, } from "../../../../repositories";
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
- notificationRepository.createInBatch(batch, {
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).forEach(({ data: bid }) => {
38
- notificationRepository.createInBatch(batch, {
39
- userId: bid.userId,
40
- type: "bid_lost",
41
- priority: "normal",
42
- title: AUCTION_MESSAGES.LOST_TITLE,
43
- message: AUCTION_MESSAGES.LOST_MESSAGE(product.title),
44
- relatedId: product.id,
45
- relatedType: "product",
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, notificationRepository } from "../../../../repositories";
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 notificationRepository.create({
22
+ await sendNotification({
22
23
  userId: offer.buyerUid,
23
24
  type: "offer_expired",
24
25
  priority: "normal",
@@ -1,5 +1,6 @@
1
- import { bidRepository, notificationRepository, productRepository, } from "../../../../repositories";
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
- notificationRepository.createInBatch(batch, {
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: BID_MESSAGES.OUTBID_MESSAGE(newBid.productTitle, newBid.currency, newBid.bidAmount),
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: BID_MESSAGES.OUTBID_MESSAGE(newBid.productTitle, newBid.currency, newBid.bidAmount),
39
+ message: outbidMessage,
38
40
  timestamp: Date.now(),
39
41
  read: false,
40
42
  });
41
43
  }
42
- }
43
- catch (rtdbError) {
44
- ctx.logger.error("Realtime DB push failed (non-fatal)", rtdbError);
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 { ORDER_MESSAGES, EMAIL_SUBJECTS, JOB_ERROR_MESSAGES } from "../handlers/messages";
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 notificationRepository.create({
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
- if (config.sendEmail) {
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 { notificationRepository } from "../../../../repositories";
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 notificationRepository.create({
10
+ await sendNotification({
12
11
  userId,
13
12
  type: "account_action",
13
+ priority: "normal",
14
14
  title: "Support ticket received",
15
- body: `We received your support request: "${ticket.subject ?? "your ticket"}". We'll get back to you soon.`,
16
- isRead: false,
17
- entityId: ticketId,
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 { notificationRepository } from "../../../../repositories";
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 notificationRepository.create({
35
+ await sendNotification({
36
36
  userId,
37
37
  type: "account_action",
38
+ priority: "normal",
38
39
  title: msg.title,
39
- body: msg.body(subject),
40
- isRead: false,
41
- entityId: ticketId,
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 { notificationRepository, orderRepository } from "../../../../repositories";
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 { notificationRepository } from "../../../../repositories";
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 notificationRepository.create({
37
+ await sendNotification({
38
38
  userId: order.userId,
39
39
  type: "prize_reveal_expired",
40
40
  priority: "high",
@@ -1,4 +1,4 @@
1
- import { notificationRepository } from "../../../../repositories";
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 notificationRepository.create({
41
+ await sendNotification({
42
42
  userId: order.userId,
43
43
  type: "prize_reveal_ready",
44
44
  priority: "high",
@@ -1,4 +1,4 @@
1
- import { notificationRepository } from "../../../../repositories";
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 notificationRepository.create({
26
+ await sendNotification({
27
27
  userId: order.userId,
28
28
  type: "prize_reveal_reminder",
29
29
  priority: "normal",
@@ -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
- const userDoc = await userRepository.findById(input.userId);
58
- userChannelPrefs = userDoc?.notificationPreferences?.channels ?? {};
59
- userTypePrefs = userDoc?.notificationPreferences?.types ?? {};
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
- userEmail &&
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: userEmail,
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
- userPhone &&
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: userPhone,
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",
@@ -10,7 +10,7 @@ import { serverLogger } from "../../../monitoring";
10
10
  import { orderRepository } from "../../orders/repository/orders.repository";
11
11
  import { RefundStatusValues } from "../../orders/schemas";
12
12
  import { siteSettingsRepository } from "../../admin/repository/site-settings.repository";
13
- import { notificationRepository } from "../../admin/repository/notification.repository";
13
+ import { sendNotification } from "../../admin/actions/notification-actions";
14
14
  const DEFAULT_PLATFORM_FEE_PERCENT = 5;
15
15
  const DEFAULT_GST_PERCENT = 18;
16
16
  export async function issuePartialRefund(adminUid, orderId, deductFees, refundNote) {
@@ -39,7 +39,7 @@ export async function issuePartialRefund(adminUid, orderId, deductFees, refundNo
39
39
  paymentStatus: "refunded",
40
40
  updatedAt: new Date(),
41
41
  });
42
- await notificationRepository.create({
42
+ await sendNotification({
43
43
  userId: order.userId,
44
44
  type: "refund_initiated",
45
45
  priority: "high",
@@ -8,7 +8,7 @@ import { serverLogger } from "../../../monitoring";
8
8
  import { offerRepository } from "../repository/offer.repository";
9
9
  import { productRepository } from "../../products/repository/products.repository";
10
10
  import { ProductStatusValues } from "../../products/schemas";
11
- import { notificationRepository } from "../../admin/repository/notification.repository";
11
+ import { sendNotification } from "../../admin/actions/notification-actions";
12
12
  import { userRepository } from "../../auth/repository/user.repository";
13
13
  import { storeRepository } from "../../stores/repository/store.repository";
14
14
  import { cartRepository } from "../../cart/repository/cart.repository";
@@ -57,7 +57,7 @@ export async function makeOffer(userId, userEmail, input) {
57
57
  });
58
58
  const sellerStore = product.storeId ? await storeRepository.findById(product.storeId) : null;
59
59
  if (sellerStore?.ownerId) {
60
- await notificationRepository.create({
60
+ await sendNotification({
61
61
  userId: sellerStore.ownerId,
62
62
  type: "offer_received",
63
63
  priority: "normal",
@@ -109,7 +109,7 @@ export async function respondToOffer(userId, input) {
109
109
  : action === "decline"
110
110
  ? `Your offer on "${offer.productTitle}" was declined.`
111
111
  : `${offer.storeName} countered with ₹${counterAmount} on "${offer.productTitle}".`;
112
- await notificationRepository.create({
112
+ await sendNotification({
113
113
  userId: offer.buyerUid,
114
114
  type: "offer_responded",
115
115
  priority: action === "accept" ? "high" : "normal",
@@ -143,7 +143,7 @@ export async function acceptCounterOffer(userId, offerId) {
143
143
  const updated = await offerRepository.acceptCounter(offerId);
144
144
  const counterStore = offer.storeId ? await storeRepository.findById(offer.storeId) : null;
145
145
  if (counterStore?.ownerId)
146
- await notificationRepository.create({
146
+ await sendNotification({
147
147
  userId: counterStore.ownerId,
148
148
  type: "offer_counter_accepted",
149
149
  priority: "high",
@@ -199,7 +199,7 @@ export async function counterOfferByBuyer(userId, userEmail, input) {
199
199
  });
200
200
  const buyerCounterStore = offer.storeId ? await storeRepository.findById(offer.storeId) : null;
201
201
  if (buyerCounterStore?.ownerId)
202
- await notificationRepository.create({
202
+ await sendNotification({
203
203
  userId: buyerCounterStore.ownerId,
204
204
  type: "offer_received",
205
205
  priority: "normal",
package/dist/index.d.ts CHANGED
@@ -532,6 +532,7 @@ export { supportRepository, SupportRepository } from "./repositories/index";
532
532
  export type { SupportTicketDocument, SupportTicketCreateInput, SupportTicketUpdateInput, TicketMessage, TicketCategory, TicketStatus, TicketPriority, } from "./repositories/index";
533
533
  export { ELIGIBLE_ORDER_STATUSES_FOR_TICKET, SUPPORT_TICKET_COLLECTION, ACTIVE_TICKET_STATUSES, SUPPORT_TICKET_FIELDS, TicketCategoryValues, TicketStatusValues, TicketPriorityValues, } from "./features/support/schemas/firestore";
534
534
  export { supportTicketsSeedData } from "./seed/index";
535
+ export { offersSeedData } from "./seed/index";
535
536
  export { productFeaturesRepository } from "./repositories/index";
536
537
  export type { ProductFeatureListFilter } from "./repositories/index";
537
538
  export { loadProductFeaturesForStore } from "./repositories/index";
package/dist/index.js CHANGED
@@ -1088,6 +1088,7 @@ export { supportRepository, SupportRepository } from "./repositories/index";
1088
1088
  export { ELIGIBLE_ORDER_STATUSES_FOR_TICKET, SUPPORT_TICKET_COLLECTION, ACTIVE_TICKET_STATUSES, SUPPORT_TICKET_FIELDS, TicketCategoryValues, TicketStatusValues, TicketPriorityValues, } from "./features/support/schemas/firestore";
1089
1089
  // Support tickets â€" seed data
1090
1090
  export { supportTicketsSeedData } from "./seed/index";
1091
+ export { offersSeedData } from "./seed/index";
1091
1092
  // SB-UNI-B â€" sublistingCategoriesRepository + SublistingCategoryDocument deleted.
1092
1093
  // Use categoriesRepository.findBySlugAndType(slug, "sublisting") and CategoryDocument with categoryType:"sublisting".
1093
1094
  // [DB]-Database layer â€" uses firebase-admin; server-only.
@@ -6,7 +6,7 @@
6
6
  * the actual collection-specific seeding logic (800+ lines with PII encryption,
7
7
  * Auth user creation, subcollection handling, etc.).
8
8
  */
9
- export type SeedCollectionName = "users" | "addresses" | "categories" | "stores" | "products" | "orders" | "reviews" | "bids" | "coupons" | "carousels" | "carouselSlides" | "homepageSections" | "siteSettings" | "faqs" | "notifications" | "payouts" | "blogPosts" | "events" | "eventEntries" | "sessions" | "carts" | "wishlists" | "history" | "conversations" | "groupedListings" | "scammerProfiles" | "supportTickets" | "productFeatures";
9
+ export type SeedCollectionName = "users" | "addresses" | "categories" | "stores" | "products" | "orders" | "reviews" | "bids" | "coupons" | "carousels" | "carouselSlides" | "homepageSections" | "siteSettings" | "faqs" | "notifications" | "payouts" | "blogPosts" | "events" | "eventEntries" | "sessions" | "carts" | "wishlists" | "history" | "conversations" | "groupedListings" | "scammerProfiles" | "supportTickets" | "productFeatures" | "offers";
10
10
  export interface SeedOperationResult {
11
11
  success?: boolean;
12
12
  message: string;
@@ -72,6 +72,7 @@ export { groupedListingsSeedData } from "./grouped-listings-seed-data";
72
72
  export { scammersSeedData } from "./scammers-seed-data";
73
73
  export { supportTicketsSeedData } from "./support-tickets-seed-data";
74
74
  export { productFeaturesSeedData } from "./product-features-seed-data";
75
+ export { offersSeedData } from "./offers-seed-data";
75
76
  export type { SeedManifest, SeedManifestEntry } from "./manifest";
76
77
  export { SEED_MANIFEST } from "./manifest";
77
78
  export type { FirestoreIndexConfig } from "./firestore-indexes";
@@ -61,6 +61,7 @@ export { groupedListingsSeedData } from "./grouped-listings-seed-data";
61
61
  export { scammersSeedData } from "./scammers-seed-data";
62
62
  export { supportTicketsSeedData } from "./support-tickets-seed-data";
63
63
  export { productFeaturesSeedData } from "./product-features-seed-data";
64
+ export { offersSeedData } from "./offers-seed-data";
64
65
  export { SEED_MANIFEST } from "./manifest";
65
66
  export { mergeFirestoreIndices, generateMergedFirestoreIndexFile, } from "./firestore-indexes";
66
67
  // Test utilities (Firestore emulator + PII assertion)
@@ -39,6 +39,7 @@ import { groupedListingsSeedData } from "./grouped-listings-seed-data";
39
39
  import { scammersSeedData } from "./scammers-seed-data";
40
40
  import { supportTicketsSeedData } from "./support-tickets-seed-data";
41
41
  import { productFeaturesSeedData } from "./product-features-seed-data";
42
+ import { offersSeedData } from "./offers-seed-data";
42
43
  function asArr(items) {
43
44
  return items ?? [];
44
45
  }
@@ -167,4 +168,8 @@ export const SEED_MANIFEST = {
167
168
  ...f,
168
169
  name: f.label ?? f.id,
169
170
  }))),
171
+ offers: pick(asArr(offersSeedData).map((o) => ({
172
+ ...o,
173
+ name: o.productTitle ?? o.id,
174
+ }))),
170
175
  };
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Offers Seed Data — 12 offers across all statuses.
3
+ *
4
+ * Covers: pending (×3), accepted (×1), declined (×2), countered (×2),
5
+ * expired (×2), withdrawn (×1), paid (×2).
6
+ * Admin as buyer: 1 pending (Hot Wheels Banana Camaro) + 1 paid (ALTER Rem Wedding).
7
+ * Paid offers link to order-admin-043-alter-rem-offer and order-arjun-044-bx01-offer.
8
+ */
9
+ import type { OfferDocument } from "../features/seller/schemas/firestore";
10
+ export declare const offersSeedData: Partial<OfferDocument>[];
@@ -0,0 +1,264 @@
1
+ /**
2
+ * Offers Seed Data — 12 offers across all statuses.
3
+ *
4
+ * Covers: pending (×3), accepted (×1), declined (×2), countered (×2),
5
+ * expired (×2), withdrawn (×1), paid (×2).
6
+ * Admin as buyer: 1 pending (Hot Wheels Banana Camaro) + 1 paid (ALTER Rem Wedding).
7
+ * Paid offers link to order-admin-043-alter-rem-offer and order-arjun-044-bx01-offer.
8
+ */
9
+ import { OfferStatusValues } from "../features/seller/schemas/firestore";
10
+ const NOW = new Date();
11
+ const daysAgo = (n) => new Date(NOW.getTime() - n * 86400000);
12
+ const daysFromNow = (n) => new Date(NOW.getTime() + n * 86400000);
13
+ export const offersSeedData = [
14
+ // ── 1. PENDING — admin as buyer — Hot Wheels Banana Camaro Redline ─────────
15
+ {
16
+ id: "offer-admin-hw-banana-pending",
17
+ productId: "product-hot-wheels-redline-banana-1968",
18
+ productTitle: "Hot Wheels 1968 Redline Banana Camaro (Custom)",
19
+ storeId: "store-diecast-depot",
20
+ storeName: "Diecast Depot",
21
+ buyerUid: "user-admin-letitrip",
22
+ buyerName: "LetItRip Admin",
23
+ buyerEmail: "admin@letitrip.in",
24
+ listedPrice: 1299900,
25
+ offerAmount: 999900,
26
+ currency: "INR",
27
+ status: OfferStatusValues.PENDING,
28
+ buyerNote: "Interested in this piece. Would you accept Rs 9,999?",
29
+ expiresAt: daysFromNow(3),
30
+ createdAt: daysAgo(1),
31
+ updatedAt: daysAgo(1),
32
+ },
33
+ // ── 2. PAID — admin as buyer — ALTER Rem Wedding (order placed) ───────────
34
+ {
35
+ id: "offer-admin-alter-rem-paid",
36
+ productId: "product-alter-rem-wedding-scale",
37
+ productTitle: "ALTER: Re:ZERO — Rem (Wedding Ver.) 1/7 Scale Figure",
38
+ storeId: "store-tokyo-toys-india",
39
+ storeName: "Tokyo Toys India",
40
+ buyerUid: "user-admin-letitrip",
41
+ buyerName: "LetItRip Admin",
42
+ buyerEmail: "admin@letitrip.in",
43
+ listedPrice: 1299900,
44
+ offerAmount: 1099900,
45
+ lockedPrice: 1099900,
46
+ currency: "INR",
47
+ status: OfferStatusValues.PAID,
48
+ buyerNote: "Would you accept Rs 10,999 for this?",
49
+ sellerNote: "Accepted. Thank you!",
50
+ expiresAt: daysAgo(3),
51
+ acceptedAt: daysAgo(12),
52
+ respondedAt: daysAgo(12),
53
+ createdAt: daysAgo(14),
54
+ updatedAt: daysAgo(11),
55
+ },
56
+ // ── 3. PENDING — Rahul — Charizard ex SIR from pokemon-palace ────────────
57
+ {
58
+ id: "offer-rahul-charizard-sar-pending",
59
+ productId: "product-pokemon-charizard-ex-sar-sv",
60
+ productTitle: "Pokemon TCG: Charizard ex Special Illustration Rare (SV3)",
61
+ storeId: "store-pokemon-palace",
62
+ storeName: "Pokemon Palace",
63
+ buyerUid: "user-rahul-sharma",
64
+ buyerName: "Rahul Sharma",
65
+ buyerEmail: "rahul.sharma@gmail.com",
66
+ listedPrice: 349900,
67
+ offerAmount: 280000,
68
+ currency: "INR",
69
+ status: OfferStatusValues.PENDING,
70
+ buyerNote: "Can you do Rs 2,800 for this card?",
71
+ expiresAt: daysFromNow(5),
72
+ createdAt: daysAgo(0),
73
+ updatedAt: daysAgo(0),
74
+ },
75
+ // ── 4. DECLINED — Priya — MAFEX Miles Morales from letitrip-official ───────
76
+ {
77
+ id: "offer-priya-mafex-miles-declined",
78
+ productId: "product-mafex-miles-morales-spiderman",
79
+ productTitle: "MAFEX No.240: Miles Morales Spider-Man",
80
+ storeId: "store-letitrip-official",
81
+ storeName: "LetItRip Official",
82
+ buyerUid: "user-priya-patel",
83
+ buyerName: "Priya Patel",
84
+ buyerEmail: "priya.patel@gmail.com",
85
+ listedPrice: 849900,
86
+ offerAmount: 650000,
87
+ currency: "INR",
88
+ status: OfferStatusValues.DECLINED,
89
+ buyerNote: "Offering Rs 6,500 — slightly above my budget.",
90
+ sellerNote: "Sorry, we cannot go below Rs 7,999 for this item.",
91
+ expiresAt: daysAgo(5),
92
+ respondedAt: daysAgo(8),
93
+ createdAt: daysAgo(10),
94
+ updatedAt: daysAgo(8),
95
+ },
96
+ // ── 5. COUNTERED — Arjun — SHF Goku Ultra Instinct from letitrip-official ──
97
+ {
98
+ id: "offer-arjun-shf-goku-countered",
99
+ productId: "product-shf-goku-ultra-instinct",
100
+ productTitle: "S.H.Figuarts Goku Ultra Instinct -Sign-",
101
+ storeId: "store-letitrip-official",
102
+ storeName: "LetItRip Official",
103
+ buyerUid: "user-arjun-singh",
104
+ buyerName: "Arjun Singh",
105
+ buyerEmail: "arjun.singh@gmail.com",
106
+ listedPrice: 699900,
107
+ offerAmount: 549900,
108
+ counterAmount: 649900,
109
+ currency: "INR",
110
+ status: OfferStatusValues.COUNTERED,
111
+ buyerNote: "Rs 5,499 — will you take it?",
112
+ sellerNote: "We can do Rs 6,499 as our best price.",
113
+ expiresAt: daysFromNow(2),
114
+ respondedAt: daysAgo(1),
115
+ createdAt: daysAgo(3),
116
+ updatedAt: daysAgo(1),
117
+ },
118
+ // ── 6. EXPIRED — Neha — Blue-Eyes LOB from cardgame-hub ──────────────────
119
+ {
120
+ id: "offer-neha-blue-eyes-expired",
121
+ productId: "product-yugioh-blue-eyes-lob-nm",
122
+ productTitle: "Yu-Gi-Oh! Blue-Eyes White Dragon LOB 1st Edition (NM)",
123
+ storeId: "store-cardgame-hub",
124
+ storeName: "CardGame Hub",
125
+ buyerUid: "user-neha-gupta",
126
+ buyerName: "Neha Gupta",
127
+ buyerEmail: "neha.gupta@gmail.com",
128
+ listedPrice: 299900,
129
+ offerAmount: 220000,
130
+ currency: "INR",
131
+ status: OfferStatusValues.EXPIRED,
132
+ buyerNote: "I can offer Rs 2,200. Let me know.",
133
+ expiresAt: daysAgo(2),
134
+ createdAt: daysAgo(9),
135
+ updatedAt: daysAgo(2),
136
+ },
137
+ // ── 7. WITHDRAWN — Vikram — Twin Mill 1970 Redline from vintage-vault ──────
138
+ {
139
+ id: "offer-vikram-twin-mill-withdrawn",
140
+ productId: "product-hot-wheels-twin-mill-1970-redline",
141
+ productTitle: "Hot Wheels Vintage: Twin Mill I 1970 Redline (Brown, Near Mint)",
142
+ storeId: "store-vintage-vault",
143
+ storeName: "Vintage Vault",
144
+ buyerUid: "user-vikram-rao",
145
+ buyerName: "Vikram Rao",
146
+ buyerEmail: "vikram.rao@gmail.com",
147
+ listedPrice: 699900,
148
+ offerAmount: 549900,
149
+ currency: "INR",
150
+ status: OfferStatusValues.WITHDRAWN,
151
+ buyerNote: "Offering Rs 5,499 for the Twin Mill.",
152
+ expiresAt: daysAgo(3),
153
+ createdAt: daysAgo(7),
154
+ updatedAt: daysAgo(5),
155
+ },
156
+ // ── 8. PAID — Arjun — BX-01 Dran Sword from beyblade-arena (offer order) ───
157
+ {
158
+ id: "offer-arjun-bx01-paid",
159
+ productId: "product-beyblade-x-bx01-dran-sword-starter-pack",
160
+ productTitle: "Beyblade X: BX-01 Dran Sword 3-60F Starter Pack",
161
+ storeId: "store-beyblade-arena",
162
+ storeName: "Beyblade Arena",
163
+ buyerUid: "user-arjun-singh",
164
+ buyerName: "Arjun Singh",
165
+ buyerEmail: "arjun.singh@gmail.com",
166
+ listedPrice: 159900,
167
+ offerAmount: 129900,
168
+ lockedPrice: 129900,
169
+ currency: "INR",
170
+ status: OfferStatusValues.PAID,
171
+ buyerNote: "Rs 1,299 for the BX-01 starter — deal?",
172
+ sellerNote: "Sure, accepted!",
173
+ expiresAt: daysAgo(10),
174
+ acceptedAt: daysAgo(20),
175
+ respondedAt: daysAgo(20),
176
+ createdAt: daysAgo(22),
177
+ updatedAt: daysAgo(19),
178
+ },
179
+ // ── 9. ACCEPTED — Pooja — GSC Aqua 1/7 from tokyo-toys (pending payment) ───
180
+ {
181
+ id: "offer-pooja-gsc-aqua-accepted",
182
+ productId: "product-gsc-aqua-konosuba-scale",
183
+ productTitle: "Good Smile Company: KonoSuba — Aqua 1/7 Scale Figure",
184
+ storeId: "store-tokyo-toys-india",
185
+ storeName: "Tokyo Toys India",
186
+ buyerUid: "user-pooja-sharma",
187
+ buyerName: "Pooja Sharma",
188
+ buyerEmail: "pooja.sharma@gmail.com",
189
+ listedPrice: 899900,
190
+ offerAmount: 749900,
191
+ lockedPrice: 749900,
192
+ currency: "INR",
193
+ status: OfferStatusValues.ACCEPTED,
194
+ buyerNote: "Rs 7,499 final offer on the Aqua figure.",
195
+ sellerNote: "Accepted! Please complete payment within 24 hours.",
196
+ expiresAt: daysFromNow(1),
197
+ acceptedAt: daysAgo(0),
198
+ respondedAt: daysAgo(0),
199
+ createdAt: daysAgo(1),
200
+ updatedAt: daysAgo(0),
201
+ },
202
+ // ── 10. DECLINED — Meera — PG Unleashed RX-78-2 from gundam-galaxy ─────────
203
+ {
204
+ id: "offer-meera-pg-rx78-declined",
205
+ productId: "product-pg-unleashed-rx78-2",
206
+ productTitle: "Bandai PG Unleashed 1/60: RX-78-2 Gundam",
207
+ storeId: "store-gundam-galaxy",
208
+ storeName: "Gundam Galaxy",
209
+ buyerUid: "user-meera-nair",
210
+ buyerName: "Meera Nair",
211
+ buyerEmail: "meera.nair@gmail.com",
212
+ listedPrice: 2499900,
213
+ offerAmount: 1999900,
214
+ currency: "INR",
215
+ status: OfferStatusValues.DECLINED,
216
+ buyerNote: "Rs 19,999 — this is a big spend, would you consider it?",
217
+ sellerNote: "Unfortunately we cannot discount below Rs 22,999 for this kit.",
218
+ expiresAt: daysAgo(1),
219
+ respondedAt: daysAgo(3),
220
+ createdAt: daysAgo(6),
221
+ updatedAt: daysAgo(3),
222
+ },
223
+ // ── 11. COUNTERED — Rohit — OP-05 Booster Box from cardgame-hub ─────────────
224
+ {
225
+ id: "offer-rohit-op05-countered",
226
+ productId: "product-onepiece-op05-booster-box",
227
+ productTitle: "One Piece Card Game: OP-05 Awakening of the New Era Booster Box",
228
+ storeId: "store-cardgame-hub",
229
+ storeName: "CardGame Hub",
230
+ buyerUid: "user-rohit-verma",
231
+ buyerName: "Rohit Verma",
232
+ buyerEmail: "rohit.verma@gmail.com",
233
+ listedPrice: 499900,
234
+ offerAmount: 399900,
235
+ counterAmount: 449900,
236
+ currency: "INR",
237
+ status: OfferStatusValues.COUNTERED,
238
+ buyerNote: "Can you do Rs 3,999 for the OP-05 box?",
239
+ sellerNote: "Best we can do is Rs 4,499 — final price.",
240
+ expiresAt: daysFromNow(1),
241
+ respondedAt: daysAgo(1),
242
+ createdAt: daysAgo(2),
243
+ updatedAt: daysAgo(1),
244
+ },
245
+ // ── 12. EXPIRED — Kavya — Nendoroid Rem from letitrip-official ───────────
246
+ {
247
+ id: "offer-kavya-nendo-rem-expired",
248
+ productId: "product-nendoroid-rem-rezero",
249
+ productTitle: "Nendoroid: Re:ZERO — Rem #663",
250
+ storeId: "store-letitrip-official",
251
+ storeName: "LetItRip Official",
252
+ buyerUid: "user-kavya-nair",
253
+ buyerName: "Kavya Nair",
254
+ buyerEmail: "kavya.nair@gmail.com",
255
+ listedPrice: 399900,
256
+ offerAmount: 329900,
257
+ currency: "INR",
258
+ status: OfferStatusValues.EXPIRED,
259
+ buyerNote: "Rs 3,299 for the Nendoroid Rem — is this okay?",
260
+ expiresAt: daysAgo(3),
261
+ createdAt: daysAgo(10),
262
+ updatedAt: daysAgo(3),
263
+ },
264
+ ];
@@ -1405,9 +1405,123 @@ export const ordersSeedData = [
1405
1405
  createdAt: daysAgo(3),
1406
1406
  updatedAt: daysAgo(3),
1407
1407
  },
1408
- // ── Admin as buyer 42. PROCESSINGadmin / Good Smile Aqua Scale ────────
1408
+ // ── 43. DELIVEREDadmin as buyer ALTER Rem Wedding via accepted offer ────
1409
1409
  {
1410
- id: "order-admin-039-gsc-aqua",
1410
+ id: "order-admin-043-alter-rem-offer",
1411
+ productId: "product-alter-rem-wedding-scale",
1412
+ productTitle: "ALTER: Re:ZERO — Rem (Wedding Ver.) 1/7 Scale Figure",
1413
+ userId: "user-admin-letitrip",
1414
+ userName: "LetItRip Admin",
1415
+ userEmail: "admin@letitrip.in",
1416
+ storeId: "store-tokyo-toys-india",
1417
+ storeName: "Tokyo Toys India",
1418
+ offerId: "offer-admin-alter-rem-paid",
1419
+ items: [
1420
+ {
1421
+ productId: "product-alter-rem-wedding-scale",
1422
+ productTitle: "ALTER Rem Wedding Ver. 1/7 (offer price)",
1423
+ quantity: 1,
1424
+ unitPrice: 1099900,
1425
+ totalPrice: 1099900,
1426
+ },
1427
+ ],
1428
+ quantity: 1,
1429
+ unitPrice: 1099900,
1430
+ totalPrice: 1099900,
1431
+ currency: "INR",
1432
+ status: OrderStatusValues.DELIVERED,
1433
+ paymentStatus: PaymentStatusValues.PAID,
1434
+ paymentMethod: PaymentMethodValues.RAZORPAY,
1435
+ paymentId: "pay_razorpay_043_demo",
1436
+ shippingAddress: "LetItRip HQ, 10th Floor, BKC G Block, Mumbai 400051",
1437
+ trackingNumber: "DTDC443322110",
1438
+ shippingCarrier: "DTDC",
1439
+ shippingFee: 0,
1440
+ platformFee: 54995,
1441
+ orderDate: daysAgo(11),
1442
+ shippingDate: daysAgo(9),
1443
+ deliveryDate: daysAgo(7),
1444
+ payoutStatus: "paid",
1445
+ createdAt: daysAgo(11),
1446
+ updatedAt: daysAgo(7),
1447
+ },
1448
+ // ── 44. DELIVERED — Arjun / BX-01 via accepted offer ─────────────────────
1449
+ {
1450
+ id: "order-arjun-044-bx01-offer",
1451
+ productId: "product-beyblade-x-bx01-dran-sword-starter-pack",
1452
+ productTitle: "Beyblade X: BX-01 Dran Sword 3-60F — Starter Pack",
1453
+ userId: "user-arjun-singh",
1454
+ userName: "Arjun Singh",
1455
+ userEmail: "arjun.singh@gmail.com",
1456
+ storeId: "store-beyblade-arena",
1457
+ storeName: "Beyblade Arena",
1458
+ offerId: "offer-arjun-bx01-paid",
1459
+ items: [
1460
+ {
1461
+ productId: "product-beyblade-x-bx01-dran-sword-starter-pack",
1462
+ productTitle: "BX-01 Dran Sword Starter Pack (offer price)",
1463
+ quantity: 1,
1464
+ unitPrice: 129900,
1465
+ totalPrice: 129900,
1466
+ },
1467
+ ],
1468
+ quantity: 1,
1469
+ unitPrice: 129900,
1470
+ totalPrice: 129900,
1471
+ currency: "INR",
1472
+ status: OrderStatusValues.DELIVERED,
1473
+ paymentStatus: PaymentStatusValues.PAID,
1474
+ paymentMethod: PaymentMethodValues.UPI_MANUAL,
1475
+ shippingAddress: "78 Indiranagar 100 Ft Road, Bengaluru 560038",
1476
+ trackingNumber: "SHPR221100334",
1477
+ shippingCarrier: "Shiprocket",
1478
+ shippingFee: 0,
1479
+ platformFee: 6495,
1480
+ orderDate: daysAgo(19),
1481
+ shippingDate: daysAgo(17),
1482
+ deliveryDate: daysAgo(14),
1483
+ payoutStatus: "paid",
1484
+ createdAt: daysAgo(19),
1485
+ updatedAt: daysAgo(14),
1486
+ },
1487
+ // ── 45. DELIVERED — Kiran / Gundam Bundle (3 kits) ───────────────────────
1488
+ {
1489
+ id: "order-kiran-045-gundam-bundle",
1490
+ productId: "group-gundam-builder-essentials",
1491
+ productTitle: "Gundam Builder Essentials — MG + RG Bundle",
1492
+ userId: "user-kiran-reddy",
1493
+ userName: "Kiran Reddy",
1494
+ userEmail: "kiran.reddy@gmail.com",
1495
+ storeId: "store-gundam-galaxy",
1496
+ storeName: "Gundam Galaxy",
1497
+ items: [
1498
+ { productId: "product-gundam-rx78-mg", productTitle: "MG RX-78-2 Gundam Ver. 3.0 1/100", quantity: 1, unitPrice: 399900, totalPrice: 399900 },
1499
+ { productId: "product-gundam-wing-zero-rg", productTitle: "RG Wing Gundam Zero EW 1/144", quantity: 1, unitPrice: 249900, totalPrice: 249900 },
1500
+ { productId: "product-hg-aerial-rebuild-gundam", productTitle: "HG Aerial Rebuild 1/144", quantity: 1, unitPrice: 249900, totalPrice: 249900 },
1501
+ ],
1502
+ quantity: 3,
1503
+ unitPrice: 299900,
1504
+ totalPrice: 899700,
1505
+ currency: "INR",
1506
+ status: OrderStatusValues.DELIVERED,
1507
+ paymentStatus: PaymentStatusValues.PAID,
1508
+ paymentMethod: PaymentMethodValues.RAZORPAY,
1509
+ paymentId: "pay_razorpay_045_demo",
1510
+ shippingAddress: "18 Kondapur, Hyderabad, Telangana 500084",
1511
+ trackingNumber: "BLUE667788901",
1512
+ shippingCarrier: "Bluedart",
1513
+ shippingFee: 0,
1514
+ platformFee: 44985,
1515
+ orderDate: daysAgo(52),
1516
+ shippingDate: daysAgo(50),
1517
+ deliveryDate: daysAgo(47),
1518
+ payoutStatus: "paid",
1519
+ createdAt: daysAgo(52),
1520
+ updatedAt: daysAgo(47),
1521
+ },
1522
+ // ── Admin as buyer — 46. PROCESSING — admin / Good Smile Aqua Scale ────────
1523
+ {
1524
+ id: "order-admin-046-gsc-aqua",
1411
1525
  productId: "product-gsc-aqua-konosuba-scale",
1412
1526
  productTitle: "Good Smile Company: KonoSuba — Aqua 1/7 Scale Figure",
1413
1527
  userId: "user-admin-letitrip",
@@ -133,4 +133,47 @@ export const supportTicketsSeedData = [
133
133
  createdAt: daysBack(1),
134
134
  updatedAt: daysBack(1),
135
135
  },
136
+ // ── 7. Resolved — escalated fraud report (admin handled) ──────────────────
137
+ {
138
+ id: "ticket-meera-fraud-002",
139
+ userId: "user-meera-nair",
140
+ userEmail: "meera.nair@example.com",
141
+ userDisplayName: "Meera Nair",
142
+ category: "scam_report",
143
+ subject: "Seller sent empty box and is now unreachable",
144
+ description: "I paid Rs 8,499 for a S.H.Figuarts Broly via Razorpay. The seller (store-beyblade-arena) has not responded in 7 days and the tracking shows the box was 200g — way too light for the figure.",
145
+ status: SUPPORT_TICKET_FIELDS.STATUS_VALUES.RESOLVED,
146
+ priority: SUPPORT_TICKET_FIELDS.PRIORITY_VALUES.HIGH,
147
+ assignedTo: "user-admin-letitrip",
148
+ assignedToName: "LetItRip Admin",
149
+ messages: [
150
+ msg("msg-meera-002-u1", "user-meera-nair", "user", "I have the weight receipt from Delhivery. 200g is impossible for this figure. The seller is not replying on messages or phone.", 10),
151
+ msg("msg-meera-002-s1", "user-simran-kaur", "support", "Hi Meera, I have escalated this to our admin team for review as it qualifies as a potential fraud case. You will hear back within 24 hours.", 9),
152
+ msg("msg-meera-002-a1", "user-admin-letitrip", "support", "Hi Meera, this is the LetItRip admin. I have reviewed the shipment weight log and the seller communication history. We are initiating a full refund of Rs 8,499 under our buyer protection policy. The seller account has been suspended pending investigation.", 8),
153
+ msg("msg-meera-002-u2", "user-meera-nair", "user", "Thank you so much. I really appreciate the quick escalation.", 7),
154
+ ],
155
+ resolvedAt: daysBack(7),
156
+ createdAt: daysBack(10),
157
+ updatedAt: daysBack(7),
158
+ },
159
+ // ── 8. In-progress — seller account ban appeal (admin handling) ────────────
160
+ {
161
+ id: "ticket-rohit-ban-appeal-001",
162
+ userId: "user-rohit-joshi",
163
+ userEmail: "rohit.joshi@example.com",
164
+ userDisplayName: "Rohit Joshi",
165
+ category: "account",
166
+ subject: "My store was suspended — I believe it was a mistake",
167
+ description: "My store Beyblade Arena was suspended 2 days ago. I received no email explanation. I have 100% positive reviews and have never violated any policy. Please review.",
168
+ status: SUPPORT_TICKET_FIELDS.STATUS_VALUES.IN_PROGRESS,
169
+ priority: SUPPORT_TICKET_FIELDS.PRIORITY_VALUES.HIGH,
170
+ assignedTo: "user-admin-letitrip",
171
+ assignedToName: "LetItRip Admin",
172
+ messages: [
173
+ msg("msg-rohit-001-u1", "user-rohit-joshi", "user", "I run a legitimate business. I have 47 verified reviews and never had a single complaint. Please explain the suspension.", 2),
174
+ msg("msg-rohit-001-a1", "user-admin-letitrip", "support", "Hi Rohit, your store was suspended as part of an automatic fraud flag triggered by a buyer report. I am manually reviewing your transaction history and the specific report. Please share your business GST number and a copy of your most recent bank statement showing the Razorpay settlements for verification.", 1),
175
+ ],
176
+ createdAt: daysBack(2),
177
+ updatedAt: daysBack(1),
178
+ },
136
179
  ];
package/dist/server.d.ts CHANGED
@@ -48,6 +48,7 @@ export { makeFullStore } from "./seed/index";
48
48
  export { makeFullUser } from "./seed/index";
49
49
  export { makeHomepageSection } from "./seed/index";
50
50
  export { makeNotification } from "./seed/index";
51
+ export { sendNotification } from "./features/admin/actions/notification-actions";
51
52
  export { makeOrder } from "./seed/index";
52
53
  export { makePayout } from "./seed/index";
53
54
  export { makeProduct } from "./seed/index";
package/dist/server.js CHANGED
@@ -149,6 +149,7 @@ export { makeHomepageSection } from "./seed/index";
149
149
  // [SERVER-ONLY]-Server-only — uses Node.js, Next.js server internals, or third-party server SDKs (auth, email, payment, shipping).
150
150
  // makeNotification - Shared export for make notification.
151
151
  export { makeNotification } from "./seed/index";
152
+ export { sendNotification } from "./features/admin/actions/notification-actions";
152
153
  // [SERVER-ONLY]-Server-only — uses Node.js, Next.js server internals, or third-party server SDKs (auth, email, payment, shipping).
153
154
  // makeOrder - Shared export for make order.
154
155
  export { makeOrder } from "./seed/index";
@@ -7,22 +7,60 @@
7
7
  padding: 0.375rem 0.625rem;
8
8
  font-size: 0.875rem;
9
9
  line-height: 1.4;
10
- transition: border-color 0.2s ease, box-shadow 0.2s ease;
10
+ transition: border-color 0.15s ease, box-shadow 0.15s ease, background 0.15s ease;
11
+ }
12
+
13
+ .appkit-input::placeholder {
14
+ color: var(--appkit-color-text-faint);
15
+ }
16
+
17
+ .appkit-input:hover:not(:disabled):not(:focus-visible) {
18
+ border-color: var(--appkit-color-zinc-400);
11
19
  }
12
20
 
13
21
  .appkit-input:focus-visible {
14
22
  outline: none;
15
23
  border-color: var(--appkit-color-secondary);
16
- box-shadow: 0 0 0 2px rgba(101, 196, 8, 0.2);
24
+ box-shadow: 0 0 0 3px rgba(101, 196, 8, 0.25), inset 0 0 0 1px rgba(101, 196, 8, 0.15);
17
25
  }
18
26
 
19
27
  .dark .appkit-input {
20
28
  border-color: var(--appkit-color-border);
21
- background: rgba(30, 41, 59, 0.6);
29
+ background: rgba(30, 41, 59, 0.7);
22
30
  color: var(--appkit-color-text);
23
31
  }
24
32
 
33
+ .dark .appkit-input::placeholder {
34
+ color: var(--appkit-color-text-muted);
35
+ }
36
+
37
+ .dark .appkit-input:hover:not(:disabled):not(:focus-visible) {
38
+ border-color: var(--appkit-color-slate-500);
39
+ }
40
+
25
41
  .dark .appkit-input:focus-visible {
26
42
  border-color: var(--appkit-color-primary);
27
- box-shadow: 0 0 0 2px rgba(233, 30, 140, 0.2);
43
+ box-shadow: 0 0 0 3px rgba(233, 30, 140, 0.25), inset 0 0 0 1px rgba(233, 30, 140, 0.15);
44
+ }
45
+
46
+ /* Error variant */
47
+ .appkit-input--error {
48
+ border-color: var(--appkit-color-error);
49
+ background: rgba(254, 202, 202, 0.06);
50
+ }
51
+
52
+ .appkit-input--error:focus-visible {
53
+ border-color: var(--appkit-color-error);
54
+ box-shadow: 0 0 0 3px rgba(248, 113, 113, 0.25), inset 0 0 0 1px rgba(248, 113, 113, 0.15);
55
+ }
56
+
57
+ /* Disabled state */
58
+ .appkit-input:disabled {
59
+ opacity: 0.55;
60
+ cursor: not-allowed;
61
+ background: var(--appkit-color-bg);
62
+ }
63
+
64
+ .dark .appkit-input:disabled {
65
+ background: rgba(30, 41, 59, 0.4);
28
66
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mohasinac/appkit",
3
- "version": "2.7.33",
3
+ "version": "2.7.34",
4
4
  "license": "MIT",
5
5
  "publishConfig": {
6
6
  "access": "public"