/opt/canhelp/apps/api/src/routes
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
}