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 generate link to book appointment #390

Open
wants to merge 10 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions firebase/functions/src/common/BookAppointmentHelper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,8 @@ export async function bookAppointment(
appointmentToBook.donorDetails = donorDetails;
appointmentToBook.assigningCoordinatorId = coordinatorId;
} else {
appointmentToBook.shareLink = generateRandom();

const donor = await getDonorOrThrow(donorId);

const updateDonorPromise = updateDonorAsync(
Expand Down Expand Up @@ -146,3 +148,11 @@ export function validateBookAppointment(
appointment: appointmentToBook,
};
}

function generateRandom(complexity: number = 4) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not use a library to generate UIDs?

let ret = "";
for (let i = 0; i < complexity; i++) {
ret += (Math.random() + 1).toString(36).substring(2);
}
return ret;
}
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,7 @@ test("Valid manual donor request books appointment with manual donor", async ()
expect(appointment.donorId).toEqual(MANUAL_DONOR_ID);
expect(appointment.assigningCoordinatorId).toEqual(COORDINATOR_ID);
expect(appointment.status).toEqual(AppointmentStatus.BOOKED);
expect(appointment.shareLink).not.toBeDefined();

const bookedAppointment = data.bookedAppointment!;
expect(bookedAppointment.id).toEqual(APPOINTMENT_TO_BOOK_2);
Expand Down
56 changes: 50 additions & 6 deletions firebase/functions/src/dal/AppointmentDataAccessLayer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,16 +113,33 @@ export async function getAppointmentsByHospital(
return toDbAppointments(appointments);
}

export async function getAvailableAppointments() {
const now = new Date();
export async function getAvailableAppointments(
hospitals?: Hospital[],
fromTime?: Date,
toTime?: Date
) {
if (!fromTime) {
fromTime = new Date();
}

const appointments = (await admin
let request = admin
.firestore()
.collection(Collections.APPOINTMENTS)
.where("status", "==", AppointmentStatus.AVAILABLE)
.where("donationStartTime", ">", now)
.orderBy("donationStartTime")
.get()) as FirebaseFirestore.QuerySnapshot<DbAppointment>;
.where("donationStartTime", ">=", fromTime);

if (hospitals) {
request = request.where("hospital", "in", hospitals);
}

if (toTime) {
request = request.where("donationStartTime", "<=", toTime);
}

request = request.orderBy("donationStartTime");

const appointments =
(await request.get()) as FirebaseFirestore.QuerySnapshot<DbAppointment>;

return toDbAppointments(appointments);
}
Expand Down Expand Up @@ -151,6 +168,33 @@ export async function getAppointmentsByStatus(
return toDbAppointments(appointments);
}

export async function getAppointmentByShareLink(
shareLink: string
): Promise<DbAppointment> {
if (!shareLink) {
evbambly marked this conversation as resolved.
Show resolved Hide resolved
throw Error("you have to give a value for share link id");
}

let request = admin
.firestore()
.collection(Collections.APPOINTMENTS)
.where("shareLink", "==", shareLink);

const appointments =
(await request.get()) as FirebaseFirestore.QuerySnapshot<DbAppointment>;

const ret = toDbAppointments(appointments);
if (ret.length > 1) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See previous comment.

throw Error("there is more then one appointment with this share link!");
}

if (ret.length == 0) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See previous comment.

throw Error("There is no appointment with this share link!");
}

return ret[0];
}

