monthly money

This commit is contained in:
Thomas Demirdjian
2025-10-31 17:31:10 +01:00
parent 6f330c1df8
commit 0dd0fa5ab7
10 changed files with 299 additions and 1 deletions
+127
View File
@@ -0,0 +1,127 @@
const admin = require("firebase-admin");
const { onSchedule } = require("firebase-functions/v2/scheduler");
const { refList } = require("../index");
const firestore = refList.projects.firestore;
const DISTRIBUTION_REVENUE_BASELINE = 1000;
const DISTRIBUTION_RATIO = 0.3;
const MAX_RECIPIENTS = 10;
const WEIGHTS = Array.from({ length: MAX_RECIPIENTS }, (_, idx) => MAX_RECIPIENTS - idx);
const buildPreviousMonthContext = (referenceDate) => {
const current = referenceDate ? new Date(referenceDate) : new Date();
current.setHours(1, 0, 0, 0);
current.setDate(1);
const target = new Date(current);
target.setMonth(target.getMonth() - 1);
const month = target.getMonth();
const year = target.getFullYear();
const monthKey = `${year}-${String(month + 1).padStart(2, "0")}`;
const rangeStart = new Date(year, month, 1, 0, 0, 0, 0);
const rangeEnd = new Date(year, month + 1, 0, 23, 59, 59, 999);
return {
year,
month: month + 1,
monthKey,
rangeStart,
rangeEnd,
};
};
exports.distributeMonthlyPayouts = onSchedule(
{
schedule: "0 1 1 * *",
timeZone: "Europe/Paris",
},
async (event) => {
const { scheduleTime } = event;
const context = buildPreviousMonthContext(
scheduleTime ? new Date(scheduleTime) : new Date()
);
const monthKey = context.monthKey;
const now = admin.firestore.Timestamp.now();
const statsSnapshot = await firestore
.collectionGroup("monthly")
.where("monthKey", "==", monthKey)
.orderBy("streams", "desc")
.limit(MAX_RECIPIENTS)
.get();
const topEntries = statsSnapshot.docs
.map((doc, index) => {
const data = doc.data() || {};
return {
rank: index + 1,
projectId: data.projectId || doc.ref.parent.parent?.id || null,
userId: data.userId || null,
streams: typeof data.streams === "number" ? data.streams : 0,
statsDocPath: doc.ref.path,
};
})
.filter((entry) => entry.projectId && entry.streams > 0);
const payoutPool = Number(
(DISTRIBUTION_REVENUE_BASELINE * DISTRIBUTION_RATIO).toFixed(2)
);
const weights = WEIGHTS.slice(0, topEntries.length);
const weightSum = weights.reduce((sum, weight) => sum + weight, 0);
let allocations = topEntries.map((entry, idx) => {
if (!weightSum) return 0;
const rawAmount = (payoutPool * weights[idx]) / weightSum;
return Number(rawAmount.toFixed(2));
});
if (allocations.length > 0) {
const allocatedTotal = Number(
allocations.reduce((sum, amount) => sum + amount, 0).toFixed(2)
);
const remainder = Number((payoutPool - allocatedTotal).toFixed(2));
if (remainder !== 0) {
allocations[0] = Number((allocations[0] + remainder).toFixed(2));
}
}
const payouts = topEntries.map((entry, idx) => ({
rank: entry.rank,
projectId: entry.projectId,
userId: entry.userId,
streams: entry.streams,
weight: weights[idx],
amount: allocations[idx],
statsDocPath: entry.statsDocPath,
}));
const summary = {
monthKey,
month: context.month,
year: context.year,
range: {
start: admin.firestore.Timestamp.fromDate(context.rangeStart),
end: admin.firestore.Timestamp.fromDate(context.rangeEnd),
},
revenueBaseline: DISTRIBUTION_REVENUE_BASELINE,
payoutRatio: DISTRIBUTION_RATIO,
payoutPool,
totalStreams: topEntries.reduce((sum, entry) => sum + entry.streams, 0),
totalRecipients: payouts.length,
totalAllocated: payouts.reduce(
(sum, payout) => Number((sum + payout.amount).toFixed(2)),
0
),
payouts,
status: payouts.length ? "computed" : "no-data",
updatedAt: admin.firestore.FieldValue.serverTimestamp(),
computedAt: now,
};
const docRef = firestore.collection("monthlyPayouts").doc(monthKey);
await docRef.set(summary, { merge: true });
}
);