Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add booking history crud service #737

Merged
merged 12 commits into from
Aug 3, 2024
14 changes: 14 additions & 0 deletions server/src/data-layer/models/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,17 @@ export type DocumentDataWithUid<T> = T & {
*/
id: string
}

/**
* Utility type to determine
*/
export type PaginatedFirebaseResponse<T> = {
/**
* The current "page" of data returned from querying
*/
data: T[]
/**
* The cursor of the next page, is `undefined` if no such cursor exists
*/
nextCursor?: string
}
184 changes: 184 additions & 0 deletions server/src/data-layer/services/BookingHistoryService.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
import { dateToFirestoreTimeStamp } from "data-layer/adapters/DateUtils"
import BookingHistoryService from "./BookingHistoryService"
import { Timestamp } from "firebase-admin/firestore"
import db from "data-layer/adapters/FirestoreCollections"
import { cleanFirestore } from "test-config/TestUtils"

const bookingHistoryService = new BookingHistoryService()
describe("BookingHistoryService integration tests", () => {
afterEach(async () => {
await cleanFirestore()
})

it("Should be able to add an event for deleted bookings", async () => {
const startDate = dateToFirestoreTimeStamp(new Date(2002, 10, 8))
const endDate = dateToFirestoreTimeStamp(new Date(2002, 10, 10))
const currentTime = dateToFirestoreTimeStamp(new Date())

const event = {
uid: "user-removed-from-booking",
start_date: startDate as Timestamp,
end_date: endDate as Timestamp,
event_type: "removed_user_from_booking",
timestamp: currentTime
} as const

const newEvent = await bookingHistoryService.addBookingDeletedEvent(event)

const result = (await db.bookingHistory.doc(newEvent.id).get()).data()

expect(result).toEqual(event)
})

it("Should be able to add an event for created bookings", async () => {
const startDate = dateToFirestoreTimeStamp(new Date(2002, 10, 8))
const endDate = dateToFirestoreTimeStamp(new Date(2002, 10, 10))
const currentTime = dateToFirestoreTimeStamp(new Date())

const event = {
uid: "user-added-to-booking",
start_date: startDate as Timestamp,
end_date: endDate as Timestamp,
event_type: "added_user_to_booking",
timestamp: currentTime
} as const

const newEvent = await bookingHistoryService.addBookingAddedEvent(event)

const result = (await db.bookingHistory.doc(newEvent.id).get()).data()

expect(result).toEqual(event)
})

it("Should be able to add an event for availability changes", async () => {
const startDate = dateToFirestoreTimeStamp(new Date(2002, 10, 8))
const endDate = dateToFirestoreTimeStamp(new Date(2002, 10, 10))
const currentTime = dateToFirestoreTimeStamp(new Date())

const event = {
start_date: startDate as Timestamp,
end_date: endDate as Timestamp,
event_type: "changed_date_availability",
timestamp: currentTime,
change: -69
} as const

const newEvent =
await bookingHistoryService.addAvailibilityChangeEvent(event)

const result = await (await db.bookingHistory.doc(newEvent.id).get()).data()

expect(result).toEqual(event)
})
describe("Fetching events", () => {
/**
* In these tests we don't care about these
*/
const startDate = dateToFirestoreTimeStamp(new Date(2002, 10, 8))
const endDate = dateToFirestoreTimeStamp(new Date(2002, 10, 10))

const availabilityEvent = {
timestamp: Timestamp.fromDate(new Date(2001, 10, 9)),
change: 69,
start_date: startDate,
end_date: endDate,
event_type: "changed_date_availability"
} as const

const deletedEvent = {
timestamp: Timestamp.fromDate(new Date(2001, 10, 9)),
start_date: startDate,
end_date: endDate,
event_type: "removed_user_from_booking",
uid: "deleted-user"
} as const

const addedEvent = {
timestamp: Timestamp.fromDate(new Date(2001, 10, 9)),
start_date: startDate,
end_date: endDate,
event_type: "added_user_to_booking",
uid: "added-user"
} as const

const notIncludedEvent = {
timestamp: Timestamp.fromDate(new Date(2001, 10, 10)),
start_date: startDate,
end_date: endDate,
event_type: "added_user_to_booking",
uid: "unincluded-user"
} as const

it("Should be able to fetch history in between a range of dates", async () => {
const searchStartDate = dateToFirestoreTimeStamp(new Date(2001, 10, 6))
const searchEndDate = dateToFirestoreTimeStamp(new Date(2001, 10, 9))

const { id: availabilityId } =
await bookingHistoryService.addAvailibilityChangeEvent(
availabilityEvent
)

const { id: deletedId } =
await bookingHistoryService.addBookingDeletedEvent(deletedEvent)

const { id: addedId } =
await bookingHistoryService.addBookingAddedEvent(addedEvent)

const { id: notIncludedId } =
await bookingHistoryService.addBookingAddedEvent(notIncludedEvent)

const foundEvents =
await bookingHistoryService.getAllHistoryBetweenDateRange(
searchStartDate,
searchEndDate
)

expect(foundEvents).toContainEqual({ ...addedEvent, id: addedId })
expect(foundEvents).toContainEqual({ ...deletedEvent, id: deletedId })
expect(foundEvents).toContainEqual({
...availabilityEvent,
id: availabilityId
})
expect(foundEvents).not.toContainEqual({
...notIncludedEvent,
id: notIncludedId
})
})

it("Should be able to fetch the latest X events", async () => {
// Ordering matters! Earlier addition means it should be the first out
const { id: notIncludedId } =
await bookingHistoryService.addBookingAddedEvent(notIncludedEvent)

const { id: availabilityId } =
await bookingHistoryService.addAvailibilityChangeEvent(
availabilityEvent
)

const { id: deletedId } =
await bookingHistoryService.addBookingDeletedEvent(deletedEvent)

const { id: addedId } =
await bookingHistoryService.addBookingAddedEvent(addedEvent)

const PAGE_LENGTH = 3 as const

const { data: foundEvents, nextCursor } =
await bookingHistoryService.getLatestHistory(PAGE_LENGTH)

expect(nextCursor).not.toBeUndefined()
expect(foundEvents).toHaveLength(PAGE_LENGTH)

expect(foundEvents).toContainEqual({ ...addedEvent, id: addedId })
expect(foundEvents).toContainEqual({ ...deletedEvent, id: deletedId })
expect(foundEvents).toContainEqual({
...availabilityEvent,
id: availabilityId
})
expect(foundEvents).not.toContainEqual({
...notIncludedEvent,
id: notIncludedId
})
})
})
})
104 changes: 104 additions & 0 deletions server/src/data-layer/services/BookingHistoryService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import { firestoreTimestampToDate } from "data-layer/adapters/DateUtils"
import db from "data-layer/adapters/FirestoreCollections"
import {
DocumentDataWithUid,
PaginatedFirebaseResponse
} from "data-layer/models/common"
import {
BookingAddedEvent,
BookingAvailabilityChangeEvent,
BookingDeletedEvent,
BookingHistoryEvent
} from "data-layer/models/firebase"
import { Timestamp } from "firebase-admin/firestore"

