feat: implementar trava automatica de limites dos planos (profissionais, servicos e agendamentos)
Build and Deploy (Gitea Actions) / build-and-deploy (push) Successful in 2m41s
Details
Build and Deploy (Gitea Actions) / build-and-deploy (push) Successful in 2m41s
Details
This commit is contained in:
parent
757029f80e
commit
cf4a838d9c
|
|
@ -146,7 +146,8 @@ export default function PublicBookingPage({ params }: { params: Promise<{ slug:
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
setBooked(true);
|
setBooked(true);
|
||||||
} else {
|
} else {
|
||||||
console.error('Falha ao agendar no banco.');
|
const errData = await res.json();
|
||||||
|
alert(errData.error || 'Erro ao realizar o agendamento.');
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { NextRequest, NextResponse } from 'next/server';
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
import { query } from '@/lib/db';
|
import { query } from '@/lib/db';
|
||||||
|
import { getTenantPlanLimits } from '@/lib/planLimits';
|
||||||
|
|
||||||
export async function GET(req: NextRequest) {
|
export async function GET(req: NextRequest) {
|
||||||
const { searchParams } = new URL(req.url);
|
const { searchParams } = new URL(req.url);
|
||||||
|
|
@ -67,6 +68,24 @@ export async function POST(req: NextRequest) {
|
||||||
if (tenantRes.rows.length === 0) return NextResponse.json({ error: 'Tenant not found' }, { status: 404 });
|
if (tenantRes.rows.length === 0) return NextResponse.json({ error: 'Tenant not found' }, { status: 404 });
|
||||||
const tenantId = tenantRes.rows[0].id;
|
const tenantId = tenantRes.rows[0].id;
|
||||||
|
|
||||||
|
// Valida limite de agendamentos mensais do plano
|
||||||
|
const limits = await getTenantPlanLimits(tenantId);
|
||||||
|
if (limits.max_appointments_month !== null) {
|
||||||
|
const countRes = await query(
|
||||||
|
`SELECT COUNT(*) FROM appointments
|
||||||
|
WHERE tenant_id = $1
|
||||||
|
AND date >= DATE_TRUNC('month', CURRENT_DATE)
|
||||||
|
AND date < DATE_TRUNC('month', CURRENT_DATE) + INTERVAL '1 month'`,
|
||||||
|
[tenantId]
|
||||||
|
);
|
||||||
|
const currentCount = parseInt(countRes.rows[0].count);
|
||||||
|
if (currentCount >= limits.max_appointments_month) {
|
||||||
|
return NextResponse.json({
|
||||||
|
error: `Limite de agendamentos mensais do seu plano (${limits.plan_name}: máximo de ${limits.max_appointments_month}) foi atingido neste mês. Faça o upgrade do seu plano para agendar mais.`
|
||||||
|
}, { status: 403 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const startTime = `${String(startHour).padStart(2, '0')}:${String(startMin).padStart(2, '0')}:00`;
|
const startTime = `${String(startHour).padStart(2, '0')}:${String(startMin).padStart(2, '0')}:00`;
|
||||||
|
|
||||||
// Calculate end time
|
// Calculate end time
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { NextRequest, NextResponse } from 'next/server';
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
import { query } from '@/lib/db';
|
import { query } from '@/lib/db';
|
||||||
|
import { getTenantPlanLimits } from '@/lib/planLimits';
|
||||||
|
|
||||||
export async function GET(req: NextRequest) {
|
export async function GET(req: NextRequest) {
|
||||||
const { searchParams } = new URL(req.url);
|
const { searchParams } = new URL(req.url);
|
||||||
|
|
@ -45,6 +46,21 @@ export async function POST(req: NextRequest) {
|
||||||
if (tenantRes.rows.length === 0) return NextResponse.json({ error: 'Tenant not found' }, { status: 404 });
|
if (tenantRes.rows.length === 0) return NextResponse.json({ error: 'Tenant not found' }, { status: 404 });
|
||||||
const tenantId = tenantRes.rows[0].id;
|
const tenantId = tenantRes.rows[0].id;
|
||||||
|
|
||||||
|
// Valida limite de serviços do plano
|
||||||
|
const limits = await getTenantPlanLimits(tenantId);
|
||||||
|
if (limits.max_services !== null) {
|
||||||
|
const countRes = await query(
|
||||||
|
`SELECT COUNT(*) FROM services WHERE tenant_id = $1 AND is_active = true`,
|
||||||
|
[tenantId]
|
||||||
|
);
|
||||||
|
const currentCount = parseInt(countRes.rows[0].count);
|
||||||
|
if (currentCount >= limits.max_services) {
|
||||||
|
return NextResponse.json({
|
||||||
|
error: `Limite de serviços do seu plano (${limits.plan_name}: máximo de ${limits.max_services}) foi atingido. Faça o upgrade do plano para cadastrar mais serviços.`
|
||||||
|
}, { status: 403 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Get or create category
|
// Get or create category
|
||||||
let categoryId = null;
|
let categoryId = null;
|
||||||
if (category) {
|
if (category) {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { NextRequest, NextResponse } from 'next/server';
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
import { query } from '@/lib/db';
|
import { query } from '@/lib/db';
|
||||||
|
import { getTenantPlanLimits } from '@/lib/planLimits';
|
||||||
import crypto from 'crypto';
|
import crypto from 'crypto';
|
||||||
|
|
||||||
export async function GET(req: NextRequest) {
|
export async function GET(req: NextRequest) {
|
||||||
|
|
@ -58,6 +59,21 @@ export async function POST(req: NextRequest) {
|
||||||
if (tenantRes.rows.length === 0) return NextResponse.json({ error: 'Tenant not found' }, { status: 404 });
|
if (tenantRes.rows.length === 0) return NextResponse.json({ error: 'Tenant not found' }, { status: 404 });
|
||||||
const tenantId = tenantRes.rows[0].id;
|
const tenantId = tenantRes.rows[0].id;
|
||||||
|
|
||||||
|
// Valida limite de profissionais do plano
|
||||||
|
const limits = await getTenantPlanLimits(tenantId);
|
||||||
|
if (limits.max_professionals !== null) {
|
||||||
|
const countRes = await query(
|
||||||
|
`SELECT COUNT(*) FROM users WHERE tenant_id = $1 AND role = 'professional' AND is_active = true`,
|
||||||
|
[tenantId]
|
||||||
|
);
|
||||||
|
const currentCount = parseInt(countRes.rows[0].count);
|
||||||
|
if (currentCount >= limits.max_professionals) {
|
||||||
|
return NextResponse.json({
|
||||||
|
error: `Limite de profissionais do seu plano (${limits.plan_name}: máximo de ${limits.max_professionals}) foi atingido. Faça o upgrade do plano para adicionar mais profissionais.`
|
||||||
|
}, { status: 403 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Use a random hash for password since they are created by admin
|
// Use a random hash for password since they are created by admin
|
||||||
const passwordHash = crypto.randomBytes(16).toString('hex');
|
const passwordHash = crypto.randomBytes(16).toString('hex');
|
||||||
const safeEmail = email || `${Date.now()}@agendapro.com`;
|
const safeEmail = email || `${Date.now()}@agendapro.com`;
|
||||||
|
|
|
||||||
|
|
@ -287,6 +287,9 @@ export default function Agenda() {
|
||||||
setNewPhone('');
|
setNewPhone('');
|
||||||
setNewEmail('');
|
setNewEmail('');
|
||||||
setNewNotes('');
|
setNewNotes('');
|
||||||
|
} else {
|
||||||
|
const errData = await res.json();
|
||||||
|
alert(errData.error || 'Erro ao realizar agendamento.');
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
|
|
|
||||||
|
|
@ -73,6 +73,9 @@ export default function Services() {
|
||||||
fetchServices();
|
fetchServices();
|
||||||
setShowModal(false);
|
setShowModal(false);
|
||||||
setEditingService(null);
|
setEditingService(null);
|
||||||
|
} else {
|
||||||
|
const errData = await res.json();
|
||||||
|
alert(errData.error || 'Erro ao salvar serviço.');
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
|
|
|
||||||
|
|
@ -69,6 +69,9 @@ export default function Team() {
|
||||||
fetchTeam();
|
fetchTeam();
|
||||||
setShowModal(false);
|
setShowModal(false);
|
||||||
setEditingProf(null);
|
setEditingProf(null);
|
||||||
|
} else {
|
||||||
|
const errData = await res.json();
|
||||||
|
alert(errData.error || 'Erro ao salvar profissional.');
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,48 @@
|
||||||
|
import { query } from '@/lib/db';
|
||||||
|
|
||||||
|
export async function getTenantPlanLimits(tenantId: string) {
|
||||||
|
try {
|
||||||
|
// Busca plano da assinatura ativa
|
||||||
|
const subRes = await query(`
|
||||||
|
SELECT p.name as plan_name, p.max_professionals, p.max_services, p.max_appointments_month
|
||||||
|
FROM subscriptions s
|
||||||
|
JOIN plans p ON s.plan_id = p.id
|
||||||
|
WHERE s.tenant_id = $1 AND s.status = 'active'
|
||||||
|
ORDER BY s.created_at DESC LIMIT 1
|
||||||
|
`, [tenantId]);
|
||||||
|
|
||||||
|
if (subRes.rows.length > 0) {
|
||||||
|
return {
|
||||||
|
plan_name: subRes.rows[0].plan_name || 'Personalizado',
|
||||||
|
max_professionals: subRes.rows[0].max_professionals !== null ? Number(subRes.rows[0].max_professionals) : null,
|
||||||
|
max_services: subRes.rows[0].max_services !== null ? Number(subRes.rows[0].max_services) : null,
|
||||||
|
max_appointments_month: subRes.rows[0].max_appointments_month !== null ? Number(subRes.rows[0].max_appointments_month) : null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Caso não haja assinatura ativa registrada, busca pelo plano Starter padrão
|
||||||
|
const planRes = await query(`
|
||||||
|
SELECT name as plan_name, max_professionals, max_services, max_appointments_month
|
||||||
|
FROM plans WHERE slug = 'starter' LIMIT 1
|
||||||
|
`);
|
||||||
|
|
||||||
|
if (planRes.rows.length > 0) {
|
||||||
|
return {
|
||||||
|
plan_name: planRes.rows[0].plan_name || 'Starter',
|
||||||
|
max_professionals: planRes.rows[0].max_professionals !== null ? Number(planRes.rows[0].max_professionals) : 1,
|
||||||
|
max_services: planRes.rows[0].max_services !== null ? Number(planRes.rows[0].max_services) : 10,
|
||||||
|
max_appointments_month: planRes.rows[0].max_appointments_month !== null ? Number(planRes.rows[0].max_appointments_month) : 200,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Erro ao buscar limites do plano:', err);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback seguro de plano Starter
|
||||||
|
return {
|
||||||
|
plan_name: 'Starter',
|
||||||
|
max_professionals: 1,
|
||||||
|
max_services: 10,
|
||||||
|
max_appointments_month: 200,
|
||||||
|
};
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue