clear lot of tickets
This commit is contained in:
@@ -43,7 +43,7 @@ function basicTemplate({ title = "", content = "", button = null }) {
|
||||
<div style="color:rgba(255,255,255,0.9)!important">${content}</div>
|
||||
${btn}
|
||||
<hr style="margin:24px 0;border:none;border-top:1px solid rgba(255,255,255,0.1)" />
|
||||
<p style="color:rgba(255,255,255,0.45);font-size:12px;margin:0">minuit.app</p>
|
||||
<p style="color:rgba(255,255,255,0.45);font-size:12px;margin:0">musicland</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
@@ -33,6 +33,7 @@ exports.ALERT_TYPE = {
|
||||
MUSIC_GENERATION_FAILED: "MUSIC_GENERATION_FAILED",
|
||||
COVER_GENERATION_SUCCESS: "COVER_GENERATION_SUCCESS",
|
||||
COVER_GENERATION_FAILED: "COVER_GENERATION_FAILED",
|
||||
CREDITS_UPDATED: "CREDITS_UPDATED",
|
||||
PAYOUT_AVAILABLE: "PAYOUT_AVAILABLE",
|
||||
};
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@ const {
|
||||
onDocumentCreated,
|
||||
onDocumentWritten,
|
||||
} = require("firebase-functions/v2/firestore");
|
||||
const admin = require("firebase-admin");
|
||||
const { FieldValue } = require("firebase-admin/firestore");
|
||||
const { refList, ALERT_TYPE } = require("../index");
|
||||
const { Expo } = require("expo-server-sdk");
|
||||
@@ -10,11 +9,12 @@ const { Resend } = require("resend");
|
||||
const { basicTemplate } = require("../helpers/email");
|
||||
const { RESEND_API_KEY } = require("../config/keys");
|
||||
|
||||
const resendInstance = new Resend(RESEND_API_KEY);
|
||||
const resendInstance = RESEND_API_KEY ? new Resend(RESEND_API_KEY) : null;
|
||||
|
||||
// Initialisation de Expo SDK
|
||||
let expo = new Expo();
|
||||
const EMAIL_FROM = "MusicLand <musicland@minuit.app>";
|
||||
const DEFAULT_EMAIL_TITLE = "MusicLand";
|
||||
|
||||
function getCollectionRef(collectionName = "") {
|
||||
const ref = refList?.[collectionName];
|
||||
@@ -24,6 +24,49 @@ function getCollectionRef(collectionName = "") {
|
||||
return ref;
|
||||
}
|
||||
|
||||
function cleanString(value) {
|
||||
return typeof value === "string" && value.trim() ? value.trim() : null;
|
||||
}
|
||||
|
||||
function buildNotificationEmailPayload({
|
||||
title = "",
|
||||
message = "",
|
||||
template = {},
|
||||
} = {}) {
|
||||
const fallbackTitle = cleanString(title) || DEFAULT_EMAIL_TITLE;
|
||||
const fallbackContent = cleanString(message) || "";
|
||||
const overrides = template && typeof template === "object" ? template : {};
|
||||
|
||||
const subject = cleanString(overrides.subject) || fallbackTitle;
|
||||
const emailTitle = cleanString(overrides.title) || fallbackTitle;
|
||||
const content = cleanString(overrides.content) || fallbackContent;
|
||||
|
||||
let button = null;
|
||||
if (overrides.button && typeof overrides.button === "object") {
|
||||
const buttonUrl =
|
||||
cleanString(overrides.button.url) || cleanString(overrides.button.href);
|
||||
if (buttonUrl) {
|
||||
button = {
|
||||
url: buttonUrl,
|
||||
label:
|
||||
cleanString(overrides.button.label) ||
|
||||
cleanString(overrides.button.text) ||
|
||||
undefined,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const templatePayload = { title: emailTitle, content };
|
||||
if (button) {
|
||||
templatePayload.button = button;
|
||||
}
|
||||
|
||||
return {
|
||||
subject,
|
||||
html: basicTemplate(templatePayload),
|
||||
};
|
||||
}
|
||||
|
||||
exports.sendNotificationWhenDocIsCreated = onDocumentCreated(
|
||||
{ region: "europe-west1", document: "notifications/{notificationId}" },
|
||||
async (event) => {
|
||||
@@ -49,52 +92,63 @@ exports.sendNotificationWhenDocIsCreated = onDocumentCreated(
|
||||
const {
|
||||
pushToken = null,
|
||||
pushTokens = [],
|
||||
email = "",
|
||||
email: receiverEmail = "",
|
||||
emailNotifications = false,
|
||||
} = receiverData;
|
||||
|
||||
if (!mailOnly) {
|
||||
const tokensSet = new Set(
|
||||
[]
|
||||
.concat(Array.isArray(pushTokens) ? pushTokens : [])
|
||||
.concat(pushToken ? [pushToken] : [])
|
||||
.filter(Boolean),
|
||||
);
|
||||
const tokens = Array.from(tokensSet);
|
||||
|
||||
if (!tokens?.length) {
|
||||
console.warn("User push token not found");
|
||||
return null;
|
||||
}
|
||||
|
||||
await sendExpoNotification({
|
||||
tokens,
|
||||
receiverId: receiver,
|
||||
receiverCollection,
|
||||
title: title || "MusicLand",
|
||||
message: message,
|
||||
data: notifData || {},
|
||||
});
|
||||
}
|
||||
if (emailNotifications && !!email) {
|
||||
try {
|
||||
if (!email) {
|
||||
throw new Error("userMail is required");
|
||||
const tokensSet = new Set(
|
||||
[]
|
||||
.concat(Array.isArray(pushTokens) ? pushTokens : [])
|
||||
.concat(pushToken ? [pushToken] : [])
|
||||
.filter(Boolean),
|
||||
);
|
||||
const tokens = Array.from(tokensSet);
|
||||
|
||||
if (!tokens?.length) {
|
||||
await sendExpoNotification({
|
||||
tokens,
|
||||
receiverId: receiver,
|
||||
receiverCollection,
|
||||
title: title || "MusicLand",
|
||||
message: message,
|
||||
data: notifData || {},
|
||||
});
|
||||
} else {
|
||||
console.log("User push token not found");
|
||||
}
|
||||
if (!email?.subject || !email?.html) {
|
||||
throw new Error("Email subject and html are required");
|
||||
}
|
||||
await resendInstance.emails.send({
|
||||
from: EMAIL_FROM,
|
||||
to: [email],
|
||||
subject: title,
|
||||
html: basicTemplate({ title, content: message }),
|
||||
});
|
||||
} catch (e) {
|
||||
console.log("Error sending email:", e);
|
||||
console.log("Error sending notif:", e);
|
||||
}
|
||||
}
|
||||
|
||||
if ((emailNotifications || mailOnly) && !!receiverEmail) {
|
||||
if (!resendInstance) {
|
||||
console.warn(
|
||||
"Resend client not configured; unable to send notification email.",
|
||||
);
|
||||
} else {
|
||||
try {
|
||||
const { subject, html } = buildNotificationEmailPayload({
|
||||
title,
|
||||
message,
|
||||
template: notifData?.email,
|
||||
});
|
||||
await resendInstance.emails.send({
|
||||
from: EMAIL_FROM,
|
||||
to: [receiverEmail],
|
||||
subject,
|
||||
html,
|
||||
});
|
||||
} catch (e) {
|
||||
console.log("Error sending email:", e);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
console.log(`Email disabled for user ${receiver} and type ${type}`);
|
||||
console.log(
|
||||
`[sendNotificationWhenDocIsCreated] Email not sent (${receiverEmail ? "user preference" : "missing email"}) for user ${receiver} and type ${notifData?.type || "UNKNOWN"}`,
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
|
||||
+138
-1
@@ -3,7 +3,8 @@ const { FieldValue } = require("firebase-admin/firestore");
|
||||
const { onDocumentCreated } = require("firebase-functions/firestore");
|
||||
const { HttpsError, onCall } = require("firebase-functions/https");
|
||||
|
||||
const { REGION } = require("../index");
|
||||
const { REGION, ALERT_TYPE } = require("../index");
|
||||
const { sendNotification } = require("./notifications");
|
||||
const {
|
||||
ORDER_TYPES,
|
||||
ORDER_STATUS,
|
||||
@@ -14,6 +15,121 @@ const {
|
||||
|
||||
const USERS_COLLECTION = "users";
|
||||
|
||||
const formatCoinsText = (value) => {
|
||||
if (typeof value !== "number" || Number.isNaN(value)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const absoluteValue = Math.abs(value);
|
||||
if (!Number.isFinite(absoluteValue)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const formatted = Number.isInteger(absoluteValue)
|
||||
? `${absoluteValue}`
|
||||
: absoluteValue.toFixed(2);
|
||||
const suffix = absoluteValue === 1 ? "crédit" : "crédits";
|
||||
|
||||
return `${formatted} ${suffix}`;
|
||||
};
|
||||
|
||||
const buildOrderNotificationContent = ({ amount, orderType, balanceAfter }) => {
|
||||
if (typeof amount !== "number" || Number.isNaN(amount) || amount === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const coinsText = formatCoinsText(amount);
|
||||
if (!coinsText) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const balanceText = formatCoinsText(balanceAfter);
|
||||
const balanceSentence = balanceText
|
||||
? ` Ton solde est maintenant de ${balanceText}.`
|
||||
: "";
|
||||
|
||||
if (amount > 0) {
|
||||
if (orderType === ORDER_TYPES.COINS) {
|
||||
return {
|
||||
title: "Crédits achetés",
|
||||
message: `Ton achat de ${coinsText} est confirmé.${balanceSentence}`,
|
||||
action: "PURCHASED",
|
||||
};
|
||||
}
|
||||
|
||||
if (orderType === ORDER_TYPES.GIFT) {
|
||||
return {
|
||||
title: "Crédits reçus",
|
||||
message: `Tu as reçu ${coinsText}.${balanceSentence}`,
|
||||
action: "EARNED",
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
title: "Crédits ajoutés",
|
||||
message: `Ton solde augmente de ${coinsText}.${balanceSentence}`,
|
||||
action: "CREDITED",
|
||||
};
|
||||
}
|
||||
|
||||
const reason =
|
||||
orderType === ORDER_TYPES.SONG ? " pour générer un nouveau son" : "";
|
||||
|
||||
return {
|
||||
title: "Crédits dépensés",
|
||||
message: `Tu as dépensé ${coinsText}${reason}.${balanceSentence}`,
|
||||
action: "SPENT",
|
||||
};
|
||||
};
|
||||
|
||||
const notifyOrderApplied = async ({
|
||||
userId,
|
||||
orderId,
|
||||
amount,
|
||||
orderType,
|
||||
balanceBefore,
|
||||
balanceAfter,
|
||||
metadata = {},
|
||||
}) => {
|
||||
const content = buildOrderNotificationContent({
|
||||
amount,
|
||||
orderType,
|
||||
balanceAfter,
|
||||
});
|
||||
|
||||
if (!content || !userId) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await sendNotification({
|
||||
sender: "SYSTEM",
|
||||
receiver: userId,
|
||||
receiverCollection: USERS_COLLECTION,
|
||||
title: content.title,
|
||||
message: content.message,
|
||||
data: {
|
||||
type: ALERT_TYPE?.CREDITS_UPDATED || "CREDITS_UPDATED",
|
||||
orderId,
|
||||
orderType: orderType || null,
|
||||
amount,
|
||||
balanceBefore,
|
||||
balanceAfter,
|
||||
action: content.action,
|
||||
source:
|
||||
typeof metadata?.source === "string" ? metadata.source : null,
|
||||
metadata: metadata || {},
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(
|
||||
"[orders-onOrderCreated] Failed to send notification",
|
||||
orderId,
|
||||
error,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const onOrderCreated = onDocumentCreated(
|
||||
`${ORDERS_COLLECTION}/{orderId}`,
|
||||
async (event) => {
|
||||
@@ -58,6 +174,8 @@ const onOrderCreated = onDocumentCreated(
|
||||
|
||||
const userRef = admin.firestore().collection(USERS_COLLECTION).doc(userId);
|
||||
|
||||
let notificationContext = null;
|
||||
|
||||
try {
|
||||
await admin.firestore().runTransaction(async (transaction) => {
|
||||
const userSnapshot = await transaction.get(userRef);
|
||||
@@ -114,6 +232,11 @@ const onOrderCreated = onDocumentCreated(
|
||||
},
|
||||
{ merge: true },
|
||||
);
|
||||
|
||||
notificationContext = {
|
||||
balanceBefore: currentBalance,
|
||||
balanceAfter: nextBalance,
|
||||
};
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(
|
||||
@@ -131,6 +254,20 @@ const onOrderCreated = onDocumentCreated(
|
||||
},
|
||||
{ merge: true },
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (notificationContext) {
|
||||
await notifyOrderApplied({
|
||||
userId,
|
||||
orderId: orderRef.id,
|
||||
amount,
|
||||
orderType:
|
||||
typeof orderData?.type === "string" ? orderData.type : null,
|
||||
balanceBefore: notificationContext.balanceBefore,
|
||||
balanceAfter: notificationContext.balanceAfter,
|
||||
metadata: orderData?.metadata || {},
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user