parent
59c10e48d8
commit
985c2a72d1
@ -1,2 +1,153 @@ |
||||
# test-assignment-enpal |
||||
--- |
||||
gitea: none |
||||
include_toc: true |
||||
--- |
||||
|
||||
## Appointment Booking Challenge |
||||
### Overview |
||||
We want to build an appointment booking system that allows customers to schedule appointments with our sales managers to discuss |
||||
one or more of our products. For the MVP, we will have a website that displays available appointment slots that a customer can choose |
||||
from. |
||||
|
||||
The goal of this project is to implement the backend for this system. We need an endpoint that returns the available appointment slots |
||||
for a customer. |
||||
|
||||
There are a few rules we need to consider when checking for available appointment slots for a customer: |
||||
|
||||
* Each slot corresponds to a one-hour appointment |
||||
* Slots can have overlapping time ranges. For example, it is possible to have the following three slots: |
||||
* 10:30 - 11:30 |
||||
* 11:00 - 12:00 |
||||
* 11:30 - 12:30 |
||||
* A sales manager CANNOT be booked for two overlapping slots at the same time. For example, if a sales manger has a slot booked |
||||
at 10:30 - 11:30, then the 11:00 - 12:00 cannot be booked anymore. |
||||
* Customers are matched to sales managers based on specific criteria. A slot CANNOT be booked by a customer if the sales manager |
||||
does not match any of these three criteria: |
||||
* Language. Currently we have 2 possible languages: German, English |
||||
* Product(s) to discuss. Currently we have 2 possible products: SolarPanels, Heatpumps |
||||
* Internal customer rating. Currently we have 3 possible ratings: Gold, Silver, Bronze. |
||||
* Customers can book one appointment to discuss multiple products |
||||
|
||||
### Requirements |
||||
Design and implement a REST endpoint in any language of your choice that: |
||||
|
||||
* Listens for POST requests on this route: http://localhost:3000/calendar/query |
||||
* Connects to the provided Postgres database instance |
||||
* Receives a request body in this format: |
||||
``` |
||||
{ |
||||
"date": "2024-05-03", |
||||
"products": ["SolarPanels", "Heatpumps"], |
||||
"language": "German", |
||||
"rating": "Gold" |
||||
} |
||||
``` |
||||
* Returns a response with an array of available slots that can be booked by the customer in this format |
||||
``` |
||||
[ |
||||
{ |
||||
"available_count": 1, |
||||
"start_date": "2024-05-03T10:30:00.00Z" |
||||
}, |
||||
{ |
||||
"available_count": 2, |
||||
"start_date": "2024-05-03T12:00:00.00Z" |
||||
} |
||||
] |
||||
``` |
||||
|
||||
#### NOTES: |
||||
|
||||
* You can use any language, framework, library of your choice for this challenge. |
||||
* The system should not book appointments in this challenge; your focus is returning available slots. |
||||
* We provide you with a docker database already populated with data. You are not allowed to modify the database structure of the |
||||
database in any way but you can create indexes or views if you think it is necessary. The docker database can be downloaded |
||||
here: https://[redacted]/Take_Home_Challenge_Resources.zip |
||||
|
||||
#### Database Schema |
||||
|
||||
The provided database has the following schema: |
||||
|
||||
##### Table: sales_managers |
||||
|
||||
| Column Name | Column Type | Comment | |
||||
| ----------- | ----------- | ------- | |
||||
| id (PK) | serial | ID of the sales manager | |
||||
| name | varchar(250) | Full name of sales manager | |
||||
| languages | array(varchar(100)) | List of languages spoken by sales manager | |
||||
| products | array(varchar(100)) | List of products the sales manager can work with | |
||||
| customer_ratings | array(varchar(100)) | List of customer ratings the sales manager can work with | |
||||
|
||||
##### Table: slots |
||||
|
||||
| Column Name | Column Type | Comment | |
||||
| ----------- | ----------- | ------- | |
||||
| id (PK) | serial | ID of the slot | |
||||
| start_date | timestampz | Start date and time of the slot | |
||||
| end_date | timestampz | End date and time of the slot | |
||||
| booked | bool | Value indicating whether the slot has already been booked | |
||||
| sales_manager_id (FK) | integer | ID of the sales manager the slot belongs to | |
||||
|
||||
### Getting Started |
||||
Download the resources for this challenge here https://[redacted]/Take_Home_Challenge_Resources.zip. This is a zip file that contains two folders: |
||||
|
||||
* database: This folder contains a Dockerfile that can be used to start a Postgres database server and an init.sql file that |
||||
initializes the database and preloads it with data. |
||||
* test-app: This folder contains a node application that can be used to run test scenarios to verify your application. |
||||
|
||||
#### Setup the database |
||||
|
||||
Extract the `Take_Home_Challenge_Resources.zip` file and run the following commands in the database folder. This requires |
||||
that you have docker installed on your local environment |
||||
|
||||
``` |
||||
docker build -t enpal-coding-challenge-db . |
||||
docker run --name enpal-coding-challenge-db -p 5432:5432 -d enpal-coding-challenge-db |
||||
``` |
||||
|
||||
Once the docker container is up and running, ensure you can connect to it using your favourite DB query tool (e.g.: DBeaver or pgAdmin). |
||||
|
||||
The default connection string is `postgres://postgres:mypassword123!@localhost:5432/coding-challenge` |
||||
|
||||
If you want to use a local database installation instead, you also can get the `init.sql` file and run it in your local database. |
||||
|
||||
#### Setup tests |
||||
Extract the `Take_Home_Challenge_Resources.zip` file and run the following commands in the `test-app` folder. This requires |
||||
that you have node installed on your local environment |
||||
|
||||
``` |
||||
npm install |
||||
npm run test |
||||
``` |
||||
|
||||
The tests try to connect to an endpoint running on http://localhost:3000/calendar/query and run several test scenarios. Since that is not |
||||
probably running yet the tests will fail. |
||||
|
||||
You can inspect the `test.js` file and see some example requests and the expected responses. |
||||
|
||||
#### Start coding |
||||
|
||||
You can now create an api in your language of choice that fulfils the requirements. |
||||
|
||||
### How to submit the solution |
||||
|
||||
The solution should contain: |
||||
|
||||
* your application code |
||||
* docker setup that starts the database |
||||
* instructions on how to run your application. |
||||
|
||||
You can send this to us as a zip file or push this to a github repository and send us the link. |
||||
|
||||
Your solution must connect to the database in the docker container and then we’ll run the same tests provided to you. We might ALSO |
||||
run additional tests, such as loading thousands of records in the database to assert the application is performant enough. |
||||
|
||||
### Evaluation Criteria |
||||
|
||||
Please note that we will be evaluating your solution not just based on correctness but also on the following criteria. We place a high |
||||
importance on these criteria, and we strongly encourage you to carefully consider them as you develop your solution: |
||||
|
||||
* Accuracy in adhering to the specified rules. |
||||
* Efficiency and performance of the api endpoint. |
||||
* Clarity, readability and testability of the code. |
||||
* Handling of edge cases and error conditions. |
||||
|
@ -0,0 +1,6 @@ |
||||
**/node_modules |
||||
*.log |
||||
.DS_Store |
||||
.git |
||||
.gitignore |
||||
.env |
@ -0,0 +1,16 @@ |
||||
# Use the official PostgreSQL image from the Docker Hub |
||||
FROM postgres:16 |
||||
|
||||
# Add the init.sql script to the Docker image |
||||
COPY init.sql /docker-entrypoint-initdb.d/ |
||||
|
||||
# Set environment variables for PostgreSQL |
||||
ENV POSTGRES_DB=coding-challenge |
||||
ENV POSTGRES_USER=postgres |
||||
ENV POSTGRES_PASSWORD=mypassword123! |
||||
|
||||
# To build and run this container run the following commands |
||||
|
||||
# docker build -t enpal-coding-challenge-db . |
||||
# docker run --name enpal-coding-challenge-db -p 5432:5432 -d enpal-coding-challenge-db |
||||
|
@ -0,0 +1,38 @@ |
||||
create table if not exists sales_managers ( |
||||
id serial primary key not null, |
||||
name varchar(250) not null, |
||||
languages varchar(100)[], |
||||
products varchar(100)[], |
||||
customer_ratings varchar(100)[] |
||||
); |
||||
|
||||
create table if not exists slots ( |
||||
id serial primary key not null, |
||||
start_date timestamptz not null, |
||||
end_date timestamptz not null, |
||||
booked boolean not null default false, |
||||
sales_manager_id int not null references sales_managers(Id) |
||||
); |
||||
|
||||
insert into sales_managers (name, languages, products, customer_ratings) values ('Seller 1', '{"German"}', '{"SolarPanels"}', '{"Bronze"}'); |
||||
insert into sales_managers (name, languages, products, customer_ratings) values ('Seller 2', '{"German", "English"}', '{"SolarPanels", "Heatpumps"}', '{"Gold","Silver","Bronze"}'); |
||||
insert into sales_managers (name, languages, products, customer_ratings) values ('Seller 3', '{"German", "English"}', '{"Heatpumps"}', '{"Gold","Silver","Bronze"}'); |
||||
|
||||
insert into slots (sales_manager_id, booked, start_date, end_date) values (1, false, '2024-05-03T10:30Z', '2024-05-03T11:30Z'); |
||||
insert into slots (sales_manager_id, booked, start_date, end_date) values (1, true, '2024-05-03T11:00Z', '2024-05-03T12:00Z'); |
||||
insert into slots (sales_manager_id, booked, start_date, end_date) values (1, false, '2024-05-03T11:30Z', '2024-05-03T12:30Z'); |
||||
insert into slots (sales_manager_id, booked, start_date, end_date) values (2, false, '2024-05-03T10:30Z', '2024-05-03T11:30Z'); |
||||
insert into slots (sales_manager_id, booked, start_date, end_date) values (2, false, '2024-05-03T11:00Z', '2024-05-03T12:00Z'); |
||||
insert into slots (sales_manager_id, booked, start_date, end_date) values (2, false, '2024-05-03T11:30Z', '2024-05-03T12:30Z'); |
||||
insert into slots (sales_manager_id, booked, start_date, end_date) values (3, true, '2024-05-03T10:30Z', '2024-05-03T11:30Z'); |
||||
insert into slots (sales_manager_id, booked, start_date, end_date) values (3, false, '2024-05-03T11:00Z', '2024-05-03T12:00Z'); |
||||
insert into slots (sales_manager_id, booked, start_date, end_date) values (3, false, '2024-05-03T11:30Z', '2024-05-03T12:30Z'); |
||||
insert into slots (sales_manager_id, booked, start_date, end_date) values (1, false, '2024-05-04T10:30Z', '2024-05-04T11:30Z'); |
||||
insert into slots (sales_manager_id, booked, start_date, end_date) values (1, false, '2024-05-04T11:00Z', '2024-05-04T12:00Z'); |
||||
insert into slots (sales_manager_id, booked, start_date, end_date) values (1, true, '2024-05-04T11:30Z', '2024-05-04T12:30Z'); |
||||
insert into slots (sales_manager_id, booked, start_date, end_date) values (2, true, '2024-05-04T10:30Z', '2024-05-04T11:30Z'); |
||||
insert into slots (sales_manager_id, booked, start_date, end_date) values (2, false, '2024-05-04T11:00Z', '2024-05-04T12:00Z'); |
||||
insert into slots (sales_manager_id, booked, start_date, end_date) values (2, true, '2024-05-04T11:30Z', '2024-05-04T12:30Z'); |
||||
insert into slots (sales_manager_id, booked, start_date, end_date) values (3, true, '2024-05-04T10:30Z', '2024-05-04T11:30Z'); |
||||
insert into slots (sales_manager_id, booked, start_date, end_date) values (3, false, '2024-05-04T11:00Z', '2024-05-04T12:00Z'); |
||||
insert into slots (sales_manager_id, booked, start_date, end_date) values (3, false, '2024-05-04T11:30Z', '2024-05-04T12:30Z'); |
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,18 @@ |
||||
{ |
||||
"name": "coding_challenge_tests", |
||||
"version": "1.0.0", |
||||
"description": "", |
||||
"main": "index.js", |
||||
"type": "module", |
||||
"scripts": { |
||||
"test": "jest" |
||||
}, |
||||
"keywords": [], |
||||
"author": "", |
||||
"license": "ISC", |
||||
"devDependencies": { |
||||
"axios": "^1.7.2", |
||||
"jest": "^29.7.0", |
||||
"pg": "^8.12.0" |
||||
} |
||||
} |
@ -0,0 +1,110 @@ |
||||
const { default: axios } = require("axios"); |
||||
|
||||
jest.setTimeout(600000); |
||||
|
||||
const tests = [ |
||||
[ |
||||
"Monday 2024-05-03, Solar Panels and Heatpumps, German and Gold customer. Only Seller 2 is selectable.", |
||||
{ |
||||
input: { |
||||
"date": "2024-05-03", |
||||
"products": ["SolarPanels", "Heatpumps"], |
||||
"language": "German", |
||||
"rating": "Gold" |
||||
}, |
||||
expectedResult: [ |
||||
{ "start_date": "2024-05-03T10:30:00.000Z", "available_count": 1 }, |
||||
{ "start_date": "2024-05-03T11:00:00.000Z", "available_count": 1 }, |
||||
{ "start_date": "2024-05-03T11:30:00.000Z", "available_count": 1 }, |
||||
]
|
||||
} |
||||
], |
||||
[ |
||||
"Monday 2024-05-03, Heatpumps, English and Silver customer. Both Seller 2 and Seller 3 are selectable.", |
||||
{ |
||||
input: { |
||||
"date": "2024-05-03", |
||||
"products": ["Heatpumps"], |
||||
"language": "English", |
||||
"rating": "Silver" |
||||
}, |
||||
expectedResult: [ |
||||
{ "start_date": "2024-05-03T10:30:00.000Z", "available_count": 1 }, |
||||
{ "start_date": "2024-05-03T11:00:00.000Z", "available_count": 1 }, |
||||
{ "start_date": "2024-05-03T11:30:00.000Z", "available_count": 2 }, |
||||
]
|
||||
} |
||||
], |
||||
[ |
||||
"Monday 2024-05-03, SolarPanels, German and Bronze customer. All Seller 1 and 2 are selectable, but Seller 1 does not have available slots.", |
||||
{ |
||||
input: { |
||||
"date": "2024-05-03", |
||||
"products": ["SolarPanels"], |
||||
"language": "German", |
||||
"rating": "Bronze" |
||||
}, |
||||
expectedResult: [ |
||||
{ "start_date": "2024-05-03T10:30:00.000Z", "available_count": 1 }, |
||||
{ "start_date": "2024-05-03T11:00:00.000Z", "available_count": 1 }, |
||||
{ "start_date": "2024-05-03T11:30:00.000Z", "available_count": 1 }, |
||||
]
|
||||
} |
||||
], |
||||
[ |
||||
"Tuesday 2024-05-04, Solar Panels and Heatpumps, German and Gold customer. Only Seller 2 is selectable, but it is fully booked", |
||||
{ |
||||
input: { |
||||
"date": "2024-05-04", |
||||
"products": ["SolarPanels", "Heatpumps"], |
||||
"language": "German", |
||||
"rating": "Gold" |
||||
}, |
||||
expectedResult: []
|
||||
} |
||||
], |
||||
[ |
||||
"Tuesday 2024-05-04, Heatpumps, English and Silver customer. Both Seller 2 and Seller 3 are selectable, but Seller 2 is fully booked.", |
||||
{ |
||||
input: { |
||||
"date": "2024-05-04", |
||||
"products": ["Heatpumps"], |
||||
"language": "English", |
||||
"rating": "Silver" |
||||
}, |
||||
expectedResult: [ |
||||
{ "start_date": "2024-05-04T11:30:00.000Z", "available_count": 1 }, |
||||
]
|
||||
} |
||||
], |
||||
[ |
||||
"Monday 2024-05-03, SolarPanels, German and Bronze customer. Seller 1 and 2 are selectable, but Seller 2 is fully booked", |
||||
{ |
||||
input: { |
||||
"date": "2024-05-04", |
||||
"products": ["SolarPanels"], |
||||
"language": "German", |
||||
"rating": "Bronze" |
||||
}, |
||||
expectedResult: [ |
||||
{ "start_date": "2024-05-04T10:30:00.000Z", "available_count": 1 }, |
||||
]
|
||||
} |
||||
], |
||||
]; |
||||
|
||||
describe("Coding challenge calendar tests", () => { |
||||
test.each(tests)( |
||||
"%s", |
||||
async (_, test_data) => { |
||||
const response = await axios.post("http://localhost:3000/calendar/query", test_data.input); |
||||
expect(response.status).toBe(200); |
||||
expect(response.data.length).toBe(test_data.expectedResult.length); |
||||
|
||||
for (let i = 0; i < test_data.expectedResult.length; i++) { |
||||
expect(response.data[i].available_count).toBe(test_data.expectedResult[i].available_count); |
||||
expect(response.data[i].start_date).toBe(test_data.expectedResult[i].start_date); |
||||
}
|
||||
} |
||||
); |
||||
}); |
Loading…
Reference in new issue