Skip to content

Commit

Permalink
✨ feat(cabins): add occupied days query (#510)
Browse files Browse the repository at this point in the history
* ✨ feat(cabins): add occupied days query

* chore: address code smell
  • Loading branch information
larwaa authored Mar 5, 2024
1 parent 73675e6 commit 03b02be
Show file tree
Hide file tree
Showing 10 changed files with 355 additions and 2 deletions.
1 change: 1 addition & 0 deletions schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ type Cabin {
id: ID!
internalPrice: Int!
name: String!
occupiedDays: [DateTime!]!
}

input CabinInput {
Expand Down
10 changes: 10 additions & 0 deletions src/graphql/cabins/resolvers/Cabin.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,14 @@
import type { CabinResolvers } from "./../../types.generated.js";
export const Cabin: CabinResolvers = {
/* Implement Cabin resolver logic here */
occupiedDays: async ({ id }, _args, ctx) => {
const occupiedDates = await ctx.cabins.getOccupiedDates(ctx, {
cabinId: id,
});
if (!occupiedDates.ok) {
throw occupiedDates.error;
}

return occupiedDates.data.days;
},
};
44 changes: 44 additions & 0 deletions src/graphql/cabins/resolvers/Query/cabins.unit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,5 +35,49 @@ describe("Cabin queries", () => {
expect(errors).toBeUndefined();
expect(cabinService.findManyCabins).toHaveBeenCalled();
});

it("resolves occupied days for the cabins", async () => {
const { client, cabinService } = createMockApolloServer();
const oksen = mock<Cabin>({
id: faker.string.uuid(),
name: "Oksen",
});
const bjørnen = mock<Cabin>({
id: faker.string.uuid(),
name: "Bjørnen",
});
cabinService.findManyCabins.mockResolvedValue([oksen, bjørnen]);
cabinService.getOccupiedDates.mockResolvedValue({
ok: true,
data: {
days: [],
},
});

const { errors } = await client.query({
query: graphql(`
query CabinsOccupiedDays {
cabins {
cabins {
id
name
occupiedDays
}
}
}
`),
});

expect(errors).toBeUndefined();
expect(cabinService.findManyCabins).toHaveBeenCalled();
expect(cabinService.getOccupiedDates).toHaveBeenCalledWith(
expect.anything(),
{ cabinId: oksen.id },
);
expect(cabinService.getOccupiedDates).toHaveBeenCalledWith(
expect.anything(),
{ cabinId: bjørnen.id },
);
});
});
});
1 change: 1 addition & 0 deletions src/graphql/cabins/schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ type Cabin {
internalPrice: Int!
externalPrice: Int!
capacity: Int!
occupiedDays: [DateTime!]!
}

