feat: billing period for subscriptions, costs in redundancy analysis

This commit is contained in:
root
2026-07-29 23:46:58 +02:00
parent deabe36727
commit 44cc29b8fe
6 changed files with 109 additions and 14 deletions
+28 -4
View File
@@ -1,6 +1,6 @@
import { Router, type IRouter } from "express";
import { sql } from "drizzle-orm";
import { db, toolsTable, ratingsTable } from "@workspace/db";
import { db, toolsTable, ratingsTable, toolCostsTable } from "@workspace/db";
import { requireAuth } from "../middleware/auth";
const router: IRouter = Router();
@@ -12,6 +12,10 @@ router.get("/admin/redundancy", requireAuth, async (_req, res): Promise<void> =>
.select({ toolId: ratingsTable.toolId, usefulness: ratingsTable.usefulness, usability: ratingsTable.usability })
.from(ratingsTable);
const allCosts = await db
.select()
.from(toolCostsTable);
const ratingsByTool = new Map<number, { usefulness: number; usability: number }[]>();
for (const r of allRatings) {
const arr = ratingsByTool.get(r.toolId) ?? [];
@@ -19,16 +23,34 @@ router.get("/admin/redundancy", requireAuth, async (_req, res): Promise<void> =>
ratingsByTool.set(r.toolId, arr);
}
const costsByTool = new Map<number, typeof allCosts>();
for (const c of allCosts) {
const arr = costsByTool.get(c.toolId) ?? [];
arr.push(c);
costsByTool.set(c.toolId, arr);
}
function buildStats(t: typeof toolsTable.$inferSelect) {
const ratings = ratingsByTool.get(t.id) ?? [];
const count = ratings.length;
const avgU = count > 0 ? ratings.reduce((s, r) => s + r.usefulness, 0) / count : null;
const avgUs = count > 0 ? ratings.reduce((s, r) => s + r.usability, 0) / count : null;
const costs = costsByTool.get(t.id) ?? [];
const totalMonthly = costs.reduce((sum, c) => {
if (c.licenseType === "free" || c.cost == null) return sum;
const amount = Number(c.cost);
if (c.licenseType === "one_time") return sum;
if (c.billingPeriod === "yearly") return sum + amount / 12;
if (c.billingPeriod === "quarterly") return sum + amount / 3;
return sum + amount; // monthly or subscription default
}, 0);
return {
id: t.id, name: t.name, category: t.category, features: t.features ?? [], tags: t.tags ?? [],
ratingCount: count,
avgUsefulness: avgU, avgUsability: avgUs,
avgCombined: avgU != null && avgUs != null ? (avgU + avgUs) / 2 : null,
costs,
totalMonthly,
};
}
@@ -40,7 +62,7 @@ router.get("/admin/redundancy", requireAuth, async (_req, res): Promise<void> =>
}
const result = Object.entries(grouped).map(([category, items]) => {
const pairs: { a: any; b: any; overlap: number; scoreDiff: number }[] = [];
const pairs: { a: any; b: any; overlap: number; scoreDiff: number; costDiff: number }[] = [];
for (let i = 0; i < items.length; i++) {
for (let j = i + 1; j < items.length; j++) {
const aFeat = new Set(items[i].features);
@@ -49,11 +71,13 @@ router.get("/admin/redundancy", requireAuth, async (_req, res): Promise<void> =>
const total = new Set([...aFeat, ...bFeat]).size;
const overlap = total > 0 ? Math.round((shared / total) * 100) : 0;
const scoreDiff = (items[j].avgCombined ?? 0) - (items[i].avgCombined ?? 0);
pairs.push({ a: items[i], b: items[j], overlap, scoreDiff });
const costDiff = (items[j].totalMonthly ?? 0) - (items[i].totalMonthly ?? 0);
pairs.push({ a: items[i], b: items[j], overlap, scoreDiff, costDiff });
}
}
pairs.sort((a, b) => b.overlap - a.overlap || Math.abs(b.scoreDiff) - Math.abs(a.scoreDiff));
return { category, tools: items, pairs: pairs.slice(0, 5) };
const totalMonthlyCost = items.reduce((s, t) => s + (t.totalMonthly ?? 0), 0);
return { category, tools: items, pairs: pairs.slice(0, 5), totalMonthlyCost };
});
res.json(result);