Refactor to vertical slice
This commit is contained in:
@@ -0,0 +1,67 @@
|
||||
import { Errors } from '@/lib/errors';
|
||||
import { db } from '@/lib/prisma';
|
||||
import { type Result, err, ok } from '@/lib/result';
|
||||
import type {
|
||||
AcceptComplexInvitationsInput,
|
||||
AcceptComplexInvitationsResponse,
|
||||
} from '@repo/api-contract';
|
||||
import { hashToken, normalizeEmail } from '../../shared/members-helpers';
|
||||
|
||||
export async function acceptComplexInvitations(
|
||||
userId: string,
|
||||
email: string,
|
||||
input: AcceptComplexInvitationsInput = {}
|
||||
): Promise<Result<AcceptComplexInvitationsResponse>> {
|
||||
const normalizedEmail = normalizeEmail(email);
|
||||
const tokenHash = input.inviteToken ? hashToken(input.inviteToken) : null;
|
||||
|
||||
const invitations = await db.complexInvitation.findMany({
|
||||
where: {
|
||||
email: normalizedEmail,
|
||||
acceptedAt: null,
|
||||
revokedAt: null,
|
||||
expiresAt: { gte: new Date() },
|
||||
...(tokenHash ? { tokenHash } : {}),
|
||||
},
|
||||
orderBy: { createdAt: 'asc' },
|
||||
});
|
||||
|
||||
if (tokenHash && invitations.length === 0) {
|
||||
return err(Errors.notFound('La invitación es inválida o ya venció.'));
|
||||
}
|
||||
|
||||
let acceptedCount = 0;
|
||||
|
||||
for (const invitation of invitations) {
|
||||
await db.$transaction(async (tx) => {
|
||||
const existingMembership = await tx.complexUser.findUnique({
|
||||
where: {
|
||||
complexId_userId: {
|
||||
complexId: invitation.complexId,
|
||||
userId,
|
||||
},
|
||||
},
|
||||
select: { userId: true },
|
||||
});
|
||||
|
||||
if (!existingMembership) {
|
||||
await tx.complexUser.create({
|
||||
data: {
|
||||
complexId: invitation.complexId,
|
||||
userId,
|
||||
role: 'EMPLOYEE',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
await tx.complexInvitation.update({
|
||||
where: { id: invitation.id },
|
||||
data: { acceptedAt: new Date() },
|
||||
});
|
||||
});
|
||||
|
||||
acceptedCount += 1;
|
||||
}
|
||||
|
||||
return ok({ acceptedCount });
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { handleResult } from '@/lib/http/handle-result';
|
||||
import type { AppContext } from '@/types/hono';
|
||||
import type { AcceptComplexInvitationsInput } from '@repo/api-contract';
|
||||
import { acceptComplexInvitations } from './accept-complex-invitations.business';
|
||||
|
||||
export async function acceptComplexInvitationsHandler(c: AppContext) {
|
||||
const user = c.get('user');
|
||||
const payload = c.req.valid('json' as never) as AcceptComplexInvitationsInput;
|
||||
|
||||
return handleResult(c, await acceptComplexInvitations(user.id, user.email, payload));
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import { Errors } from '@/lib/errors';
|
||||
import { db } from '@/lib/prisma';
|
||||
import { type Result, err, ok } from '@/lib/result';
|
||||
import type { CancelComplexInvitationResponse } from '@repo/api-contract';
|
||||
import { ensureComplexAdmin } from '../../shared/members-helpers';
|
||||
|
||||
export async function cancelComplexInvitation(
|
||||
userId: string,
|
||||
complexId: string,
|
||||
invitationId: string
|
||||
): Promise<Result<CancelComplexInvitationResponse>> {
|
||||
const adminResult = await ensureComplexAdmin(userId, complexId);
|
||||
if (!adminResult.ok) return adminResult;
|
||||
|
||||
const invitation = await db.complexInvitation.findUnique({
|
||||
where: { id: invitationId },
|
||||
select: { complexId: true, acceptedAt: true, revokedAt: true },
|
||||
});
|
||||
|
||||
if (!invitation || invitation.complexId !== complexId) {
|
||||
return err(Errors.notFound('La invitación no existe.'));
|
||||
}
|
||||
|
||||
if (invitation.acceptedAt) {
|
||||
return err(Errors.conflict('La invitación ya fue aceptada.'));
|
||||
}
|
||||
|
||||
if (invitation.revokedAt) {
|
||||
return ok({ ok: true });
|
||||
}
|
||||
|
||||
await db.complexInvitation.update({
|
||||
where: { id: invitationId },
|
||||
data: { revokedAt: new Date() },
|
||||
});
|
||||
|
||||
return ok({ ok: true });
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { handleResult } from '@/lib/http/handle-result';
|
||||
import type { AppContext } from '@/types/hono';
|
||||
import { z } from 'zod';
|
||||
import { cancelComplexInvitation } from './cancel-complex-invitation.business';
|
||||
|
||||
const cancelInvitationParamsSchema = z.object({ id: z.uuid(), invitationId: z.uuid() });
|
||||
|
||||
export async function cancelComplexInvitationHandler(c: AppContext) {
|
||||
const user = c.get('user');
|
||||
const params = cancelInvitationParamsSchema.parse(c.req.param());
|
||||
|
||||
return handleResult(c, await cancelComplexInvitation(user.id, params.id, params.invitationId));
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
import { db } from '@/lib/prisma';
|
||||
import { buildUniqueSlug, slugify } from '@/lib/slug';
|
||||
import { ensureActiveSport } from '@/modules/court/shared/guards';
|
||||
import type { CreateComplexInput } from '@repo/api-contract';
|
||||
import { v7 as uuidv7 } from 'uuid';
|
||||
|
||||
type DayOfWeek = 'MONDAY' | 'TUESDAY' | 'WEDNESDAY' | 'THURSDAY' | 'FRIDAY' | 'SATURDAY' | 'SUNDAY';
|
||||
|
||||
type CreateComplexInternalInput = CreateComplexInput & {
|
||||
adminEmail: string;
|
||||
userId?: string;
|
||||
};
|
||||
|
||||
function toMinutes(value: string): number {
|
||||
const [hours, minutes] = value.split(':').map((part) => Number(part));
|
||||
return hours * 60 + minutes;
|
||||
}
|
||||
|
||||
function assertAvailabilityRanges(
|
||||
availability: Array<{
|
||||
dayOfWeek: DayOfWeek;
|
||||
startTime: string;
|
||||
endTime: string;
|
||||
}>
|
||||
) {
|
||||
for (const range of availability) {
|
||||
const start = toMinutes(range.startTime);
|
||||
const end = toMinutes(range.endTime);
|
||||
if (start >= end) {
|
||||
throw new Error(`El rango ${range.startTime}-${range.endTime} es inválido.`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function createComplex(input: CreateComplexInternalInput) {
|
||||
return db.$transaction(async (tx) => {
|
||||
const base = slugify(input.complexName);
|
||||
const fallback = base.length > 0 ? base : `complex-${uuidv7().slice(0, 8)}`;
|
||||
const complexSlug = await buildUniqueSlug(fallback, (slug) =>
|
||||
tx.complex.findFirst({ where: { complexSlug: slug }, select: { id: true } })
|
||||
);
|
||||
|
||||
const complex = await tx.complex.create({
|
||||
data: {
|
||||
id: uuidv7(),
|
||||
complexName: input.complexName,
|
||||
physicalAddress: input.physicalAddress.trim(),
|
||||
city: input.city?.trim() || null,
|
||||
state: input.state?.trim() || null,
|
||||
country: input.country?.trim() || null,
|
||||
complexSlug,
|
||||
adminEmail: input.adminEmail,
|
||||
planCode: input.planCode,
|
||||
},
|
||||
});
|
||||
|
||||
if (input.userId) {
|
||||
await tx.complexUser.create({
|
||||
data: {
|
||||
complexId: complex.id,
|
||||
userId: input.userId,
|
||||
role: 'ADMIN',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (input.setupCourts && input.courtSportId) {
|
||||
const sport = await ensureActiveSport(input.courtSportId);
|
||||
const availability = (input.courtDaysOfWeek ?? []).map((day) => ({
|
||||
dayOfWeek: day as DayOfWeek,
|
||||
startTime: input.courtStartTime ?? '08:00',
|
||||
endTime: input.courtEndTime ?? '22:00',
|
||||
}));
|
||||
|
||||
assertAvailabilityRanges(availability);
|
||||
|
||||
const court = await tx.court.create({
|
||||
data: {
|
||||
id: uuidv7(),
|
||||
complexId: complex.id,
|
||||
sportId: input.courtSportId,
|
||||
name: `${sport.name} 1`,
|
||||
slotDurationMinutes: 60,
|
||||
basePrice: 0,
|
||||
},
|
||||
});
|
||||
|
||||
await tx.courtAvailability.createMany({
|
||||
data: availability.map((avail) => ({
|
||||
id: uuidv7(),
|
||||
courtId: court.id,
|
||||
dayOfWeek: avail.dayOfWeek,
|
||||
startTime: avail.startTime,
|
||||
endTime: avail.endTime,
|
||||
})),
|
||||
});
|
||||
}
|
||||
|
||||
return complex;
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
import { db } from '@/lib/prisma';
|
||||
import type { AppContext } from '@/types/hono';
|
||||
import type { CreateComplexInput } from '@repo/api-contract';
|
||||
import { createComplex } from './create-complex.business';
|
||||
|
||||
export async function createComplexHandler(c: AppContext) {
|
||||
const payload = c.req.valid('json' as never) as CreateComplexInput;
|
||||
|
||||
const user = c.get('user');
|
||||
let adminEmail = user?.email;
|
||||
let userId = user?.id;
|
||||
|
||||
if (!adminEmail) {
|
||||
const body = await c.req.json().catch(() => ({}));
|
||||
adminEmail = body.adminEmail;
|
||||
userId = body.userId;
|
||||
}
|
||||
|
||||
if (!adminEmail) {
|
||||
return c.json({ message: 'Se requiere email del administrador.' }, 400);
|
||||
}
|
||||
|
||||
if (!userId && adminEmail) {
|
||||
const existingUser = await db.user.findUnique({
|
||||
where: { email: adminEmail },
|
||||
select: { id: true },
|
||||
});
|
||||
if (!existingUser) {
|
||||
return c.json({ message: 'El usuario no existe.' }, 404);
|
||||
}
|
||||
userId = existingUser.id;
|
||||
}
|
||||
|
||||
const complex = await createComplex({
|
||||
complexName: payload.complexName,
|
||||
physicalAddress: payload.physicalAddress,
|
||||
adminEmail,
|
||||
userId,
|
||||
planCode: payload.planCode,
|
||||
city: payload.city,
|
||||
state: payload.state,
|
||||
country: payload.country,
|
||||
setupCourts: payload.setupCourts,
|
||||
courtSportId: payload.courtSportId,
|
||||
courtStartTime: payload.courtStartTime,
|
||||
courtEndTime: payload.courtEndTime,
|
||||
courtDaysOfWeek: payload.courtDaysOfWeek,
|
||||
});
|
||||
|
||||
return c.json(complex, 201);
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import { db } from '@/lib/prisma';
|
||||
|
||||
export async function getComplexById(id: string) {
|
||||
return db.complex.findUnique({ where: { id } });
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import type { AppContext } from '@/types/hono';
|
||||
import { getComplexById } from './get-complex-by-id.business';
|
||||
|
||||
type ComplexIdParams = { id: string };
|
||||
|
||||
export async function getComplexByIdHandler(c: AppContext) {
|
||||
const { id } = c.req.valid('param' as never) as ComplexIdParams;
|
||||
|
||||
const complex = await getComplexById(id);
|
||||
|
||||
if (!complex) {
|
||||
return c.json({ message: 'Complejo no encontrado.' }, 404);
|
||||
}
|
||||
|
||||
return c.json(complex);
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import { db } from '@/lib/prisma';
|
||||
|
||||
export async function getComplexBySlug(slug: string) {
|
||||
return db.complex.findUnique({ where: { complexSlug: slug } });
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import type { AppContext } from '@/types/hono';
|
||||
import { getComplexBySlug } from './get-complex-by-slug.business';
|
||||
|
||||
type ComplexSlugParams = { slug: string };
|
||||
|
||||
export async function getComplexBySlugHandler(c: AppContext) {
|
||||
const { slug } = c.req.valid('param' as never) as ComplexSlugParams;
|
||||
|
||||
const complex = await getComplexBySlug(slug);
|
||||
|
||||
if (!complex) {
|
||||
return c.json({ message: 'Complejo no encontrado.' }, 404);
|
||||
}
|
||||
|
||||
return c.json(complex);
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import { db } from '@/lib/prisma';
|
||||
import { parsePlanRules } from '@/modules/plan/services/plan-rules.service';
|
||||
|
||||
export async function getCurrentComplex(userId: string, complexId: string) {
|
||||
const complexUser = await db.complexUser.findUnique({
|
||||
where: {
|
||||
complexId_userId: {
|
||||
complexId,
|
||||
userId,
|
||||
},
|
||||
},
|
||||
include: {
|
||||
complex: {
|
||||
include: {
|
||||
plan: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!complexUser) return null;
|
||||
|
||||
return {
|
||||
...complexUser.complex,
|
||||
role: complexUser.role,
|
||||
planFeatures: complexUser.complex.plan
|
||||
? parsePlanRules(complexUser.complex.plan.rules).features
|
||||
: null,
|
||||
};
|
||||
}
|
||||
|
||||
export async function selectComplex(userId: string, complexId: string) {
|
||||
return getCurrentComplex(userId, complexId);
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import type { AppContext } from '@/types/hono';
|
||||
import { getCookie } from 'hono/cookie';
|
||||
import { getCurrentComplex } from './get-current-complex.business';
|
||||
|
||||
const SELECTED_COMPLEX_COOKIE = 'selected-complex-id';
|
||||
|
||||
export async function getCurrentComplexHandler(c: AppContext) {
|
||||
const user = c.get('user');
|
||||
const complexId = getCookie(c, SELECTED_COMPLEX_COOKIE);
|
||||
|
||||
if (!complexId) {
|
||||
return c.json(null);
|
||||
}
|
||||
|
||||
const complex = await getCurrentComplex(user.id, complexId);
|
||||
|
||||
if (!complex) {
|
||||
return c.json(null);
|
||||
}
|
||||
|
||||
return c.json(complex);
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
import { Errors } from '@/lib/errors';
|
||||
import { db } from '@/lib/prisma';
|
||||
import { type Result, err, ok } from '@/lib/result';
|
||||
import type { InviteComplexUserInput, InviteComplexUserResponse } from '@repo/api-contract';
|
||||
import { v7 as uuidv7 } from 'uuid';
|
||||
import {
|
||||
createInvitationToken,
|
||||
ensureComplexAdmin,
|
||||
formatInvitation,
|
||||
hashToken,
|
||||
normalizeEmail,
|
||||
nowPlusDays,
|
||||
sendInvitationEmail,
|
||||
} from '../../shared/members-helpers';
|
||||
|
||||
export async function inviteComplexUser(
|
||||
userId: string,
|
||||
complexId: string,
|
||||
input: InviteComplexUserInput
|
||||
): Promise<Result<InviteComplexUserResponse>> {
|
||||
const adminResult = await ensureComplexAdmin(userId, complexId);
|
||||
if (!adminResult.ok) return adminResult;
|
||||
|
||||
const email = normalizeEmail(input.email);
|
||||
const complex = await db.complex.findUnique({
|
||||
where: { id: complexId },
|
||||
select: { complexName: true },
|
||||
});
|
||||
|
||||
if (!complex) {
|
||||
return err(Errors.notFound('El complejo no existe.'));
|
||||
}
|
||||
|
||||
const existingUser = await db.user.findUnique({
|
||||
where: { email },
|
||||
select: { id: true },
|
||||
});
|
||||
|
||||
if (existingUser) {
|
||||
const existingMembership = await db.complexUser.findUnique({
|
||||
where: {
|
||||
complexId_userId: {
|
||||
complexId,
|
||||
userId: existingUser.id,
|
||||
},
|
||||
},
|
||||
select: { userId: true },
|
||||
});
|
||||
|
||||
if (existingMembership) {
|
||||
return err(Errors.conflict('Ese usuario ya pertenece al complejo.'));
|
||||
}
|
||||
}
|
||||
|
||||
const token = createInvitationToken();
|
||||
|
||||
const invitation = await db.$transaction(async (tx) => {
|
||||
const pendingInvitation = await tx.complexInvitation.findFirst({
|
||||
where: {
|
||||
complexId,
|
||||
email,
|
||||
acceptedAt: null,
|
||||
revokedAt: null,
|
||||
expiresAt: { gte: new Date() },
|
||||
},
|
||||
orderBy: { createdAt: 'desc' },
|
||||
});
|
||||
|
||||
if (pendingInvitation) {
|
||||
return tx.complexInvitation.update({
|
||||
where: { id: pendingInvitation.id },
|
||||
data: {
|
||||
tokenHash: hashToken(token),
|
||||
expiresAt: nowPlusDays(Number(Bun.env.COMPLEX_INVITATION_TTL_DAYS ?? 7)),
|
||||
acceptedAt: null,
|
||||
revokedAt: null,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return tx.complexInvitation.create({
|
||||
data: {
|
||||
id: uuidv7(),
|
||||
complexId,
|
||||
email,
|
||||
tokenHash: hashToken(token),
|
||||
expiresAt: nowPlusDays(Number(Bun.env.COMPLEX_INVITATION_TTL_DAYS ?? 7)),
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
await sendInvitationEmail({
|
||||
complexName: complex.complexName,
|
||||
inviteeEmail: email,
|
||||
token,
|
||||
});
|
||||
|
||||
return ok({ invitation: formatInvitation(invitation) });
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { handleResult } from '@/lib/http/handle-result';
|
||||
import type { AppContext } from '@/types/hono';
|
||||
import type { InviteComplexUserInput } from '@repo/api-contract';
|
||||
import { z } from 'zod';
|
||||
import { inviteComplexUser as inviteComplexUserBusiness } from './invite-complex-user.business';
|
||||
|
||||
const complexParamsSchema = z.object({ id: z.uuid() });
|
||||
|
||||
type Deps = {
|
||||
inviteComplexUser: typeof inviteComplexUserBusiness;
|
||||
};
|
||||
|
||||
export function createInviteComplexUserHandler(
|
||||
deps: Deps = { inviteComplexUser: inviteComplexUserBusiness }
|
||||
) {
|
||||
return async function inviteComplexUserHandler(c: AppContext) {
|
||||
const user = c.get('user');
|
||||
const params = complexParamsSchema.parse(c.req.param());
|
||||
const payload = c.req.valid('json' as never) as InviteComplexUserInput;
|
||||
|
||||
return handleResult(c, await deps.inviteComplexUser(user.id, params.id, payload), 201);
|
||||
};
|
||||
}
|
||||
|
||||
export const inviteComplexUserHandler = createInviteComplexUserHandler();
|
||||
@@ -0,0 +1,44 @@
|
||||
import { resolveAvatarUrl } from '@/lib/avatar';
|
||||
import { db } from '@/lib/prisma';
|
||||
import { ensureComplexMember, formatInvitation } from '../../shared/members-helpers';
|
||||
|
||||
export async function listComplexUsers(complexId: string) {
|
||||
const [members, invitations] = await Promise.all([
|
||||
db.complexUser.findMany({
|
||||
where: { complexId },
|
||||
include: { user: true },
|
||||
orderBy: { createdAt: 'asc' },
|
||||
}),
|
||||
db.complexInvitation.findMany({
|
||||
where: {
|
||||
complexId,
|
||||
acceptedAt: null,
|
||||
revokedAt: null,
|
||||
expiresAt: { gte: new Date() },
|
||||
},
|
||||
orderBy: { createdAt: 'desc' },
|
||||
}),
|
||||
]);
|
||||
|
||||
const membersWithAvatars = members.map((member) => ({
|
||||
userId: member.userId,
|
||||
fullName: member.user.name,
|
||||
email: member.user.email,
|
||||
avatarUrl: resolveAvatarUrl({
|
||||
email: member.user.email,
|
||||
image: member.user.image,
|
||||
}),
|
||||
role: member.role,
|
||||
createdAt: member.createdAt.toISOString(),
|
||||
}));
|
||||
|
||||
return {
|
||||
members: membersWithAvatars,
|
||||
invitations: invitations.map(formatInvitation),
|
||||
};
|
||||
}
|
||||
|
||||
export async function getComplexUsersOverview(userId: string, complexId: string) {
|
||||
await ensureComplexMember(userId, complexId);
|
||||
return listComplexUsers(complexId);
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import type { AppContext } from '@/types/hono';
|
||||
import { z } from 'zod';
|
||||
import { getComplexUsersOverview } from './list-complex-users.business';
|
||||
|
||||
const complexParamsSchema = z.object({ id: z.uuid() });
|
||||
|
||||
export async function listComplexUsersHandler(c: AppContext) {
|
||||
const user = c.get('user');
|
||||
const params = complexParamsSchema.parse(c.req.param());
|
||||
const result = await getComplexUsersOverview(user.id, params.id);
|
||||
return c.json(result);
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import { db } from '@/lib/prisma';
|
||||
import { parsePlanRules } from '@/modules/plan/services/plan-rules.service';
|
||||
|
||||
export async function listMyComplexes(userId: string) {
|
||||
const complexUsers = await db.complexUser.findMany({
|
||||
where: { userId },
|
||||
include: {
|
||||
complex: {
|
||||
include: {
|
||||
plan: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
orderBy: { createdAt: 'asc' },
|
||||
});
|
||||
|
||||
return complexUsers.map((complexUser) => ({
|
||||
...complexUser.complex,
|
||||
role: complexUser.role,
|
||||
planFeatures: complexUser.complex.plan
|
||||
? parsePlanRules(complexUser.complex.plan.rules).features
|
||||
: null,
|
||||
}));
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import type { AppContext } from '@/types/hono';
|
||||
import { listMyComplexes } from './list-my-complexes.business';
|
||||
|
||||
export async function listMyComplexesHandler(c: AppContext) {
|
||||
const user = c.get('user');
|
||||
const complexes = await listMyComplexes(user.id);
|
||||
return c.json(complexes);
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
import { Errors } from '@/lib/errors';
|
||||
import { db } from '@/lib/prisma';
|
||||
import { type Result, err, ok } from '@/lib/result';
|
||||
import type { InviteComplexUserResponse } from '@repo/api-contract';
|
||||
import {
|
||||
ensureComplexAdmin,
|
||||
formatInvitation,
|
||||
refreshPendingInvitation,
|
||||
sendInvitationEmail,
|
||||
} from '../../shared/members-helpers';
|
||||
|
||||
export async function resendComplexInvitation(
|
||||
userId: string,
|
||||
complexId: string,
|
||||
invitationId: string
|
||||
): Promise<Result<InviteComplexUserResponse>> {
|
||||
const adminResult = await ensureComplexAdmin(userId, complexId);
|
||||
if (!adminResult.ok) return adminResult;
|
||||
|
||||
const pendingInvitation = await db.complexInvitation.findUnique({
|
||||
where: { id: invitationId },
|
||||
include: {
|
||||
complex: { select: { complexName: true } },
|
||||
},
|
||||
});
|
||||
|
||||
if (!pendingInvitation || pendingInvitation.complexId !== complexId) {
|
||||
return err(Errors.notFound('La invitación no existe.'));
|
||||
}
|
||||
|
||||
if (pendingInvitation.acceptedAt) {
|
||||
return err(Errors.conflict('La invitación ya fue aceptada.'));
|
||||
}
|
||||
|
||||
if (pendingInvitation.revokedAt) {
|
||||
return err(Errors.conflict('La invitación fue cancelada.'));
|
||||
}
|
||||
|
||||
const refreshed = await refreshPendingInvitation(invitationId);
|
||||
|
||||
await sendInvitationEmail({
|
||||
complexName: refreshed.invitation.complex.complexName,
|
||||
inviteeEmail: pendingInvitation.email,
|
||||
token: refreshed.token,
|
||||
});
|
||||
|
||||
return ok({
|
||||
invitation: formatInvitation({
|
||||
id: refreshed.invitation.id,
|
||||
complexId: refreshed.invitation.complexId,
|
||||
email: refreshed.invitation.email,
|
||||
expiresAt: refreshed.invitation.expiresAt,
|
||||
acceptedAt: refreshed.invitation.acceptedAt,
|
||||
revokedAt: refreshed.invitation.revokedAt,
|
||||
createdAt: refreshed.invitation.createdAt,
|
||||
}),
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { handleResult } from '@/lib/http/handle-result';
|
||||
import type { AppContext } from '@/types/hono';
|
||||
import { z } from 'zod';
|
||||
import { resendComplexInvitation } from './resend-complex-invitation.business';
|
||||
|
||||
const resendInvitationParamsSchema = z.object({ id: z.uuid(), invitationId: z.uuid() });
|
||||
|
||||
export async function resendComplexInvitationHandler(c: AppContext) {
|
||||
const user = c.get('user');
|
||||
const params = resendInvitationParamsSchema.parse(c.req.param());
|
||||
|
||||
return handleResult(c, await resendComplexInvitation(user.id, params.id, params.invitationId));
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import { Errors } from '@/lib/errors';
|
||||
import { db } from '@/lib/prisma';
|
||||
import { type Result, err, ok } from '@/lib/result';
|
||||
import type { RevokeComplexUserInput } from '@repo/api-contract';
|
||||
import { ensureComplexAdmin } from '../../shared/members-helpers';
|
||||
|
||||
export async function revokeComplexUser(
|
||||
userId: string,
|
||||
complexId: string,
|
||||
input: RevokeComplexUserInput
|
||||
): Promise<Result<{ ok: boolean }>> {
|
||||
const adminResult = await ensureComplexAdmin(userId, complexId);
|
||||
if (!adminResult.ok) return adminResult;
|
||||
|
||||
const membership = await db.complexUser.findUnique({
|
||||
where: {
|
||||
complexId_userId: {
|
||||
complexId,
|
||||
userId: input.userId,
|
||||
},
|
||||
},
|
||||
select: { role: true },
|
||||
});
|
||||
|
||||
if (!membership) {
|
||||
return err(Errors.notFound('Ese usuario no pertenece al complejo.'));
|
||||
}
|
||||
|
||||
if (membership.role !== 'EMPLOYEE') {
|
||||
return err(Errors.conflict('Solo podés revocar usuarios con rol EMPLOYEE.'));
|
||||
}
|
||||
|
||||
await db.complexUser.delete({
|
||||
where: {
|
||||
complexId_userId: {
|
||||
complexId,
|
||||
userId: input.userId,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return ok({ ok: true });
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { handleResult } from '@/lib/http/handle-result';
|
||||
import type { AppContext } from '@/types/hono';
|
||||
import { z } from 'zod';
|
||||
import { revokeComplexUser } from './revoke-complex-user.business';
|
||||
|
||||
const revokeParamsSchema = z.object({ id: z.uuid(), userId: z.string().min(1) });
|
||||
|
||||
export async function revokeComplexUserHandler(c: AppContext) {
|
||||
const user = c.get('user');
|
||||
const params = revokeParamsSchema.parse(c.req.param());
|
||||
|
||||
return handleResult(c, await revokeComplexUser(user.id, params.id, { userId: params.userId }));
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import {
|
||||
getCurrentComplex,
|
||||
selectComplex,
|
||||
} from '@/modules/complex/features/get-current-complex/get-current-complex.business';
|
||||
import type { AppContext } from '@/types/hono';
|
||||
import { setCookie } from 'hono/cookie';
|
||||
import { z } from 'zod';
|
||||
|
||||
const SELECTED_COMPLEX_COOKIE = 'selected-complex-id';
|
||||
const COOKIE_MAX_AGE = 60 * 60 * 24 * 30;
|
||||
|
||||
const selectComplexSchema = z.object({
|
||||
complexId: z.string().uuid(),
|
||||
});
|
||||
|
||||
export async function selectComplexHandler(c: AppContext) {
|
||||
const user = c.get('user');
|
||||
const body = await c.req.json();
|
||||
const result = selectComplexSchema.safeParse(body);
|
||||
|
||||
if (!result.success) {
|
||||
return c.json({ message: 'Invalid payload', issues: result.error.issues }, { status: 400 });
|
||||
}
|
||||
|
||||
const { complexId } = result.data;
|
||||
const complex = await selectComplex(user.id, complexId);
|
||||
|
||||
if (!complex) {
|
||||
return c.json({ message: 'Complex not found or not associated with user' }, { status: 404 });
|
||||
}
|
||||
|
||||
setCookie(c, SELECTED_COMPLEX_COOKIE, complexId, {
|
||||
httpOnly: true,
|
||||
secure: process.env.NODE_ENV === 'production',
|
||||
sameSite: 'strict',
|
||||
path: '/',
|
||||
maxAge: COOKIE_MAX_AGE,
|
||||
});
|
||||
|
||||
return c.json(complex);
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
import { Prisma } from '@/generated/prisma/client';
|
||||
import { db } from '@/lib/prisma';
|
||||
import { buildUniqueSlug, slugify } from '@/lib/slug';
|
||||
import type { UpdateComplexInput } from '@repo/api-contract';
|
||||
|
||||
export async function getComplexById(id: string) {
|
||||
return db.complex.findUnique({ where: { id } });
|
||||
}
|
||||
|
||||
export async function updateComplex(id: string, input: UpdateComplexInput) {
|
||||
const data: Record<string, unknown> = {};
|
||||
|
||||
if (input.complexName) {
|
||||
data.complexName = input.complexName;
|
||||
const base = slugify(input.complexName);
|
||||
data.complexSlug = await buildUniqueSlug(base, (slug) =>
|
||||
db.complex.findFirst({
|
||||
where: { complexSlug: slug, id: { not: id } },
|
||||
select: { id: true },
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
if (input.complexSlug) {
|
||||
const base = slugify(input.complexSlug);
|
||||
data.complexSlug = await buildUniqueSlug(base, (slug) =>
|
||||
db.complex.findFirst({
|
||||
where: { complexSlug: slug, id: { not: id } },
|
||||
select: { id: true },
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
if (input.adminEmail) {
|
||||
data.adminEmail = input.adminEmail;
|
||||
}
|
||||
|
||||
if (input.planCode !== undefined) {
|
||||
data.planCode = input.planCode;
|
||||
}
|
||||
|
||||
if (input.physicalAddress !== undefined) {
|
||||
data.physicalAddress = input.physicalAddress?.trim() ?? null;
|
||||
}
|
||||
|
||||
if (input.city !== undefined) {
|
||||
data.city = input.city?.trim() ?? null;
|
||||
}
|
||||
|
||||
if (input.state !== undefined) {
|
||||
data.state = input.state?.trim() ?? null;
|
||||
}
|
||||
|
||||
if (input.country !== undefined) {
|
||||
data.country = input.country?.trim() ?? null;
|
||||
}
|
||||
|
||||
return db.complex.update({
|
||||
where: { id },
|
||||
data: data as Prisma.ComplexUncheckedUpdateInput,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import { Prisma } from '@/generated/prisma/client';
|
||||
import type { AppContext } from '@/types/hono';
|
||||
import type { UpdateComplexInput } from '@repo/api-contract';
|
||||
import { getComplexById, updateComplex } from './update-complex.business';
|
||||
|
||||
type ComplexIdParams = { id: string };
|
||||
|
||||
export async function updateComplexHandler(c: AppContext) {
|
||||
const { id } = c.req.valid('param' as never) as ComplexIdParams;
|
||||
const payload = c.req.valid('json' as never) as UpdateComplexInput;
|
||||
|
||||
const existing = await getComplexById(id);
|
||||
|
||||
if (!existing) {
|
||||
return c.json({ message: 'Complejo no encontrado.' }, 404);
|
||||
}
|
||||
|
||||
try {
|
||||
const complex = await updateComplex(id, payload);
|
||||
return c.json(complex);
|
||||
} catch (error) {
|
||||
if (error instanceof Prisma.PrismaClientKnownRequestError) {
|
||||
return c.json({ message: 'No se pudo actualizar el complejo.' }, 409);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user