type Booking {
Expand Down
4 changes: 4 additions & 0 deletions src/lib/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,10 @@ interface ICabinService {
{ totalCost: number },
InternalServerError | NotFoundError | InvalidArgumentError
>;
getOccupiedDates(
ctx: Context,
params: { cabinId: string },
): ResultAsync<{ days: Date[] }, NotFoundError | InternalServerError>;
}

interface IEventService {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import { faker } from "@faker-js/faker";
import { DateTime } from "luxon";
import { makeMockContext } from "~/lib/context.js";
import { makeDependencies } from "./dependencies.js";

describe("CabinRepository", () => {
describe("#findManyBookings", () => {
it("returns all bookings for a cabin", async () => {
const {
cabinRepository,
oksen,
oksenBooking,
bothBooking,
bjørnenBooking,
} = await makeDependencies();

const result = await cabinRepository.findManyBookings(makeMockContext(), {
cabinId: oksen.id,
});

expect(result).toEqual({
ok: true,
data: {
bookings: expect.arrayContaining([oksenBooking, bothBooking]),
},
});

expect(result).toEqual({
ok: true,
data: {
bookings: expect.not.arrayContaining([bjørnenBooking]),
},
});
});

it("returns empty array if cabin with the id does not exist", async () => {
const { cabinRepository } = await makeDependencies();

const result = await cabinRepository.findManyBookings(makeMockContext(), {
cabinId: faker.string.uuid(),
});

expect(result).toEqual({
ok: true,
data: {
bookings: [],
},
});
});

it("returns only bookings matching status", async () => {
const { cabinRepository, makeBooking, oksen } = await makeDependencies();

const confirmedBooking = await makeBooking({
cabins: [{ id: oksen.id }],
status: "CONFIRMED",
endDate: DateTime.now().plus({ days: 4 }).toJSDate(),
startDate: DateTime.now().plus({ days: 1 }).toJSDate(),
});

const pendingBooking = await makeBooking({
cabins: [{ id: oksen.id }],
status: "PENDING",
endDate: DateTime.now().plus({ days: 4 }).toJSDate(),
startDate: DateTime.now().plus({ days: 1 }).toJSDate(),
});

const result = await cabinRepository.findManyBookings(makeMockContext(), {
cabinId: oksen.id,
bookingStatus: "CONFIRMED",
});

expect(result).toEqual({
ok: true,
data: {
bookings: expect.arrayContaining([confirmedBooking]),
},
});

expect(result).toEqual({
ok: true,
data: {
bookings: expect.not.arrayContaining([pendingBooking]),
},
});
});
});
});
54 changes: 53 additions & 1 deletion src/repositories/cabins/repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,71 @@ import type {
Semester,
} from "@prisma/client";
import { PrismaClientKnownRequestError } from "@prisma/client/runtime/library.js";
import { Booking, type BookingType } from "~/domain/cabins.js";
import {
Booking,
type BookingStatus,
type BookingType,
} from "~/domain/cabins.js";
import {
InternalServerError,
InvalidArgumentError,
NotFoundError,
} from "~/domain/errors.js";
import type { Context } from "~/lib/context.js";
import { prismaKnownErrorCodes } from "~/lib/prisma.js";
import type { ResultAsync } from "~/lib/result.js";
import type { ICabinRepository } from "~/services/cabins/service.js";

export class CabinRepository implements ICabinRepository {
constructor(private db: PrismaClient) {}

async findManyBookings(
ctx: Context,
params: {
cabinId: string;
endAtGte?: Date;
bookingStatus?: BookingStatus;
},
): ResultAsync<{ bookings: BookingType[] }, InternalServerError> {
ctx.log.info(params, "Finding bookings for cabin");
try {
const bookings = await this.db.booking.findMany({
include: {
cabins: {
select: {
id: true,
},
},
},
where: {
status: params.bookingStatus,
cabins: {
some: {
id: params.cabinId,
},
},
endDate: {
gte: params.endAtGte,
},
},
});
return {
ok: true,
data: {
bookings: bookings.map((booking) => new Booking(booking)),
},
};
} catch (err) {
return {
ok: false,
error: new InternalServerError(
"Failed to find bookings for cabin",
err,
),
};
}
}

async createCabin(params: {
name: string;
capacity: number;
Expand Down
36 changes: 36 additions & 0 deletions src/repositories/cabins/seed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,42 @@ const bookingCreateInput: Prisma.BookingCreateInput[] = [
},
},
},
{
id: faker.string.uuid(),
startDate: DateTime.now().plus({ weeks: 1 }).toJSDate(),
endDate: DateTime.now().plus({ weeks: 2 }).toJSDate(),
status: BookingStatus.CONFIRMED,
email: faker.internet.exampleEmail(),
phoneNumber: faker.phone.number(),
firstName: faker.person.firstName(),
lastName: faker.person.lastName(),
internalParticipantsCount: 5,
externalParticipantsCount: 10,
totalCost: 1 * 250 + 5 * 200 + 1 * 2500 + 5 * 2000,
cabins: {
connect: {
id: oksenId,
},
},
},
{
id: faker.string.uuid(),
startDate: DateTime.now().plus({ weeks: 3 }).toJSDate(),
endDate: DateTime.now().plus({ weeks: 4 }).toJSDate(),
status: BookingStatus.CONFIRMED,
email: faker.internet.exampleEmail(),
phoneNumber: faker.phone.number(),
firstName: faker.person.firstName(),
lastName: faker.person.lastName(),
internalParticipantsCount: 5,
externalParticipantsCount: 10,
totalCost: 1 * 250 + 5 * 200 + 1 * 2500 + 5 * 2000,
cabins: {
connect: {
id: bjornenId,
},
},
},
];

const createBookingSemesterInput: Prisma.BookingSemesterCreateInput[] = [
Expand Down
60 changes: 60 additions & 0 deletions src/services/cabins/__tests__/unit/get-occupied-dates.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { faker } from "@faker-js/faker";
import { type DeepMockProxy, mock, mockDeep } from "jest-mock-extended";
import { range } from "lodash-es";
import { DateTime } from "luxon";
import type { BookingType } from "~/domain/cabins.js";
import { makeMockContext } from "~/lib/context.js";
import { CabinService, type ICabinRepository } from "../../service.js";

describe("CabinService", () => {
let cabinService: CabinService;
let mockCabinRepository: DeepMockProxy<ICabinRepository>;

beforeAll(() => {
mockCabinRepository = mockDeep<ICabinRepository>();
cabinService = new CabinService(mockCabinRepository, mock(), mock());
});

describe("#getOccupiedDates", () => {
it("returns all occupied dates for a cabin", async () => {
const booking1 = {
...mock<BookingType>(),
startDate: DateTime.now().plus({ days: 1 }).toJSDate(),
endDate: DateTime.now().plus({ days: 3 }).toJSDate(),
};

const booking2 = {
...mock<BookingType>(),
startDate: DateTime.now().plus({ days: 5 }).toJSDate(),
endDate: DateTime.now().plus({ days: 7 }).toJSDate(),
};

mockCabinRepository.findManyBookings.mockResolvedValue({
ok: true,
data: {
bookings: [booking1, booking2],
},
});

const result = await cabinService.getOccupiedDates(makeMockContext(), {
cabinId: faker.string.uuid(),
});

if (!result.ok) throw result.error;

const { days } = result.data;

for (const day of range(1, 9)) {
const date = DateTime.now()
.plus({ days: day })
.startOf("day")
.toJSDate();
if (day === 4 || day === 8) {
expect(days).not.toContainEqual(date);
} else {
expect(days).toContainEqual(date);
}
}
});
});
});
Loading

0 comments on commit 03b02be

Please sign in to comment.