Refactor to vertical slice

This commit is contained in:
Jose Selesan
2026-06-26 14:05:37 -03:00
parent 3949c9add1
commit c8477de5d2
149 changed files with 5011 additions and 5127 deletions

View File

@@ -1,27 +1,22 @@
import { beforeEach, expect, mock, test } from 'bun:test';
const rescheduleAdminBookingMock = mock(
async (_userId: string, _id: string, _input: unknown) => ({})
import { Errors } from '@/lib/errors';
import { err, ok } from '@/lib/result';
const rescheduleAdminBookingMock = mock(async (_userId: string, _id: string, _input: unknown) =>
ok({})
);
class MockAdminBookingServiceError extends Error {
status: 400 | 403 | 404 | 409;
constructor(message: string, status: 400 | 403 | 404 | 409 = 400) {
super(message);
this.name = 'AdminBookingServiceError';
this.status = status;
}
}
mock.module('@/modules/admin-booking/services/admin-booking.service', () => ({
__esModule: true,
AdminBookingServiceError: MockAdminBookingServiceError,
rescheduleAdminBooking: rescheduleAdminBookingMock,
}));
mock.module(
'@/modules/admin-booking/features/reschedule-admin-booking/reschedule-admin-booking.business',
() => ({
__esModule: true,
rescheduleAdminBooking: rescheduleAdminBookingMock,
})
);
const { rescheduleAdminBookingHandler } = await import(
'@/modules/admin-booking/handlers/reschedule-admin-booking.handler'
'@/modules/admin-booking/features/reschedule-admin-booking/reschedule-admin-booking.handler'
);
type HandlerContext = {
@@ -82,7 +77,7 @@ test('returns 200 with the updated booking', async () => {
updatedAt: '2026-07-14T00:00:00.000Z',
} as const;
rescheduleAdminBookingMock.mockResolvedValue(updatedBooking);
rescheduleAdminBookingMock.mockResolvedValue(ok(updatedBooking));
const { c, json } = createContext({
userId: 'user-1',
@@ -96,16 +91,13 @@ test('returns 200 with the updated booking', async () => {
startTime: '14:00',
});
expect(json.mock.calls[0]?.[0]).toEqual(updatedBooking);
expect(json.mock.calls[0]?.[1]).toBeUndefined();
expect(json.mock.calls[0]?.[1]).toBe(200);
expect(response).toBeDefined();
});
test('maps AdminBookingServiceError to an error response', async () => {
rescheduleAdminBookingMock.mockRejectedValue(
new MockAdminBookingServiceError(
'Solo se pueden reprogramar reservas en estado confirmada.',
409
)
rescheduleAdminBookingMock.mockResolvedValue(
err(Errors.conflict('Solo se pueden reprogramar reservas en estado confirmada.'))
);
const { c, json } = createContext({
@@ -119,7 +111,10 @@ test('maps AdminBookingServiceError to an error response', async () => {
startTime: '14:00',
});
expect(json.mock.calls[0]?.[0]).toEqual({
message: 'Solo se pueden reprogramar reservas en estado confirmada.',
type: 'https://api.myapp.dev/problems/conflict',
title: 'Conflict',
status: 409,
detail: 'Solo se pueden reprogramar reservas en estado confirmada.',
});
expect(json.mock.calls[0]?.[1]).toBe(409);
expect(response).toBeDefined();

View File

@@ -2,8 +2,8 @@ import { beforeEach, expect, test } from 'bun:test';
import { prismaMock, transactionMock, uuidV7Mock } from '../support/prisma.mock';
const { AdminBookingServiceError, rescheduleAdminBooking } = await import(
'@/modules/admin-booking/services/admin-booking.service'
const { rescheduleAdminBooking } = await import(
'@/modules/admin-booking/features/reschedule-admin-booking/reschedule-admin-booking.business'
);
function makeBooking(overrides?: Record<string, unknown>) {
@@ -84,9 +84,11 @@ test('reschedules a booking changing both court and time', async () => {
startTime: '14:00',
});
expect(result.courtId).toBe('court-2');
expect(result.startTime).toBe('14:00');
expect(result.courtName).toBe('Cancha 2');
expect(result.ok).toBe(true);
if (!result.ok) return;
expect(result.value.courtId).toBe('court-2');
expect(result.value.startTime).toBe('14:00');
expect(result.value.courtName).toBe('Cancha 2');
expect(prismaMock.courtBookingLog.create).toHaveBeenCalledTimes(1);
expect(prismaMock.courtBooking.update).toHaveBeenCalledWith({
where: { id: 'booking-1' },
@@ -137,8 +139,10 @@ test('reschedules a booking changing only the time', async () => {
startTime: '15:00',
});
expect(result.startTime).toBe('15:00');
expect(result.courtId).toBe('court-1');
expect(result.ok).toBe(true);
if (!result.ok) return;
expect(result.value.startTime).toBe('15:00');
expect(result.value.courtId).toBe('court-1');
expect(prismaMock.courtBookingLog.create).toHaveBeenCalledTimes(1);
expect(prismaMock.courtBooking.update).toHaveBeenCalledWith({
where: { id: 'booking-1' },
@@ -153,15 +157,11 @@ test('reschedules a booking changing only the time', async () => {
test('rejects when booking is not found', async () => {
prismaMock.courtBooking.findFirst.mockResolvedValue(null as never);
let caught: unknown;
try {
await rescheduleAdminBooking('user-1', 'non-existent', { startTime: '14:00' });
} catch (error) {
caught = error;
}
const result = await rescheduleAdminBooking('user-1', 'non-existent', { startTime: '14:00' });
expect(caught).toBeInstanceOf(AdminBookingServiceError);
expect((caught as InstanceType<typeof AdminBookingServiceError>).status).toBe(404);
expect(result.ok).toBe(false);
if (result.ok) return;
expect(result.error.type).toBe('not_found');
expect(transactionMock).not.toHaveBeenCalled();
});
@@ -170,15 +170,11 @@ test('rejects when booking status is not CONFIRMED', async () => {
makeBooking({ status: 'COMPLETED' }) as never
);
let caught: unknown;
try {
await rescheduleAdminBooking('user-1', 'booking-1', { startTime: '14:00' });
} catch (error) {
caught = error;
}
const result = await rescheduleAdminBooking('user-1', 'booking-1', { startTime: '14:00' });
expect(caught).toBeInstanceOf(AdminBookingServiceError);
expect((caught as InstanceType<typeof AdminBookingServiceError>).status).toBe(409);
expect(result.ok).toBe(false);
if (result.ok) return;
expect(result.error.type).toBe('conflict');
expect(transactionMock).not.toHaveBeenCalled();
});
@@ -194,18 +190,14 @@ test('rejects when slot is not available in target court', async () => {
}) as never
);
let caught: unknown;
try {
await rescheduleAdminBooking('user-1', 'booking-1', {
courtId: 'court-2',
startTime: '14:00',
});
} catch (error) {
caught = error;
}
const result = await rescheduleAdminBooking('user-1', 'booking-1', {
courtId: 'court-2',
startTime: '14:00',
});
expect(caught).toBeInstanceOf(AdminBookingServiceError);
expect((caught as InstanceType<typeof AdminBookingServiceError>).status).toBe(409);
expect(result.ok).toBe(false);
if (result.ok) return;
expect(result.error.type).toBe('conflict');
});
test('rejects when target court has a different sport', async () => {
@@ -216,18 +208,14 @@ test('rejects when target court has a different sport', async () => {
}) as never
);
let caught: unknown;
try {
await rescheduleAdminBooking('user-1', 'booking-1', {
courtId: 'court-2',
startTime: '14:00',
});
} catch (error) {
caught = error;
}
const result = await rescheduleAdminBooking('user-1', 'booking-1', {
courtId: 'court-2',
startTime: '14:00',
});
expect(caught).toBeInstanceOf(AdminBookingServiceError);
expect((caught as InstanceType<typeof AdminBookingServiceError>).status).toBe(409);
expect(result.ok).toBe(false);
if (result.ok) return;
expect(result.error.type).toBe('conflict');
expect(transactionMock).not.toHaveBeenCalled();
});
@@ -239,18 +227,14 @@ test('rejects when target court is under maintenance', async () => {
}) as never
);
let caught: unknown;
try {
await rescheduleAdminBooking('user-1', 'booking-1', {
courtId: 'court-2',
startTime: '14:00',
});
} catch (error) {
caught = error;
}
const result = await rescheduleAdminBooking('user-1', 'booking-1', {
courtId: 'court-2',
startTime: '14:00',
});
expect(caught).toBeInstanceOf(AdminBookingServiceError);
expect((caught as InstanceType<typeof AdminBookingServiceError>).status).toBe(409);
expect(result.ok).toBe(false);
if (result.ok) return;
expect(result.error.type).toBe('conflict');
});
test('rejects when there is an overlapping booking', async () => {
@@ -266,33 +250,25 @@ test('rejects when there is an overlapping booking', async () => {
}) as never
);
let caught: unknown;
try {
await rescheduleAdminBooking('user-1', 'booking-1', {
courtId: 'court-2',
startTime: '14:00',
});
} catch (error) {
caught = error;
}
const result = await rescheduleAdminBooking('user-1', 'booking-1', {
courtId: 'court-2',
startTime: '14:00',
});
expect(caught).toBeInstanceOf(AdminBookingServiceError);
expect((caught as InstanceType<typeof AdminBookingServiceError>).status).toBe(409);
expect(result.ok).toBe(false);
if (result.ok) return;
expect(result.error.type).toBe('conflict');
expect(transactionMock).not.toHaveBeenCalled();
});
test('rejects when neither courtId nor startTime changes', async () => {
prismaMock.courtBooking.findFirst.mockResolvedValue(makeBooking() as never);
let caught: unknown;
try {
await rescheduleAdminBooking('user-1', 'booking-1', {});
} catch (error) {
caught = error;
}
const result = await rescheduleAdminBooking('user-1', 'booking-1', {});
expect(caught).toBeInstanceOf(AdminBookingServiceError);
expect((caught as InstanceType<typeof AdminBookingServiceError>).status).toBe(400);
expect(result.ok).toBe(false);
if (result.ok) return;
expect(result.error.type).toBe('validation');
expect(transactionMock).not.toHaveBeenCalled();
});
@@ -301,17 +277,13 @@ test('rejects when target court does not belong to the same complex', async () =
// Court belongs to a different complex
prismaMock.court.findFirst.mockResolvedValue(null as never);
let caught: unknown;
try {
await rescheduleAdminBooking('user-1', 'booking-1', {
courtId: 'other-complex-court',
startTime: '14:00',
});
} catch (error) {
caught = error;
}
const result = await rescheduleAdminBooking('user-1', 'booking-1', {
courtId: 'other-complex-court',
startTime: '14:00',
});
expect(caught).toBeInstanceOf(AdminBookingServiceError);
expect((caught as InstanceType<typeof AdminBookingServiceError>).status).toBe(404);
expect(result.ok).toBe(false);
if (result.ok) return;
expect(result.error.type).toBe('not_found');
expect(transactionMock).not.toHaveBeenCalled();
});