feat(cost-guard): global Gemini daily budget + EOC alarm; trial requires verified email
This commit is contained in:
@@ -64,12 +64,6 @@ export async function authRoutes(app: FastifyInstance) {
|
||||
updatedAt: consentNow,
|
||||
});
|
||||
|
||||
// 7 dagars trial utan kort startar direkt (spec §46)
|
||||
const now = new Date();
|
||||
await app.db
|
||||
.insert(schema.trials)
|
||||
.values({ userId: user.id, startedAt: now, endsAt: trialEndsAt(now) });
|
||||
|
||||
await audit(app.db, {
|
||||
actorUserId: user.id,
|
||||
action: "auth.register",
|
||||
@@ -235,6 +229,20 @@ export async function authRoutes(app: FastifyInstance) {
|
||||
.update(schema.users)
|
||||
.set({ emailVerifiedAt: new Date(), updatedAt: new Date() })
|
||||
.where(eq(schema.users.id, stored.userId));
|
||||
|
||||
// Trial startas först när e-posten är verifierad. En trial per verifierad e-post.
|
||||
const [existingTrial] = await app.db
|
||||
.select({ id: schema.trials.id })
|
||||
.from(schema.trials)
|
||||
.where(eq(schema.trials.userId, stored.userId))
|
||||
.limit(1);
|
||||
if (!existingTrial) {
|
||||
const now = new Date();
|
||||
await app.db
|
||||
.insert(schema.trials)
|
||||
.values({ userId: stored.userId, startedAt: now, endsAt: trialEndsAt(now) });
|
||||
}
|
||||
|
||||
await audit(app.db, { actorUserId: stored.userId, action: "auth.email_verified", ip: req.ip });
|
||||
return reply.send({ ok: true });
|
||||
});
|
||||
|
||||
@@ -33,6 +33,10 @@ class RedisBudgetStore implements BudgetStore {
|
||||
private readonly key: string,
|
||||
) {}
|
||||
|
||||
private alarmKey(): string {
|
||||
return this.key.replace(/^gemini:daily:budget:/, "gemini:daily:budget:alarm:");
|
||||
}
|
||||
|
||||
async getDailySpendUsd(): Promise<number> {
|
||||
const val = await this.redis.get(this.key);
|
||||
return val ? Number(val) : 0;
|
||||
@@ -46,6 +50,11 @@ class RedisBudgetStore implements BudgetStore {
|
||||
return Number(newVal);
|
||||
}
|
||||
|
||||
async raiseBudgetAlarm(): Promise<void> {
|
||||
const ttl = this.secondsUntilMidnightUtc();
|
||||
await this.redis.set(this.alarmKey(), "1", "EX", ttl);
|
||||
}
|
||||
|
||||
private secondsUntilMidnightUtc(): number {
|
||||
const now = new Date();
|
||||
const midnight = new Date(
|
||||
|
||||
@@ -102,6 +102,9 @@ function mapPayloadForEoc(parsed: Record<string, unknown>): Record<string, unkno
|
||||
if (tile.label === "Allergen-brott" && allergenBreach) {
|
||||
tone = "crit";
|
||||
}
|
||||
if (tile.label === "AI-budget" && tone === "crit") {
|
||||
// already crit from buildWall
|
||||
}
|
||||
return { ...tile, tone };
|
||||
}),
|
||||
})),
|
||||
|
||||
Reference in New Issue
Block a user