@mohasinac/appkit 2.7.43 → 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.
- package/dist/_internal/server/features/checkout/actions.js +60 -1
- package/dist/_internal/server/features/products/actions.js +7 -0
- package/dist/_internal/server/features/refunds/actions.js +44 -0
- package/dist/features/contact/email.d.ts +8 -0
- package/dist/features/contact/email.js +60 -0
- package/package.json +1 -1
|
@@ -14,7 +14,7 @@ import { sendOrderConfirmationEmail } from "../../../../features/contact/server"
|
|
|
14
14
|
import { splitCartIntoOrderGroups } from "../../../../features/orders/index";
|
|
15
15
|
import { resolveDate } from "../../../../utils";
|
|
16
16
|
import { getAdminDb, getAdminRealtimeDb, RTDB_PATHS, } from "../../../../providers/db-firebase";
|
|
17
|
-
import { PRODUCT_COLLECTION } from "../../../../features/products/schemas/firestore";
|
|
17
|
+
import { PRODUCT_COLLECTION, PRODUCT_CODES_SUBCOLLECTION } from "../../../../features/products/schemas/firestore";
|
|
18
18
|
import { CART_COLLECTION } from "../../../../features/cart/schemas/index";
|
|
19
19
|
// S-SBUNI-RULES 2026-05-13 — bundle cart-line stock fan-out helpers.
|
|
20
20
|
import { getCartItemMemberIds, getExpandedDecrements, validateCartItemStock, } from "./bundle-expansion";
|
|
@@ -27,6 +27,47 @@ import { formatShippingAddress } from "./data";
|
|
|
27
27
|
import { enforceMaxPerUserForCart, computePrizeRevealDeadline, } from "./prize-bundle-gates";
|
|
28
28
|
import { getListingRule, runSyncPreflight, } from "../../../shared/checkout/rules";
|
|
29
29
|
import { cartIsDigitalOnly } from "../../../shared/listing-types/cart-shipping";
|
|
30
|
+
/**
|
|
31
|
+
* SB-UNI-N — Atomically claim the next available digital code for a confirmed
|
|
32
|
+
* order. Pre-fetches a candidate code outside the transaction then locks and
|
|
33
|
+
* verifies in a micro-transaction. Fire-and-forget safe (logs on exhaustion
|
|
34
|
+
* but never fails the already-created order).
|
|
35
|
+
*/
|
|
36
|
+
async function claimDigitalCodeForOrder(db, productId, orderId, userId, opts) {
|
|
37
|
+
const codesRef = db
|
|
38
|
+
.collection(PRODUCT_COLLECTION)
|
|
39
|
+
.doc(productId)
|
|
40
|
+
.collection(PRODUCT_CODES_SUBCOLLECTION);
|
|
41
|
+
const snap = await codesRef.where("status", "==", "available").limit(1).get();
|
|
42
|
+
if (snap.empty) {
|
|
43
|
+
serverLogger.warn("claimDigitalCode: code pool exhausted", { productId, orderId });
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
const codeRef = snap.docs[0].ref;
|
|
47
|
+
let claimed = false;
|
|
48
|
+
await db.runTransaction(async (tx) => {
|
|
49
|
+
const latest = await tx.get(codeRef);
|
|
50
|
+
if (!latest.exists || latest.data()?.status !== "available")
|
|
51
|
+
return;
|
|
52
|
+
tx.update(codeRef, {
|
|
53
|
+
status: "claimed",
|
|
54
|
+
orderId,
|
|
55
|
+
claimedByUserId: userId,
|
|
56
|
+
claimedAt: new Date(),
|
|
57
|
+
updatedAt: new Date(),
|
|
58
|
+
});
|
|
59
|
+
claimed = true;
|
|
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
|
+
}
|
|
70
|
+
}
|
|
30
71
|
/**
|
|
31
72
|
* SB-UNI-O — Live-item jurisdiction guard.
|
|
32
73
|
* Throws ValidationError if any live cart item's allowed states don't include
|
|
@@ -470,6 +511,15 @@ export async function createCheckoutOrderAction(input) {
|
|
|
470
511
|
...extraOrderFields,
|
|
471
512
|
});
|
|
472
513
|
orderIds.push(order.id);
|
|
514
|
+
// SB-UNI-N — claim a digital code for digital-code orders (fire-and-forget,
|
|
515
|
+
// order is already persisted so a claim failure is logged, not thrown).
|
|
516
|
+
if ((group[0]?.product?.listingType ?? "standard") === "digital-code") {
|
|
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));
|
|
522
|
+
}
|
|
473
523
|
for (const code of groupCouponCodes) {
|
|
474
524
|
const entry = couponUsageAccumulator.get(code);
|
|
475
525
|
if (entry)
|
|
@@ -840,6 +890,15 @@ export async function verifyAndPlaceRazorpayOrderAction(input) {
|
|
|
840
890
|
...extraOrderFields,
|
|
841
891
|
});
|
|
842
892
|
orderIds.push(order.id);
|
|
893
|
+
// SB-UNI-N — claim a digital code for digital-code orders (fire-and-forget,
|
|
894
|
+
// order is already persisted so a claim failure is logged, not thrown).
|
|
895
|
+
if ((group[0]?.product?.listingType ?? "standard") === "digital-code") {
|
|
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));
|
|
901
|
+
}
|
|
843
902
|
if (userEmail) {
|
|
844
903
|
emailsToSend.push({
|
|
845
904
|
to: userEmail,
|
|
@@ -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;">🔑</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;">© ${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();
|