/opt/canhelp/apps/api/src/lib
Edit: /opt/canhelp/apps/api/src/lib/daily-limits.ts (4282B)
import { eq, and, sql } from 'drizzle-orm'
import { db } from '../db.js'
import { dailyUsage, users, plans } from '@canhelp/db'
import type { PlanRow } from '../middleware/plan.js'
/** Get today's date in YYYY-MM-DD (Europe/Athens timezone) */
function todayDate(): string {
return new Date().toLocaleDateString('sv-SE', { timeZone: 'Europe/Athens' })
}
/** Get or create today's usage record for a user */
async function getOrCreateUsage(userId: string) {
const date = todayDate()
const [existing] = await db
.select()
.from(dailyUsage)
.where(and(eq(dailyUsage.userId, userId), eq(dailyUsage.date, date)))
if (existing) return existing
const [created] = await db
.insert(dailyUsage)
.values({ userId, date, messagesSent: 0, ordersMade: 0 })
.onConflictDoNothing()
.returning()
// Race condition: another request created it first
if (!created) {
const [retry] = await db
.select()
.from(dailyUsage)
.where(and(eq(dailyUsage.userId, userId), eq(dailyUsage.date, date)))
return retry!
}
return created
}
/** Fetch the user's plan from DB */
async function getUserPlan(userId: string): Promise
{
const [user] = await db
.select({ planId: users.planId })
.from(users)
.where(eq(users.id, userId))
if (!user?.planId) return null
const [plan] = await db.select().from(plans).where(eq(plans.id, user.planId))
return plan ?? null
}
export interface LimitCheckResult {
allowed: boolean
current: number
limit: number | null // null = unlimited
remaining: number | null // null = unlimited
}
/**
* Check if user can send a message today.
* Returns { allowed, current, limit, remaining }
*/
export async function checkMessageLimit(userId: string, plan?: PlanRow | null): Promise {
const userPlan = plan ?? (await getUserPlan(userId))
const limit = userPlan?.maxMessagesPerDay ?? null
if (limit === null) {
return { allowed: true, current: 0, limit: null, remaining: null }
}
const usage = await getOrCreateUsage(userId)
const current = usage.messagesSent
const remaining = Math.max(0, limit - current)
return { allowed: current < limit, current, limit, remaining }
}
/**
* Check if user can create an order/offer today.
* Returns { allowed, current, limit, remaining }
*/
export async function checkOrderLimit(userId: string, plan?: PlanRow | null): Promise {
const userPlan = plan ?? (await getUserPlan(userId))
const limit = userPlan?.maxOrdersPerDay ?? null
if (limit === null) {
return { allowed: true, current: 0, limit: null, remaining: null }
}
const usage = await getOrCreateUsage(userId)
const current = usage.ordersMade
const remaining = Math.max(0, limit - current)
return { allowed: current < limit, current, limit, remaining }
}
/** Increment message counter for today */
export async function incrementMessages(userId: string): Promise {
const date = todayDate()
// Ensure record exists
await getOrCreateUsage(userId)
// Atomically increment
await db
.update(dailyUsage)
.set({ messagesSent: sql`${dailyUsage.messagesSent} + 1` })
.where(and(eq(dailyUsage.userId, userId), eq(dailyUsage.date, date)))
}
/** Increment order counter for today */
export async function incrementOrders(userId: string): Promise {
const date = todayDate()
// Ensure record exists
await getOrCreateUsage(userId)
// Atomically increment
await db
.update(dailyUsage)
.set({ ordersMade: sql`${dailyUsage.ordersMade} + 1` })
.where(and(eq(dailyUsage.userId, userId), eq(dailyUsage.date, date)))
}
/**
* Get daily usage summary for current user (for UI display).
*/
export async function getDailyUsageSummary(userId: string, plan?: PlanRow | null) {
const userPlan = plan ?? (await getUserPlan(userId))
const usage = await getOrCreateUsage(userId)
const msgLimit = userPlan?.maxMessagesPerDay ?? null
const orderLimit = userPlan?.maxOrdersPerDay ?? null
return {
messagesLeft: msgLimit !== null ? Math.max(0, msgLimit - usage.messagesSent) : null,
ordersLeft: orderLimit !== null ? Math.max(0, orderLimit - usage.ordersMade) : null,
messagesSent: usage.messagesSent,
ordersMade: usage.ordersMade,
}
}