Files
zeekay 04afaaa695 feat(usage): unified per-user AI Usage settings tab
Backend: GET /api/usage (requireJwtAuth) aggregates this user's prompt+
completion Transaction spend over today/7d/30d plus a per-model breakdown,
enriched with the org tier from CommerceClient. Response mirrors the
@hanzo/usage UsageSnapshot shape (providerId 'hanzo', totals, providerCost)
for one wire format across Hanzo products.

Frontend: new Usage settings tab beside Balance, reusing the UsageBar idiom;
useGetUserUsage hook follows useGetUserBalance; en localization strings added.
2026-07-07 15:31:03 -07:00

94 lines
3.1 KiB
JavaScript

const mongoose = require('mongoose');
const { Transaction } = require('~/db/models');
const { getCommerceClient } = require('~/server/services/CommerceClient');
/** 1e6 tokenCredits == $1 USD (mirrors client creditsToUsd). */
const CREDITS_PER_USD = 1_000_000;
/** Only prompt/completion transactions represent AI usage (spend); `credits` are deposits. */
const SPEND_TYPES = ['prompt', 'completion'];
/** One accumulator over abs(rawAmount) tokens, abs(tokenValue) credits, and request count. */
const spendGroup = (id) => ({
$group: {
_id: id,
tokens: { $sum: { $abs: { $ifNull: ['$rawAmount', 0] } } },
credits: { $sum: { $abs: { $ifNull: ['$tokenValue', 0] } } },
requests: { $sum: 1 },
},
});
/** Shape a facet bucket into the @hanzo/usage window schema (UsageTotals + ProviderCostSnapshot). */
function toWindow(bucket) {
const row = (bucket && bucket[0]) || { tokens: 0, credits: 0, requests: 0 };
return {
totals: { tokens: row.tokens, requests: row.requests },
providerCost: { used: row.credits / CREDITS_PER_USD, currencyCode: 'USD' },
};
}
/**
* Unified per-user AI usage. Aggregates this user's spend (prompt + completion
* Transactions) over today / 7d / 30d plus a per-model breakdown, enriched with
* the org tier from Commerce. Response mirrors @hanzo/usage's UsageSnapshot
* shape (providerId 'hanzo', totals, providerCost) so it is one wire format
* across Hanzo products.
*/
async function usageController(req, res) {
const userId = new mongoose.Types.ObjectId(req.user.id);
const now = new Date();
const since30 = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000);
const since7 = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);
const startOfToday = new Date(now.getFullYear(), now.getMonth(), now.getDate());
const [facet] = await Transaction.aggregate([
{ $match: { user: userId, tokenType: { $in: SPEND_TYPES }, createdAt: { $gte: since30 } } },
{
$facet: {
today: [{ $match: { createdAt: { $gte: startOfToday } } }, spendGroup(null)],
d7: [{ $match: { createdAt: { $gte: since7 } } }, spendGroup(null)],
d30: [spendGroup(null)],
models: [spendGroup('$model'), { $sort: { credits: -1 } }, { $limit: 20 }],
},
},
]);
const models = (facet?.models ?? [])
.filter((m) => m._id)
.map((m) => ({
model: m._id,
tokens: m.tokens,
requests: m.requests,
cost: m.credits / CREDITS_PER_USD,
}));
const payload = {
providerId: 'hanzo',
currencyCode: 'USD',
tier: 'Free',
windows: {
today: toWindow(facet?.today),
'7d': toWindow(facet?.d7),
'30d': toWindow(facet?.d30),
},
models,
updatedAt: now.toISOString(),
};
// Enrich with Commerce tier (fail-open: usage is still authoritative locally).
const commerceClient = getCommerceClient();
if (commerceClient) {
try {
const tier = await commerceClient.getTierConfig(req.user.id);
if (tier) {
payload.tier = tier.displayName || tier.name || payload.tier;
}
} catch {
// ignore — return local usage without tier enrichment
}
}
res.status(200).json(payload);
}
module.exports = usageController;