export async function getAllAppointments() {
const appointments = (await admin
.firestore()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,8 @@ test("Valid request books appointment", async () => {
const appointment = await getAppointmentByIdOrThrow(APPOINTMENT_TO_BOOK_2);
expect(appointment.donorId).toEqual(DONOR_ID);
expect(appointment.status).toEqual(AppointmentStatus.BOOKED);
expect(appointment.shareLink).toBeDefined();
expect(appointment.shareLink).not.toEqual("");

const data = response as FunctionsApi.BookAppointmentResponse;
expect(data.status).toEqual(FunctionsApi.BookAppointmentStatus.SUCCESS);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,11 @@ import {
export default async function (
request: FunctionsApi.GetAvailableAppointmentsRequest
): Promise<FunctionsApi.GetAvailableAppointmentsResponse> {
const availableAppointments = await getAvailableAppointments();
const availableAppointments = await getAvailableAppointments(
request.hospitals,
request.fromMillis ? new Date(request.fromMillis) : undefined,
request.toMillis ? new Date(request.toMillis) : undefined
);

const result = availableAppointments.map<AvailableAppointment>(
(appointment) => ({
Expand Down
108 changes: 108 additions & 0 deletions firebase/functions/src/donor/GetSharedLinkAppointmentHandler.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import firebaseFunctionsTest from "../testUtils/FirebaseTestUtils";
import {
FunctionsApi,
Hospital,
AppointmentStatus,
} from "@zm-blood-components/common";
import * as Functions from "../index";
import {
deleteAppointmentsByIds,
setAppointment,
} from "../dal/AppointmentDataAccessLayer";
import * as admin from "firebase-admin";
import { DbAppointment } from "../function-types";
import { saveTestDonor } from "../testUtils/TestSamples";
import { deleteDonor } from "../dal/DonorDataAccessLayer";

const wrapped = firebaseFunctionsTest.wrap(
Functions[FunctionsApi.GetSharedLinkAppointmentFunctionName]
);

const CREATING_USER_ID = "GetAvailableAppointmentsHandlerCreatingUserId";
const DONOR_ID = "GetAvailableAppointmentsHandlerDonorId";
const SHARED_DONOR_ID = "SHARED_DONOR_ID";

const SHARE_LINK = "JUST_A_Random_Share_link";

const SHARED_APPOINTMENT = "GetAvailableAppointmentsHandlerAppointment1";
const APPOINTMENT_IN_SAME_TIME = "GetAvailableAppointmentsHandlerAppointment2";
const APPOINTMENT_IN_SAME_TIME_BOOKED =
"GetAvailableAppointmentsHandlerAppointment4";
const DIFFRENT_APPOINTMENT = "GetAvailableAppointmentsHandlerAppointment3";

const ALL_TEST_APPOINTMENTS_IDS = [
SHARED_APPOINTMENT,
APPOINTMENT_IN_SAME_TIME,
APPOINTMENT_IN_SAME_TIME_BOOKED,
DIFFRENT_APPOINTMENT,
];

beforeAll(reset);
afterEach(reset);

async function reset() {
await deleteDonor(SHARED_DONOR_ID);
await deleteDonor(DONOR_ID);
await deleteAppointmentsByIds(ALL_TEST_APPOINTMENTS_IDS);
}

async function saveAppointment(
id: string,
donationStartTime: Date,
booked: boolean,
shared: boolean
) {
const appointment: DbAppointment = {
id: id,
creationTime: admin.firestore.Timestamp.fromDate(donationStartTime),
creatorUserId: CREATING_USER_ID,
donationStartTime: admin.firestore.Timestamp.fromDate(donationStartTime),
hospital: Hospital.ASAF_HAROFE,
donorId: "",
status: booked ? AppointmentStatus.BOOKED : AppointmentStatus.AVAILABLE,
};

if (shared) {
appointment.shareLink = SHARE_LINK;
}

if (booked) {
appointment.donorId = DONOR_ID;
appointment.bookingTime = admin.firestore.Timestamp.now();
}

await setAppointment(appointment);
return appointment;
}

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about the logic test cases,
as written in getAppointmentByShareLink?

test("Not authenticated", async () => {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As per previous, this should not stop users from accessing the API.

await saveTestDonor(DONOR_ID);

await saveAppointment(SHARED_APPOINTMENT, new Date(), true, true);
await saveAppointment(APPOINTMENT_IN_SAME_TIME, new Date(), false, false);

await expect(
wrapped({ donorId: SHARED_DONOR_ID, shareLink: SHARE_LINK })
).rejects.toThrow(Error);
});

test("Returns available appointments is ascending start time order", async () => {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure I understand the test title.

await saveTestDonor(SHARED_DONOR_ID);
await saveTestDonor(DONOR_ID);

await saveAppointment(SHARED_APPOINTMENT, new Date(), true, true);
await saveAppointment(APPOINTMENT_IN_SAME_TIME, new Date(), false, false);

const appointment = (await callTarget()).appointment;

expect(appointment).not.toBeNaN();
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you mean to test "not to be undefined"?

expect(appointment.id).toEqual(SHARED_APPOINTMENT);
expect(appointment.shareLink).toEqual(SHARE_LINK);
});

async function callTarget() {
return (await wrapped(
{ donorId: SHARED_DONOR_ID, shareLink: SHARE_LINK },
{ auth: { uid: SHARED_DONOR_ID } }
)) as FunctionsApi.GetSharedLinkAppointmentResponse;
}
20 changes: 20 additions & 0 deletions firebase/functions/src/donor/GetSharedLinkAppointmentHandler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { getAppointmentByShareLink } from "../dal/AppointmentDataAccessLayer";
import { FunctionsApi } from "@zm-blood-components/common";
import * as DbAppointmentUtils from "../utils/DbAppointmentUtils";

export default async function (
request: FunctionsApi.GetSharedLinkAppointmentRequest,
callerId: string
): Promise<FunctionsApi.GetSharedLinkAppointmentResponse> {
if (callerId !== request.donorId) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could this API be called by an unregistered user?

throw Error("Unauthorized to access user");
}

const appointment = await getAppointmentByShareLink(request.shareLink);

const result = await DbAppointmentUtils.toBookedAppointmentAsync(appointment);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As mentioned in the design doc,
we don't want to expose sensitive information about the shared appointment.
Please create a new object reflecting the data which needs to be transferred.


return {
appointment: result,
};
}
2 changes: 2 additions & 0 deletions firebase/functions/src/function-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,4 +71,6 @@ export type DbAppointment = {
lastChangeTime?: firestore.Timestamp;
lastChangeType?: BookingChange;
donationDoneTimeMillis?: firestore.Timestamp;

shareLink?: string;
};
4 changes: 4 additions & 0 deletions firebase/functions/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import getDonorHandler from "./donor/GetDonorHandler";
import saveDonorHandler from "./donor/SaveDonorHandler";
import getAvailableAppointmentsHandler from "./donor/GetAvailableAppointmentsHandler";
import getDonorAppointmentsHandler from "./donor/GetDonorAppointmentsHandler";
import GetSharedLinkAppointmentHandler from "./donor/GetSharedLinkAppointmentHandler";
import getDonorsHandler from "./coordinator/GetDonorsHandler";
import getBookedDonationsInHospitalHandler from "./reports/BookedDonationInHospitalReportHandler";
import * as admin from "firebase-admin";
Expand Down Expand Up @@ -55,6 +56,9 @@ export const getAvailableAppointments = unauthenticatedHandler(
getAvailableAppointmentsHandler
);
export const getDonorAppointments = handler(getDonorAppointmentsHandler);
export const getSharedLinkAppointment = handler(
GetSharedLinkAppointmentHandler
);
export const unsubscribe = unsubscribeHandler;

// Jobs
Expand Down
1 change: 1 addition & 0 deletions firebase/functions/src/utils/DbAppointmentUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,7 @@ function toBookedAppointmentInternal(
bloodType: bloodType,
status: appointment.status,
recentChangeType: getRecentChangeType(appointment),
shareLink: appointment.shareLink,
};
}

Expand Down
Loading