class BookingHistoryService {
/**
* Stores a manual deletion of a booking (by admin) into the booking history collection
*
* @param event the required parameters defined by {@link BookingDeletedEvent}
* @returns the created document reference
*/
public async addBookingDeletedEvent(event: BookingDeletedEvent) {
return await db.bookingHistory.add(event)
}

/**
* Stores a manual creation of a booking (by admin) into the booking history collection
*
* @param event the required parameters defined by {@link BookingAddedEvent}
* @returns the created document reference
*/
public async addBookingAddedEvent(event: BookingAddedEvent) {
return await db.bookingHistory.add(event)
}

/**
* Stores a modification to the booking availability into the booking history collection
*
* @param event the required parameters defined by {@link BookingAvailabilityChangeEvent}
* @returns the created document reference
*/
public async addAvailibilityChangeEvent(
event: BookingAvailabilityChangeEvent
) {
return await db.bookingHistory.add(event)
}

/**
* Returns all history events whose timestamps fall between the given timestamps
*
* @param startDate the first date to return history events for
* @param endDate the last date to return history events for
* @returns a list of all events that fall between the specified date range
*/
public async getAllHistoryBetweenDateRange(
startDate: Timestamp,
endDate: Timestamp
): Promise<DocumentDataWithUid<BookingHistoryEvent>[]> {
const res = await db.bookingHistory
.where("timestamp", ">=", firestoreTimestampToDate(startDate))
.where("timestamp", "<=", firestoreTimestampToDate(endDate))
.get()

return res.docs.map((doc) => {
return { ...doc.data(), id: doc.id }
})
}

/**
* Fetches the **latest** page of booking history events.
*
* @param limit how many history events to fetch, defaults to `100`
* @param startAfter the firebase document snapshot to paginate from
* @returns the page of booking history items and a cursor pointing to the
* last `id` to use for pagination
*/
public async getLatestHistory(
limit: number = 100,
startAfter?: FirebaseFirestore.DocumentSnapshot<
BookingHistoryEvent,
FirebaseFirestore.DocumentData
>
): Promise<
PaginatedFirebaseResponse<DocumentDataWithUid<BookingHistoryEvent>>
> {
const res = await db.bookingHistory
.orderBy("timestamp")
.startAfter(startAfter || 0)
.limit(limit)
.get()

const historyPage: DocumentDataWithUid<BookingHistoryEvent>[] =
res.docs.map((event) => {
return { ...event.data(), id: event.id }
})

return {
data: historyPage,
nextCursor: res.docs[res.docs.length - 1]?.id || undefined
}
}
}

export default BookingHistoryService
Loading