Skip to content

Commit

Permalink
✨ feat(cabins): get booking (#565)
Browse files Browse the repository at this point in the history
* ✨ feat(cabins): get booking

* chore: rename to byIdAndEmail to avoid collision
  • Loading branch information
larwaa authored Mar 21, 2024
1 parent 624cd4a commit 6122eb2
Show file tree
Hide file tree
Showing 13 changed files with 398 additions and 11 deletions.
16 changes: 16 additions & 0 deletions schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ type Booking {
"""
feedback: String!
firstName: String!
guests: Guests!
id: ID!
lastName: String!
phoneNumber: String!
Expand All @@ -71,6 +72,15 @@ type BookingContactResponse {
bookingContact: BookingContact!
}

input BookingInput {
email: String!
id: ID!
}

type BookingResponse {
booking: Booking
}

type BookingSemester {
bookingsEnabled: Boolean!
endAt: DateTime!
Expand Down Expand Up @@ -632,6 +642,11 @@ type GetAvailabilityCalendarResponse {
calendarMonths: [CalendarMonth!]!
}

type Guests {
external: Int!
internal: Int!
}

input GuestsInput {
external: Int!
internal: Int!
Expand Down Expand Up @@ -1127,6 +1142,7 @@ type PublicUser {
}

type Query {
booking(data: BookingInput!): BookingResponse!
bookingContact: BookingContactResponse!
bookingSemesters: BookingSemestersResponse!
bookingTerms: BookingTermsResponse!
Expand Down
6 changes: 6 additions & 0 deletions src/graphql/cabins/resolvers/Booking.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,10 @@ export const Booking: BookingResolvers = {
);
return cabinsResult;
},
guests: ({ internalParticipantsCount, externalParticipantsCount }) => {
return {
external: externalParticipantsCount,
internal: internalParticipantsCount,
};
},
};
4 changes: 4 additions & 0 deletions src/graphql/cabins/resolvers/BookingResponse.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import type { BookingResponseResolvers } from "./../../types.generated.js";
export const BookingResponse: BookingResponseResolvers = {
/* Implement BookingResponse resolver logic here */
};
4 changes: 4 additions & 0 deletions src/graphql/cabins/resolvers/Guests.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import type { GuestsResolvers } from "./../../types.generated.js";
export const Guests: GuestsResolvers = {
/* Implement Guests resolver logic here */
};
19 changes: 19 additions & 0 deletions src/graphql/cabins/resolvers/Query/booking.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import type { QueryResolvers } from "./../../../types.generated.js";
export const booking: NonNullable<QueryResolvers["booking"]> = async (
_parent,
{ data },
ctx,
) => {
const getBookingResult = await ctx.cabins.getBookingByIdAndEmail(ctx, data);
if (!getBookingResult.ok) {
switch (getBookingResult.error.name) {
case "InternalServerError":
throw getBookingResult.error;
case "NotFoundError":
return {
booking: null,
};
}
}
return getBookingResult.data;
};
136 changes: 136 additions & 0 deletions src/graphql/cabins/resolvers/Query/booking.unit.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
import { faker } from "@faker-js/faker";
import { Booking } from "~/domain/cabins.js";
import { InternalServerError, NotFoundError } from "~/domain/errors.js";
import { createMockApolloServer } from "~/graphql/test-clients/mock-apollo-server.js";
import { graphql } from "~/graphql/test-clients/unit/gql.js";
import { Result } from "~/lib/result.js";

describe("Cabin queries", () => {
describe("booking", () => {
it("returns a booking", async () => {
const { client, cabinService } = createMockApolloServer();
const booking = new Booking({
cabins: [],
createdAt: faker.date.recent(),
email: faker.internet.email(),
endDate: faker.date.recent(),
externalParticipantsCount: faker.number.int({ max: 10 }),
feedback: faker.lorem.paragraph(),
firstName: faker.person.firstName(),
id: faker.string.uuid(),
internalParticipantsCount: faker.number.int({ max: 10 }),
lastName: faker.person.lastName(),
phoneNumber: faker.phone.number(),
questions: faker.lorem.paragraph(),
startDate: faker.date.recent(),
status: "PENDING",
totalCost: faker.number.int({ max: 200 }),
});
cabinService.getBookingByIdAndEmail.mockResolvedValue(
Result.success({
booking,
}),
);

const { data } = await client.query({
query: graphql(`
query Booking($data: BookingInput!) {
booking(data: $data) {
booking {
id
guests {
internal
external
}
}
}
}
`),
variables: {
data: {
id: faker.string.uuid(),
email: faker.internet.email(),
},
},
});

expect(data).toEqual({
booking: {
booking: {
id: booking.id,
guests: {
internal: booking.internalParticipantsCount,
external: booking.externalParticipantsCount,
},
},
},
});
});

it("returns null on NotFoundError", async () => {
const { client, cabinService } = createMockApolloServer();
cabinService.getBookingByIdAndEmail.mockResolvedValue(
Result.error(new NotFoundError("")),
);

const { data } = await client.query({
query: graphql(`
query Booking($data: BookingInput!) {
booking(data: $data) {
booking {
id
guests {
internal
external
}
}
}
}
`),
variables: {
data: {
id: faker.string.uuid(),
email: faker.internet.email(),
},
},
});

expect(data).toEqual({
booking: {
booking: null,
},
});
});

it("throws on InternalServerError", async () => {
const { client, cabinService } = createMockApolloServer();
cabinService.getBookingByIdAndEmail.mockResolvedValue(
Result.error(new InternalServerError("")),
);

const { errors } = await client.query({
query: graphql(`
query Booking($data: BookingInput!) {
booking(data: $data) {
booking {
id
guests {
internal
external
}
}
}
}
`),
variables: {
data: {
id: faker.string.uuid(),
email: faker.internet.email(),
},
},
});

expect(errors).toBeDefined();
});
});
});
16 changes: 16 additions & 0 deletions src/graphql/cabins/schema.graphql
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
type Query {
booking(data: BookingInput!): BookingResponse!
cabins: CabinsResponse!
bookingSemesters: BookingSemestersResponse!
bookingContact: BookingContactResponse!
Expand Down Expand Up @@ -43,6 +44,15 @@ type Mutation {
updateBookingTerms: UpdateBookingTermsResponse!
}

input BookingInput {
id: ID!
email: String!
}

type BookingResponse {
booking: Booking
}

type BookingTermsResponse {
bookingTerms: BookingTerms
}
Expand Down Expand Up @@ -158,6 +168,12 @@ type Booking {
Feedback from the cabin administrators to the user, e.g. why a booking was rejected
"""
feedback: String!
guests: Guests!
}

type Guests {
internal: Int!
external: Int!
}

type BookingContact {
Expand Down
2 changes: 1 addition & 1 deletion src/graphql/cabins/schema.mappers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
type BookingSemestersResponseMapper = {};

export type {
BookingType as BookingMapper,
Booking as BookingMapper,
BookingSemester as BookingSemesterMapper,
Cabin as CabinMapper,
CalendarMonth as CalendarMonthMapper,
Expand Down
5 changes: 5 additions & 0 deletions src/lib/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import type { FastifyInstance, FastifyRequest } from "fastify";
import { BlobStorageAdapter } from "~/adapters/azure-blob-storage.js";
import { type Configuration, env } from "~/config.js";
import type {
Booking,
BookingContact,
BookingSemester,
BookingSemesterEnumType,
Expand Down Expand Up @@ -388,6 +389,10 @@ interface ICabinService {
{ bookingTerms: BookingTerms },
NotFoundError | InternalServerError
>;
getBookingByIdAndEmail(
ctx: Context,
params: { id: string; email: string },
): ResultAsync<{ booking: Booking }, NotFoundError | InternalServerError>;
}

interface IEventService {
Expand Down
100 changes: 100 additions & 0 deletions src/services/cabins/__tests__/unit/get-booking-by-id-and-email.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import { faker } from "@faker-js/faker";
import { Booking } from "~/domain/cabins.js";
import { NotFoundError } from "~/domain/errors.js";
import { makeMockContext } from "~/lib/context.js";
import { Result } from "~/lib/result.js";
import { makeDependencies } from "./dependencies.js";

describe("Cabin service", () => {
describe("#getBookingByIdAndEmail", () => {
it("returns a booking if the email matches the booking", async () => {
const { cabinService, cabinRepository } = makeDependencies();
const email = faker.internet.email();

const booking = new Booking({
id: faker.string.uuid(),
cabins: [],
createdAt: faker.date.recent(),
email,
endDate: faker.date.recent(),
externalParticipantsCount: faker.number.int({ max: 10 }),
feedback: faker.lorem.paragraph(),
firstName: faker.person.firstName(),
internalParticipantsCount: faker.number.int({ max: 10 }),
lastName: faker.person.lastName(),
phoneNumber: faker.phone.number(),
questions: faker.lorem.paragraph(),
startDate: faker.date.recent(),
status: "PENDING",
totalCost: faker.number.int({ max: 200 }),
});

cabinRepository.getBookingById.mockResolvedValue(
Result.success({ booking }),
);

const result = await cabinService.getBookingByIdAndEmail(
makeMockContext(),
{
id: faker.string.uuid(),
email,
},
);

expect(result).toEqual(Result.success({ booking }));
});

it("returns NotFound if the email does not match the booking", async () => {
const { cabinService, cabinRepository } = makeDependencies();

const booking = new Booking({
id: faker.string.uuid(),
cabins: [],
createdAt: faker.date.recent(),
email: faker.internet.email({ firstName: "test" }),
endDate: faker.date.recent(),
externalParticipantsCount: faker.number.int({ max: 10 }),
feedback: faker.lorem.paragraph(),
firstName: faker.person.firstName(),
internalParticipantsCount: faker.number.int({ max: 10 }),
lastName: faker.person.lastName(),
phoneNumber: faker.phone.number(),
questions: faker.lorem.paragraph(),
startDate: faker.date.recent(),
status: "PENDING",
totalCost: faker.number.int({ max: 200 }),
});

cabinRepository.getBookingById.mockResolvedValue(
Result.success({ booking }),
);

const result = await cabinService.getBookingByIdAndEmail(
makeMockContext(),
{
id: faker.string.uuid(),
email: faker.internet.email({ firstName: "not-test" }),
},
);

expect(result).toEqual(Result.error(expect.any(NotFoundError)));
});
it("returns NotFound if the repository returns NotFound", async () => {
const { cabinService, cabinRepository } = makeDependencies();

cabinRepository.getBookingById.mockResolvedValue(
Result.error(new NotFoundError("")),
);

const result = await cabinService.getBookingByIdAndEmail(
makeMockContext(),
{
id: faker.string.uuid(),
email: faker.internet.email(),
},
);

expect(result).toEqual(Result.error(expect.any(NotFoundError)));
});
});
});
Loading

0 comments on commit 6122eb2

Please sign in to comment.