@mohasinac/appkit 2.7.44 → 2.7.45

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.
@@ -33,7 +33,7 @@ import { cartIsDigitalOnly } from "../../../shared/listing-types/cart-shipping";
33
33
  * verifies in a micro-transaction. Fire-and-forget safe (logs on exhaustion
34
34
  * but never fails the already-created order).
35
35
  */
36
- async function claimDigitalCodeForOrder(db, productId, orderId, userId) {
36
+ async function claimDigitalCodeForOrder(db, productId, orderId, userId, opts) {
37
37
  const codesRef = db
38
38
  .collection(PRODUCT_COLLECTION)
39
39
  .doc(productId)
@@ -44,6 +44,7 @@ async function claimDigitalCodeForOrder(db, productId, orderId, userId) {
44
44
  return;
45
45
  }
46
46
  const codeRef = snap.docs[0].ref;
47
+ let claimed = false;
47
48
  await db.runTransaction(async (tx) => {
48
49
  const latest = await tx.get(codeRef);
49
50
  if (!latest.exists || latest.data()?.status !== "available")
@@ -55,7 +56,17 @@ async function claimDigitalCodeForOrder(db, productId, orderId, userId) {
55
56
  claimedAt: new Date(),
56
57
  updatedAt: new Date(),
57
58
  });
59
+ claimed = true;
58
60
  });
61
+ if (claimed && opts?.userEmail) {
62
+ const { sendDigitalCodeClaimedEmail } = await import("../../../../features/contact/server");
63
+ sendDigitalCodeClaimedEmail({
64
+ to: opts.userEmail,
65
+ userName: opts.userName ?? "there",
66
+ productTitle: opts.productTitle ?? "your product",
67
+ orderId,
68
+ }).catch((e) => serverLogger.error("claimDigitalCode: email failed", e));
69
+ }
59
70
  }
