105 lines
2.9 KiB
TypeScript
105 lines
2.9 KiB
TypeScript
import { assertAvailabilityRanges } from '@/lib/booking-utils';
|
|
import { ComplexAccessError, ensureComplexAccess } from '@/lib/complex-access';
|
|
import { Errors } from '@/lib/errors';
|
|
import { db } from '@/lib/prisma';
|
|
import { type Result, err, ok } from '@/lib/result';
|
|
import { evaluatePlanUsage, parsePlanRules } from '@/modules/plan/services/plan-rules.service';
|
|
import type { CreateCourtInput } from '@repo/api-contract';
|
|
import { v7 as uuidv7 } from 'uuid';
|
|
import { CourtBusinessError } from '../../shared/errors';
|
|
import { ensureActiveSport, getCourtByIdForUser } from '../../shared/guards';
|
|
import { mapCourtResponse } from '../../shared/mappers';
|
|
|
|
async function enforcePlanCourtLimit(complexId: string): Promise<Result<void>> {
|
|
const complex = await db.complex.findUnique({
|
|
where: { id: complexId },
|
|
select: {
|
|
plan: {
|
|
select: {
|
|
rules: true,
|
|
},
|
|
},
|
|
},
|
|
});
|
|
|
|
if (!complex?.plan) {
|
|
return ok(undefined);
|
|
}
|
|
|
|
const rules = parsePlanRules(complex.plan.rules);
|
|
const courtsCount = await db.court.count({
|
|
where: { complexId },
|
|
});
|
|
|
|
const violations = evaluatePlanUsage(rules, {
|
|
courtsCount,
|
|
bookingsToday: 0,
|
|
});
|
|
|
|
const maxCourtViolation = violations.find((violation) => violation.code === 'MAX_COURTS_REACHED');
|
|
|
|
if (maxCourtViolation) {
|
|
return err(Errors.conflict(maxCourtViolation.message));
|
|
}
|
|
|
|
return ok(undefined);
|
|
}
|
|
|
|
export async function createCourt(
|
|
userId: string,
|
|
complexId: string,
|
|
input: CreateCourtInput
|
|
): Promise<Result<ReturnType<typeof mapCourtResponse>>> {
|
|
try {
|
|
await ensureComplexAccess(complexId, userId);
|
|
} catch (error) {
|
|
if (error instanceof ComplexAccessError) {
|
|
return err(Errors.forbidden(error.message));
|
|
}
|
|
throw error;
|
|
}
|
|
|
|
const sportResult = await ensureActiveSport(input.sportId);
|
|
if (!sportResult.ok) return sportResult;
|
|
|
|
const planResult = await enforcePlanCourtLimit(complexId);
|
|
if (!planResult.ok) return planResult;
|
|
|
|
assertAvailabilityRanges(input.availability);
|
|
|
|
const createdCourt = await db.$transaction(async (tx) => {
|
|
const court = await tx.court.create({
|
|
data: {
|
|
id: uuidv7(),
|
|
complexId,
|
|
sportId: input.sportId,
|
|
name: input.name.trim(),
|
|
slotDurationMinutes: input.slotDurationMinutes,
|
|
basePrice: input.basePrice,
|
|
},
|
|
});
|
|
|
|
await tx.courtAvailability.createMany({
|
|
data: input.availability.map((availability) => ({
|
|
id: uuidv7(),
|
|
courtId: court.id,
|
|
dayOfWeek: availability.dayOfWeek,
|
|
startTime: availability.startTime,
|
|
endTime: availability.endTime,
|
|
})),
|
|
});
|
|
|
|
return court;
|
|
});
|
|
|
|
const court = await getCourtByIdForUser(createdCourt.id, userId);
|
|
|
|
if (!court) {
|
|
return err(Errors.notFound('No se pudo obtener la cancha creada.'));
|
|
}
|
|
|
|
return ok(mapCourtResponse(court));
|
|
}
|
|
|
|
export { CourtBusinessError };
|