feat: scheduled report digests + per-session capacity (M33)

POST /v1/internal/jobs/reports/dispatch emails each tenant owner a
trailing-window digest (bookings, revenue, per-service and per-staff
breakdown) localized to the tenant locale; zero-activity tenants are
skipped. Metrics cores extracted to tenant-scoped variants so the job
needs no principal.

PATCH /v1/catalog/class-sessions/:id sets a session's seat count with
a confirmed-bookings floor — shrinking below it needs allowOverCapacity,
matching the waitlist-promotion convention. Session rows gain an inline
capacity editor with an overbook confirm state.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
Tomas DvorakandDevin committed 2026-09-22 21:29:44 +02:00
1 parent e6f1ec38a4
commit e680e0c2b8
14 files changed
+808 -10

No files matched your search

+109 -1
View File
@@ -461,7 +461,8 @@ export interface paths {
delete: operations["deleteClassSession"];
options?: never;
head?: never;
patch?: never;
/** Patch a class session (currently capacity only) */
patch: operations["updateClassSession"];
trace?: never;
};
"/v1/catalog/class-sessions/{sessionID}/roster": {
@@ -816,6 +817,23 @@ export interface paths {
patch?: never;
trace?: never;
};
"/v1/internal/jobs/reports/dispatch": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
get?: never;
put?: never;
/** Email each tenant owner a trailing-window business digest */
post: operations["dispatchReportDigests"];
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/v1/internal/jobs/sms/invoices": {
parameters: {
query?: never;
@@ -1564,6 +1582,12 @@ export interface components {
sentCount: number;
skippedCount: number;
};
DispatchReportDigestsResponse: {
failedCount: number;
sentCount: number;
skippedCount: number;
tenantCount: number;
};
EmailTemplate: {
bodyText?: string;
isEnabled?: boolean;
@@ -1922,6 +1946,11 @@ export interface components {
/** @description Tenant-local start time, HH:MM */
startsLocal?: string;
};
UpdateClassSessionRequest: {
/** @description Permit shrinking below the confirmed seat count */
allowOverCapacity?: boolean;
capacity: number;
};
UpdateEmailTemplateRequest: {
body?: string;
isEnabled?: boolean;
@@ -3039,6 +3068,53 @@ export interface operations {
};
};
};
updateClassSession: {
parameters: {
query?: never;
header?: never;
path: {
sessionID: string;
};
cookie?: never;
};
requestBody: {
content: {
"application/json": components["schemas"]["UpdateClassSessionRequest"];
};
};
responses: {
/** @description Updated class session */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["ClassSession"];
};
};
/** @description Invalid request */
400: {
headers: {
[name: string]: unknown;
};
content?: never;
};
/** @description Class session not found */
404: {
headers: {
[name: string]: unknown;
};
content?: never;
};
/** @description Capacity below confirmed bookings (pass allowOverCapacity to force) */
409: {
headers: {
[name: string]: unknown;
};
content?: never;
};
};
};
getSessionRoster: {
parameters: {
query?: never;
@@ -3965,6 +4041,38 @@ export interface operations {
};
};
};
dispatchReportDigests: {
parameters: {
query?: {
/** @description Trailing window length in days */
days?: number;
/** @description Scope the run to a single tenant */
tenantId?: string;
};
header?: never;
path?: never;
cookie?: never;
};
requestBody?: never;
responses: {
/** @description Digests dispatched */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["DispatchReportDigestsResponse"];
};
};
/** @description Unauthorized */
401: {
headers: {
[name: string]: unknown;
};
content?: never;
};
};
};
generateSmsInvoices: {
parameters: {
query?: never;