SES volume

This commit is contained in:
2026-04-30 09:50:18 -05:00
parent cd2bbe9b7d
commit c44d3228c6
4 changed files with 485 additions and 180 deletions

View File

@@ -1,29 +1,23 @@
import { Router } from 'express';
import { requireAuth, requireSuperAdmin } from '../middleware/auth.js';
import { computeMonthlyBilling, listBillingEvents, PRICE_PER_INBOX } from '../services/billing.js';
import { getDomainVolumeForMonth, currentYm } from '../services/ses-events.js';
import {
getDomainVolumeForMonth,
getVolumeOverview,
currentYm,
previousYm,
} from '../services/ses-events.js';
export const billingRouter = Router();
billingRouter.use(requireAuth);
billingRouter.use(requireSuperAdmin);
/**
* GET /api/billing/summary?domain=foo.com
* Inbox-count based monthly summary ($5 per inbox per month).
*/
billingRouter.get('/summary', async (req, res) => {
const domain = req.query.domain ? String(req.query.domain).toLowerCase() : undefined;
const months = await computeMonthlyBilling({ domain });
res.json({
price_per_inbox: PRICE_PER_INBOX,
months,
});
res.json({ price_per_inbox: PRICE_PER_INBOX, months });
});
/**
* GET /api/billing/events?domain=foo.com
* Raw mailbox lifecycle event log (created/deleted).
*/
billingRouter.get('/events', async (req, res) => {
const events = await listBillingEvents({
domain: req.query.domain ? String(req.query.domain).toLowerCase() : undefined,
@@ -36,13 +30,7 @@ billingRouter.get('/events', async (req, res) => {
/**
* GET /api/billing/volume?domain=foo.com&ym=2026-04
* SES outbound volume for a domain in a specific month.
*
* Returns per-inbox + domain totals with send count, total bytes,
* bounce count and complaint count.
*
* Defaults to the current month if ym is omitted.
* Domain is required because volume is always per-domain.
* Per-inbox drilldown for a single domain.
*/
billingRouter.get('/volume', async (req, res) => {
const domain = req.query.domain ? String(req.query.domain).toLowerCase() : '';
@@ -51,16 +39,38 @@ billingRouter.get('/volume', async (req, res) => {
return;
}
const ym = req.query.ym
? String(req.query.ym)
: currentYm();
// Validate ym format
const ym = req.query.ym ? String(req.query.ym) : currentYm();
if (!/^\d{4}-\d{2}$/.test(ym)) {
res.status(400).json({ error: 'ym must be in YYYY-MM format' });
return;
}
const volume = await getDomainVolumeForMonth(domain, ym);
res.json(volume);
res.json(await getDomainVolumeForMonth(domain, ym));
});
/**
* GET /api/billing/volume-overview?ym=2026-04
* Cross-domain overview for super admin. One DynamoDB scan, aggregates
* per domain. ym defaults to the current month.
*
* Returns:
* {
* ym: 'YYYY-MM',
* total_send_count, total_bounce_count, total_complaint_count, total_inbox_count,
* rows: [{ domain, send_count, bytes_total, bounce_count, complaint_count, inbox_count }, ...]
* }
*/
billingRouter.get('/volume-overview', async (req, res) => {
// Limit to current/previous month per product decision — no point in
// accepting arbitrary ym values from the client right now.
const ym = req.query.ym ? String(req.query.ym) : currentYm();
const allowed = new Set([currentYm(), previousYm()]);
if (!allowed.has(ym)) {
res.status(400).json({
error: `ym must be one of: ${[...allowed].join(', ')}`,
});
return;
}
res.json(await getVolumeOverview(ym));
});