60
71
  /**
61
72
  * SB-UNI-O — Live-item jurisdiction guard.
@@ -503,7 +514,11 @@ export async function createCheckoutOrderAction(input) {
503
514
  // SB-UNI-N — claim a digital code for digital-code orders (fire-and-forget,
504
515
  // order is already persisted so a claim failure is logged, not thrown).
505
516
  if ((group[0]?.product?.listingType ?? "standard") === "digital-code") {
506
- claimDigitalCodeForOrder(getAdminDb(), firstItem.productId, order.id, uid).catch((e) => serverLogger.error("claimDigitalCode", e));
517
+ claimDigitalCodeForOrder(getAdminDb(), firstItem.productId, order.id, uid, {
518
+ userEmail: userEmail || undefined,
519
+ userName,
520
+ productTitle: firstItem.productTitle,
521
+ }).catch((e) => serverLogger.error("claimDigitalCode", e));
507
522
  }
508
523
  for (const code of groupCouponCodes) {
509
524
  const entry = couponUsageAccumulator.get(code);
@@ -878,7 +893,11 @@ export async function verifyAndPlaceRazorpayOrderAction(input) {
878
893
  // SB-UNI-N — claim a digital code for digital-code orders (fire-and-forget,
879
894
  // order is already persisted so a claim failure is logged, not thrown).
880
895
  if ((group[0]?.product?.listingType ?? "standard") === "digital-code") {
881
- claimDigitalCodeForOrder(getAdminDb(), firstItem.productId, order.id, uid).catch((e) => serverLogger.error("claimDigitalCode", e));
896
+ claimDigitalCodeForOrder(getAdminDb(), firstItem.productId, order.id, uid, {
897
+ userEmail: userEmail || undefined,
898
+ userName,
899
+ productTitle: firstItem.productTitle,
900
+ }).catch((e) => serverLogger.error("claimDigitalCode", e));
882
901
  }
883
902
  if (userEmail) {
884
903
  emailsToSend.push({
@@ -63,6 +63,13 @@ export async function setProductStatusAction(input) {
63
63
  throw new ValidationError(parsed.error.issues[0]?.message ?? "Invalid input");
64
64
  const product = await assertProductOwnership(parsed.data.productId, user.uid);
65
65
  assertStatusTransition(product.status, parsed.data.status);
66
+ // SB-UNI-O — live listings must be admin-verified before a seller can publish them.
67
+ if (parsed.data.status === "published" &&
68
+ (product.listingType ?? "standard") === "live" &&
69
+ !product.liveItem?.vendorVerified &&
70
+ user.role !== "admin") {
71
+ throw new ValidationError("Live listings require admin verification before publishing. Contact support to request verification.");
72
+ }
66
73
  return productRepository.update(parsed.data.productId, { status: parsed.data.status });
67
74
  }
68
75
  export async function setProductFeaturedAction(input) {
@@ -17,7 +17,10 @@ import { randomUUID } from "crypto";
17
17
  import { getProviders } from "../../../../contracts/registry";
18
18
  import { orderRepository } from "../../../..";
19
19
  import { NotFoundError, ValidationError } from "../../../../errors";
20
+ import { serverLogger } from "../../../../monitoring";
20
21
  import { applyRefundDeductionAction } from "../payouts/actions";
22
+ import { getAdminDb } from "../../../../providers/db-firebase";
23
+ import { PRODUCT_COLLECTION, PRODUCT_CODES_SUBCOLLECTION, } from "../../../../features/products/schemas/firestore";
21
24
  export async function processRefundAction(input) {
22
25
  const order = await orderRepository.findById(input.orderId);
23
26
  if (!order)
@@ -31,6 +34,26 @@ export async function processRefundAction(input) {
31
34
  if (order.isNonRefundable) {
32
35
  throw new ValidationError("This order is marked non-refundable and cannot be refunded.");
33
36
  }
37
+ // SB-UNI-N — digital-code refund gate: block if code is already claimed (revealed).
38
+ const digitalCodeItems = (order.items ?? []).filter((i) => (i.listingType ?? "standard") === "digital-code");
39
+ if (digitalCodeItems.length > 0) {
40
+ const db = getAdminDb();
41
+ for (const item of digitalCodeItems) {
42
+ const codesSnap = await db
43
+ .collection(PRODUCT_COLLECTION)
44
+ .doc(item.productId)
45
+ .collection(PRODUCT_CODES_SUBCOLLECTION)
46
+ .where("orderId", "==", input.orderId)
47
+ .limit(1)
48
+ .get();
49
+ if (!codesSnap.empty) {
50
+ const codeStatus = codesSnap.docs[0].data()?.status;
51
+ if (codeStatus === "claimed") {
52
+ throw new ValidationError("The digital code for this order has already been revealed and cannot be refunded automatically. Please contact support.");
53
+ }
54
+ }
55
+ }
56
+ }
34
57
  const refundId = randomUUID();
35
58
  const now = new Date();
36
59
  let razorpayRefundId;
@@ -62,6 +85,27 @@ export async function processRefundAction(input) {
62
85
  : {}),
63
86
  };
64
87
  await orderRepository.postRefundEvent(input.orderId, event, isFull);
88
+ // SB-UNI-N — revoke any available (unclaimed) digital codes linked to this order.
89
+ if (digitalCodeItems.length > 0) {
90
+ const db = getAdminDb();
91
+ for (const item of digitalCodeItems) {
92
+ db.collection(PRODUCT_COLLECTION)
93
+ .doc(item.productId)
94
+ .collection(PRODUCT_CODES_SUBCOLLECTION)
95
+ .where("orderId", "==", input.orderId)
96
+ .where("status", "==", "available")
97
+ .limit(1)
98
+ .get()
99
+ .then((snap) => {
100
+ if (!snap.empty) {
101
+ snap.docs[0].ref
102
+ .update({ status: "revoked", updatedAt: new Date() })
103
+ .catch((e) => serverLogger.error("refund: code revoke failed", e));
104
+ }
105
+ })
106
+ .catch((e) => serverLogger.error("refund: code query failed", e));
107
+ }
108
+ }
65
109
  // Fire-and-forget: deduct from the store's pending payout if one exists.
66
110
  // Failure here must not roll back the already-posted refund event.
67
111
  if (order.storeId) {
@@ -49,6 +49,14 @@ export declare function sendContactEmail(params: {
49
49
  success: boolean;
50
50
  data?: unknown;
51
51
  }>;
52
+ export declare function sendDigitalCodeClaimedEmail(params: {
53
+ to: string;
54
+ userName: string;
55
+ productTitle: string;
56
+ orderId: string;
57
+ }): Promise<{
58
+ success: boolean;
59
+ }>;
52
60
  export declare function sendSiteSettingsChangedEmail(params: {
53
61
  adminEmails: string[];
54
62
  changedByEmail: string;
@@ -220,6 +220,66 @@ export async function sendContactEmail(params) {
220
220
  return { success: false };
221
221
  }
222
222
  }
223
+ export async function sendDigitalCodeClaimedEmail(params) {
224
+ const siteName = getSiteName();
225
+ const siteUrl = getSiteUrl();
226
+ const { to, userName, productTitle, orderId } = params;
227
+ const orderUrl = `${siteUrl}/user/orders/view/${orderId}`;
228
+ try {
229
+ const { error } = await sendConfiguredEmail({
230
+ to,
231
+ subject: `Your digital code for "${productTitle}" is ready`,
232
+ html: `
233
+ <!DOCTYPE html><html>
234
+ <head><meta charset="utf-8"><title>Digital Code Ready</title></head>
235
+ <body style="margin:0;padding:0;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,'Helvetica Neue',Arial,sans-serif;background-color:#f5f5f5;">
236
+ <table width="100%" cellpadding="0" cellspacing="0" border="0" style="background-color:#f5f5f5;padding:40px 0;">
237
+ <tr><td align="center">
238
+ <table width="600" cellpadding="0" cellspacing="0" border="0" style="background-color:#ffffff;border-radius:8px;overflow:hidden;box-shadow:0 2px 8px rgba(0,0,0,0.1);">
239
+ <tr><td style="background:linear-gradient(135deg,#6366f1 0%,#4f46e5 100%);padding:40px;text-align:center;">
240
+ <p style="font-size:48px;margin:0 0 12px;">&#x1F511;</p>
241
+ <h1 style="color:#ffffff;margin:0;font-size:26px;font-weight:600;">Your Digital Code Is Ready</h1>
242
+ </td></tr>
243
+ <tr><td style="padding:40px;">
244
+ <p style="color:#333;font-size:16px;line-height:1.6;margin:0 0 16px;">Hi ${userName},</p>
245
+ <p style="color:#333;font-size:16px;line-height:1.6;margin:0 0 24px;">
246
+ Your digital code for <strong>${productTitle}</strong> has been assigned to your order and is ready to reveal.
247
+ </p>
248
+ <table width="100%" cellpadding="12" cellspacing="0" border="0" style="background:#f8f9fa;border-radius:8px;margin-bottom:24px;">
249
+ <tr><td style="color:#666;font-size:14px;width:40%;">Order ID</td>
250
+ <td style="color:#333;font-size:14px;font-weight:600;">${orderId}</td></tr>
251
+ <tr><td style="color:#666;font-size:14px;">Product</td>
252
+ <td style="color:#333;font-size:14px;">${productTitle}</td></tr>
253
+ </table>
254
+ <table width="100%" cellpadding="0" cellspacing="0" border="0" style="margin:24px 0;">
255
+ <tr><td align="center">
256
+ <a href="${orderUrl}" style="display:inline-block;padding:14px 40px;background:linear-gradient(135deg,#6366f1 0%,#4f46e5 100%);color:#ffffff;text-decoration:none;border-radius:6px;font-size:15px;font-weight:600;">
257
+ Reveal Your Code
258
+ </a>
259
+ </td></tr>
260
+ </table>
261
+ <p style="color:#999;font-size:13px;line-height:1.6;margin:0;">For security, the code is only shown inside your ${siteName} account.</p>
262
+ </td></tr>
263
+ <tr><td style="background-color:#f8f9fa;padding:24px;text-align:center;border-top:1px solid #eee;">
264
+ <p style="color:#9ca3af;font-size:12px;margin:0;">&#169; ${currentYear()} ${siteName}. All rights reserved.</p>
265
+ </td></tr>
266
+ </table>
267
+ </td></tr>
268
+ </table>
269
+ </body></html>`,
270
+ text: `Hi ${userName},\n\nYour digital code for "${productTitle}" (Order: ${orderId}) is ready to reveal.\n\nVisit your order: ${orderUrl}\n\nFor security, the code is only shown inside your ${siteName} account.\n\n© ${currentYear()} ${siteName}`,
271
+ });
272
+ if (error) {
273
+ serverLogger.error("Failed to send digital code claimed email", { error });
274
+ return { success: false };
275
+ }
276
+ return { success: true };
277
+ }
278
+ catch (error) {
279
+ serverLogger.error("Error sending digital code claimed email", { error });
280
+ return { success: false };
281
+ }
282
+ }
223
283
  export async function sendSiteSettingsChangedEmail(params) {
224
284
  const siteName = getSiteName();
225
285
  const siteUrl = getSiteUrl();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mohasinac/appkit",
3
- "version": "2.7.44",
3
+ "version": "2.7.45",
4
4
  "license": "MIT",
5
5
  "publishConfig": {
6
6
  "access": "public"