feat: fixes and formatter

This commit is contained in:
2026-01-12 16:01:32 +01:00
parent 85c6084351
commit 11e632acff
353 changed files with 23315 additions and 27361 deletions
+119 -145
View File
@@ -1,151 +1,134 @@
const admin = require("firebase-admin");
const { FieldValue } = require("firebase-admin/firestore");
const { onSchedule } = require("firebase-functions/v2/scheduler");
const _ = require("lodash");
const { refList, db, ALERT_TYPE } = require("../index");
const { batchFirestore } = require("../helpers/firebase");
const { BATCH_TYPE } = require("../config/types");
const {
buildMonthKey,
buildPreviousMonthContext,
} = require("../helpers/stats");
const { sendNotification } = require("./notifications");
const admin = require('firebase-admin')
const { FieldValue } = require('firebase-admin/firestore')
const { onSchedule } = require('firebase-functions/v2/scheduler')
const _ = require('lodash')
const { refList, db, ALERT_TYPE } = require('../index')
const { batchFirestore } = require('../helpers/firebase')
const { BATCH_TYPE } = require('../config/types')
const { buildMonthKey, buildPreviousMonthContext } = require('../helpers/stats')
const { sendNotification } = require('./notifications')
const DISTRIBUTION_REVENUE_BASELINE = 1000;
const DISTRIBUTION_RATIO = 0.3;
const ACTIVE_SUBSCRIPTION_STATUSES = new Set(["active"]);
const DISTRIBUTION_REVENUE_BASELINE = 1000
const DISTRIBUTION_RATIO = 0.3
const ACTIVE_SUBSCRIPTION_STATUSES = new Set(['active'])
const hasActiveSubscription = (userData) => {
if (!userData || typeof userData !== "object") {
return false;
if (!userData || typeof userData !== 'object') {
return false
}
const isPremium = userData.isPremium === true;
const isPremium = userData.isPremium === true
if (!isPremium) {
return false;
return false
}
const status =
typeof userData.stripeSubscriptionStatus === "string"
typeof userData.stripeSubscriptionStatus === 'string'
? userData.stripeSubscriptionStatus.trim().toLowerCase()
: null;
: null
if (status && ACTIVE_SUBSCRIPTION_STATUSES.has(status)) {
return true;
return true
}
const billingPeriod =
typeof userData.premiumBillingPeriod === "string"
typeof userData.premiumBillingPeriod === 'string'
? userData.premiumBillingPeriod.trim().toLowerCase()
: null;
if (billingPeriod === "monthly" || billingPeriod === "annual") {
: null
if (billingPeriod === 'monthly' || billingPeriod === 'annual') {
// Fallback: billing period is set only for active subscribers.
return true;
return true
}
return false;
};
return false
}
exports.distributeMonthlyPayouts = onSchedule(
{
schedule: "0 1 1 * *",
timeZone: "Europe/Paris",
schedule: '0 1 1 * *',
timeZone: 'Europe/Paris',
},
async (event) => {
const { scheduleTime } = event;
const context = buildPreviousMonthContext(
scheduleTime ? new Date(scheduleTime) : new Date(),
);
const monthKey = buildMonthKey(context.rangeStart);
const now = admin.firestore.Timestamp.now();
const { scheduleTime } = event
const context = buildPreviousMonthContext(scheduleTime ? new Date(scheduleTime) : new Date())
const monthKey = buildMonthKey(context.rangeStart)
const now = admin.firestore.Timestamp.now()
const statsSnapshot = await db
.collectionGroup("monthlyListens")
.where("monthKey", "==", monthKey)
.orderBy("streams", "desc")
.get();
.collectionGroup('monthlyListens')
.where('monthKey', '==', monthKey)
.orderBy('streams', 'desc')
.get()
const totalsDocRef = refList.projectStreamStatsMonthlyTotals.doc(monthKey);
const totalsSnapshot = await totalsDocRef.get();
const totalsDocRef = refList.projectStreamStatsMonthlyTotals.doc(monthKey)
const totalsSnapshot = await totalsDocRef.get()
const entries = _.chain(statsSnapshot.docs)
.map((doc) => {
const data = doc.data() || {};
const data = doc.data() || {}
return {
projectId:
_.get(data, "projectId") || doc.ref.parent.parent?.id || null,
userId: _.get(data, "userId", null),
streams: _.toFinite(_.get(data, "streams", 0)),
projectId: _.get(data, 'projectId') || doc.ref.parent.parent?.id || null,
userId: _.get(data, 'userId', null),
streams: _.toFinite(_.get(data, 'streams', 0)),
statsDocPath: doc.ref.path,
};
}
})
.filter((entry) => entry.projectId && entry.streams > 0)
.orderBy(["streams"], ["desc"])
.value();
.orderBy(['streams'], ['desc'])
.value()
const userEligibilityMap = {};
const userIds = _.uniq(
entries.map((entry) => entry.userId).filter((userId) => !!userId),
);
const userEligibilityMap = {}
const userIds = _.uniq(entries.map((entry) => entry.userId).filter((userId) => !!userId))
if (userIds.length) {
const chunkSize = 300;
const chunkSize = 300
for (let index = 0; index < userIds.length; index += chunkSize) {
const chunk = userIds.slice(index, index + chunkSize);
const chunk = userIds.slice(index, index + chunkSize)
const snapshots = await Promise.all(
chunk.map(async (userId) => {
try {
return await refList.users.doc(userId).get();
return await refList.users.doc(userId).get()
} catch (error) {
console.warn(
"[distributeMonthlyPayouts] Unable to load user profile",
{
userId,
error: error?.message || String(error),
},
);
return null;
console.warn('[distributeMonthlyPayouts] Unable to load user profile', {
userId,
error: error?.message || String(error),
})
return null
}
}),
);
})
)
snapshots.forEach((snapshot, snapshotIndex) => {
const userId = chunk[snapshotIndex];
const userId = chunk[snapshotIndex]
if (snapshot?.exists) {
userEligibilityMap[userId] = hasActiveSubscription(
snapshot.data(),
);
userEligibilityMap[userId] = hasActiveSubscription(snapshot.data())
} else {
userEligibilityMap[userId] = false;
userEligibilityMap[userId] = false
}
});
})
}
}
const eligibleEntries = entries.filter(
(entry) =>
!!entry.userId && userEligibilityMap[entry.userId] === true,
);
const eligibleTotalStreams = _.sumBy(eligibleEntries, "streams");
(entry) => !!entry.userId && userEligibilityMap[entry.userId] === true
)
const eligibleTotalStreams = _.sumBy(eligibleEntries, 'streams')
const payoutsTotalStreamsFromDocs = _.sumBy(entries, "streams");
const totalsData = totalsSnapshot.exists ? totalsSnapshot.data() : null;
let totalStreams = _.toFinite(_.get(totalsData, "totalStreams", 0));
const payoutsTotalStreamsFromDocs = _.sumBy(entries, 'streams')
const totalsData = totalsSnapshot.exists ? totalsSnapshot.data() : null
let totalStreams = _.toFinite(_.get(totalsData, 'totalStreams', 0))
if (!totalStreams || totalStreams < payoutsTotalStreamsFromDocs) {
totalStreams = payoutsTotalStreamsFromDocs;
totalStreams = payoutsTotalStreamsFromDocs
}
const payoutPool = _.round(
DISTRIBUTION_REVENUE_BASELINE * DISTRIBUTION_RATIO,
2,
);
const payoutPool = _.round(DISTRIBUTION_REVENUE_BASELINE * DISTRIBUTION_RATIO, 2)
let allocations = _.map(eligibleEntries, (entry) => {
if (!eligibleTotalStreams) return 0;
const rawAmount = (payoutPool * entry.streams) / eligibleTotalStreams;
return _.round(rawAmount, 2);
});
if (!eligibleTotalStreams) return 0
const rawAmount = (payoutPool * entry.streams) / eligibleTotalStreams
return _.round(rawAmount, 2)
})
if (allocations.length > 0) {
const allocatedTotal = _.round(_.sum(allocations), 2);
const remainder = _.round(payoutPool - allocatedTotal, 2);
const allocatedTotal = _.round(_.sum(allocations), 2)
const remainder = _.round(payoutPool - allocatedTotal, 2)
if (remainder !== 0) {
allocations[0] = _.round(allocations[0] + remainder, 2);
allocations[0] = _.round(allocations[0] + remainder, 2)
}
}
@@ -154,40 +137,36 @@ exports.distributeMonthlyPayouts = onSchedule(
projectId: entry.projectId,
userId: entry.userId,
streams: entry.streams,
share: eligibleTotalStreams
? _.round(entry.streams / eligibleTotalStreams, 6)
: 0,
share: eligibleTotalStreams ? _.round(entry.streams / eligibleTotalStreams, 6) : 0,
amount: allocations[idx],
statsDocPath: entry.statsDocPath,
}));
}))
const userDocs = _(payouts)
.filter((payout) => !!payout.userId)
.groupBy("userId")
.groupBy('userId')
.map((projects, userId) => {
const sortedProjects = _.orderBy(projects, ["amount"], ["desc"]).map(
(project) => ({
projectId: project.projectId,
rank: project.rank,
amount: project.amount,
streams: project.streams,
share: project.share,
statsDocPath: project.statsDocPath,
}),
);
const sortedProjects = _.orderBy(projects, ['amount'], ['desc']).map((project) => ({
projectId: project.projectId,
rank: project.rank,
amount: project.amount,
streams: project.streams,
share: project.share,
statsDocPath: project.statsDocPath,
}))
return {
userId,
totalAmount: _.round(_.sumBy(projects, "amount"), 2),
totalStreams: _.sumBy(projects, "streams"),
totalAmount: _.round(_.sumBy(projects, 'amount'), 2),
totalStreams: _.sumBy(projects, 'streams'),
projects: sortedProjects,
};
}
})
.value();
.value()
if (userDocs.length) {
const docs = userDocs.map((userData) => {
const docId = `${monthKey}_${userData.userId}`;
const docId = `${monthKey}_${userData.userId}`
return {
ref: refList.monthlyPayoutEntries.doc(docId),
data: {
@@ -200,35 +179,33 @@ exports.distributeMonthlyPayouts = onSchedule(
computedAt: now,
updatedAt: FieldValue.serverTimestamp(),
},
};
});
}
})
await batchFirestore({
docs,
type: BATCH_TYPE.UPDATE,
});
})
const payoutLabel = `${String(context.month).padStart(2, "0")}/${
context.year
}`;
const payoutLabel = `${String(context.month).padStart(2, '0')}/${context.year}`
await Promise.all(
userDocs.map(async (userData) => {
const receiverId =
typeof userData.userId === "string" && userData.userId.trim()
typeof userData.userId === 'string' && userData.userId.trim()
? userData.userId.trim()
: null;
const amount = Number(userData.totalAmount) || 0;
: null
const amount = Number(userData.totalAmount) || 0
if (!receiverId || amount <= 0) {
return null;
return null
}
const amountLabel = amount.toFixed(2);
const message = `Tes revenus de ${payoutLabel} (${amountLabel} €) sont disponibles.`;
const amountLabel = amount.toFixed(2)
const message = `Tes revenus de ${payoutLabel} (${amountLabel} €) sont disponibles.`
try {
await sendNotification({
sender: "SYSTEM",
sender: 'SYSTEM',
receiver: receiverId,
receiverCollection: "users",
title: "Revenus disponibles",
receiverCollection: 'users',
title: 'Revenus disponibles',
message,
data: {
type: ALERT_TYPE?.PAYOUT_AVAILABLE,
@@ -239,19 +216,16 @@ exports.distributeMonthlyPayouts = onSchedule(
totalStreams: userData.totalStreams,
projects: userData.projects,
},
});
})
} catch (notifError) {
console.log(
"[distributeMonthlyPayouts] Failed to send payout notification:",
{
userId: receiverId,
error: notifError?.message || String(notifError),
},
);
console.log('[distributeMonthlyPayouts] Failed to send payout notification:', {
userId: receiverId,
error: notifError?.message || String(notifError),
})
}
return null;
}),
);
return null
})
)
}
const summary = {
@@ -270,14 +244,14 @@ exports.distributeMonthlyPayouts = onSchedule(
totalRecipients: payouts.length,
totalEntries: entries.length,
eligibleEntries: eligibleEntries.length,
totalAllocated: _.round(_.sumBy(payouts, "amount"), 2),
totalAllocated: _.round(_.sumBy(payouts, 'amount'), 2),
payouts,
status: payouts.length ? "computed" : "no-data",
status: payouts.length ? 'computed' : 'no-data',
updatedAt: FieldValue.serverTimestamp(),
computedAt: now,
};
}
const docRef = refList.monthlyPayouts.doc(monthKey);
await docRef.set(summary, { merge: true });
},
);
const docRef = refList.monthlyPayouts.doc(monthKey)
await docRef.set(summary, { merge: true })
}
)