/opt/canhelp/apps/api/src/routes
NameSizeModeActions
admin.ts950570644editdlrm
auto-match.ts74060644editdlrm
auto-response.ts81110644editdlrm
availability.ts72940644editdlrm
categories.ts18670644editdlrm
chat.ts93770644editdlrm
dashboard.ts51420644editdlrm
favorites.ts44410644editdlrm
fomo.ts68440644editdlrm
google.ts48720644editdlrm
health.ts12140644editdlrm
i18n.ts1670860644editdlrm
locations.ts17810644editdlrm
notifications.ts28620644editdlrm
offers.ts100470644editdlrm
payments.ts211670644editdlrm
plans.ts13520644editdlrm
portfolio.ts66580644editdlrm
price-list.ts144170644editdlrm
reports.ts20070644editdlrm
reviews.ts27830644editdlrm
skill-suggestions.ts10350644editdlrm
specialist-cards.ts186410644editdlrm
stats.ts10930644editdlrm
statuses.ts37310644editdlrm
support.ts84520644editdlrm
tasks.ts300180644editdlrm
uploads.ts15260644editdlrm
users.ts371570644editdlrm
Edit: /opt/canhelp/apps/api/src/routes/auto-response.ts (8111B)
import { Hono } from 'hono' import { zValidator } from '@hono/zod-validator' import { z } from 'zod' import { eq, and, sql, inArray } from 'drizzle-orm' import { db } from '../db.js' import { autoResponseSettings, users, plans, offers, specialistCards, tasks, } from '@canhelp/db' import { requireAuth, type AuthVariables } from '../middleware/auth.js' import { withPlan } from '../middleware/plan.js' import { getHelperCapability } from '../lib/helper-capability.js' const app = new Hono<{ Variables: AuthVariables }>() // GET /auto-response — get current specialist's auto-response settings app.get('/', requireAuth, withPlan, async (c) => { const user = c.get('user') if (!(await getHelperCapability(user.id))) { return c.json({ error: 'Specialists only' }, 403) } const plan = c.get('plan' as never) as { hasAutoResponse: boolean } | null if (!plan?.hasAutoResponse) { return c.json({ error: 'Auto-response requires Ultimate plan', code: 'PLAN_UPGRADE_REQUIRED' }, 403) } const [settings] = await db .select() .from(autoResponseSettings) .where(eq(autoResponseSettings.specialistId, user.id)) if (!settings) { return c.json({ isEnabled: false, categories: null, locations: null, minBudget: null, maxBudget: null, defaultPrice: null, defaultMessage: null, maxAutoOffersPerDay: 10, }) } return c.json(settings) }) // PUT /auto-response — create or update auto-response settings const settingsSchema = z.object({ isEnabled: z.boolean(), categories: z.array(z.string()).nullable().optional(), locations: z.array(z.string()).nullable().optional(), minBudget: z.number().min(0).nullable().optional(), maxBudget: z.number().min(0).nullable().optional(), defaultPrice: z.number().min(0).nullable().optional(), defaultMessage: z.string().max(1000).nullable().optional(), maxAutoOffersPerDay: z.number().int().min(1).max(100).optional(), }) app.put('/', requireAuth, withPlan, zValidator('json', settingsSchema), async (c) => { const user = c.get('user') if (!(await getHelperCapability(user.id))) { return c.json({ error: 'Specialists only' }, 403) } const plan = c.get('plan' as never) as { hasAutoResponse: boolean } | null if (!plan?.hasAutoResponse) { return c.json({ error: 'Auto-response requires Ultimate plan', code: 'PLAN_UPGRADE_REQUIRED' }, 403) } const body = c.req.valid('json') const [existing] = await db .select({ id: autoResponseSettings.id }) .from(autoResponseSettings) .where(eq(autoResponseSettings.specialistId, user.id)) if (existing) { const [updated] = await db .update(autoResponseSettings) .set({ isEnabled: body.isEnabled, categories: body.categories ?? null, locations: body.locations ?? null, minBudget: body.minBudget?.toString() ?? null, maxBudget: body.maxBudget?.toString() ?? null, defaultPrice: body.defaultPrice?.toString() ?? null, defaultMessage: body.defaultMessage ?? null, maxAutoOffersPerDay: body.maxAutoOffersPerDay ?? 10, updatedAt: new Date(), }) .where(eq(autoResponseSettings.id, existing.id)) .returning() return c.json(updated) } const [created] = await db .insert(autoResponseSettings) .values({ specialistId: user.id, isEnabled: body.isEnabled, categories: body.categories ?? null, locations: body.locations ?? null, minBudget: body.minBudget?.toString() ?? null, maxBudget: body.maxBudget?.toString() ?? null, defaultPrice: body.defaultPrice?.toString() ?? null, defaultMessage: body.defaultMessage ?? null, maxAutoOffersPerDay: body.maxAutoOffersPerDay ?? 10, }) .returning() return c.json(created, 201) }) export default app // ─── Auto-Response Trigger (called when a new task is created) ──────────── /** Find matching auto-response specialists and create offers automatically */ export async function triggerAutoResponses(task: { id: string title: string category: string | null location: string | null budget: string | null customerId: string }): Promise { if (!task.category) return 0 const today = new Date().toLocaleDateString('sv-SE', { timeZone: 'Europe/Athens' }) // Find all enabled auto-response settings const allSettings = await db .select() .from(autoResponseSettings) .where(eq(autoResponseSettings.isEnabled, true)) if (!allSettings.length) return 0 // Filter: specialists whose plan still has hasAutoResponse const specialistIds = allSettings.map((s) => s.specialistId) const specPlans = await db .select({ id: users.id, planId: users.planId }) .from(users) .where(inArray(users.id, specialistIds)) const planIds = [...new Set(specPlans.map((u) => u.planId).filter(Boolean))] as string[] const planRows = planIds.length > 0 ? await db.select({ id: plans.id, hasAutoResponse: plans.hasAutoResponse }).from(plans).where(inArray(plans.id, planIds)) : [] const planMap = Object.fromEntries(planRows.map((p) => [p.id, p])) const specPlanMap = Object.fromEntries(specPlans.map((u) => [u.id, u.planId])) // Filter: specialist must have an active card matching the task's category const matchingCards = await db .select({ specialistId: specialistCards.specialistId }) .from(specialistCards) .where( and( eq(specialistCards.isActive, true), sql`${task.category} = ANY(${specialistCards.categories})`, ), ) const matchingSpecIds = new Set(matchingCards.map((c) => c.specialistId)) let created = 0 for (const settings of allSettings) { // Check plan allows auto-response const userPlanId = specPlanMap[settings.specialistId] if (!userPlanId || !planMap[userPlanId]?.hasAutoResponse) continue // Don't auto-respond to own tasks if (settings.specialistId === task.customerId) continue // Check specialist has matching card if (!matchingSpecIds.has(settings.specialistId)) continue // Check category filter if (settings.categories && settings.categories.length > 0) { if (!settings.categories.includes(task.category!)) continue } // Check location filter if (settings.locations && settings.locations.length > 0 && task.location) { if (!settings.locations.includes(task.location)) continue } // Check budget filter const taskBudget = task.budget ? Number(task.budget) : null if (settings.minBudget && taskBudget !== null && taskBudget < Number(settings.minBudget)) continue if (settings.maxBudget && taskBudget !== null && taskBudget > Number(settings.maxBudget)) continue // Check daily auto-offer limit (reset if new day) let todayOffers = settings.todayAutoOffers if (settings.todayDate !== today) { todayOffers = 0 await db .update(autoResponseSettings) .set({ todayAutoOffers: 0, todayDate: today }) .where(eq(autoResponseSettings.id, settings.id)) } if (todayOffers >= settings.maxAutoOffersPerDay) continue // Check if specialist already made an offer for this task const [existingOffer] = await db .select({ id: offers.id }) .from(offers) .where(and(eq(offers.taskId, task.id), eq(offers.specialistId, settings.specialistId))) if (existingOffer) continue // Create auto-offer const price = settings.defaultPrice ?? task.budget ?? '0' const message = settings.defaultMessage ?? 'Αυτόματη πρόταση — ενδιαφέρομαι για αυτή την εργασία!' await db.insert(offers).values({ taskId: task.id, specialistId: settings.specialistId, price: price.toString(), message, status: 'pending', }) // Increment daily counter await db .update(autoResponseSettings) .set({ todayAutoOffers: todayOffers + 1, todayDate: today, }) .where(eq(autoResponseSettings.id, settings.id)) created++ } return created }