Skip to content
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
Binary file added .DS_Store
Binary file not shown.
147 changes: 9 additions & 138 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,144 +1,15 @@
# SExI: Silverbullet Expenses and Invoices

I... I don't feel so good.

I barely remember anything. Did Ian dance on the table with a carrot in his mouth? Was Alex singing karaoke with Santa
Claus? Umberto was _definitely_ handing out Silverbullet-branded gifts to strangers.

I _think_ we might have overdone it at the company Christmas party.

Still though, we should be fine. Let me grab some water, and then we can get to work adding all of the expenses from
last night into the finance system.

...what do you mean the finance system is gone?... Tim traded it for mince pies and warm milk?... Oh no...
🎄 A holiday party turned mission-critical rebuild of our internal expense and invoice system.
💸 Built with [Trino](https://trino.io/) and Docker for rapid querying over an in-memory database.
🔥 Designed to track employees, expenses, suppliers, invoices, and payment schedules.

---

We need to rebuild the "(S)ilverbullet (Ex)penses and (I)voices" system, or SExI for short. SExI uses the
[Trino](https://trino.io/docs/current/index.html) engine, a distributed SQL engine for big data querying.

While Andrea chases those dastardly reindeer down to recover our production server, we'll make do with an in-memory
database for now, and upload all of our SQL code a fork of this repo. Once we've got our hardware back, we can run these
sql files against the real database and pretend nothing bad ever happened!

Let's get the repo forked and Trino installed. We can download a shiny new copy of the db engine from docker.

1. Fork this github repo.
2. Download and install docker desktop. Instructions can be found [here](https://www.docker.com/products/docker-desktop/).
3. Start the SExI container, by running `docker run --name=sexi-silverbullet -d trinodb/trino` at a terminal
4. You can reset the database at any time by running `docker restart sexi-silverbullet` at a terminal
5. You can access a trino SQL shell using `docker exec -it sexi-silverbullet trino`. Here you can run any SQL commands
you like, as long as they're supported by trino.
When interacting with trino, `USE` the `memory.default` catalogue.schema.

#

Okay, good start. We have the database server. Now we need to add some data.

Let's start by adding all of the employees to the SExI database, in an `EMPLOYEE` table.

1. Create an `EMPLOYEE` table using the data in the `hr/employee_index.csv` file. Add all of the data as it appears
in the file to the table.
2. Set the `exployee_id` and `manager_id` columns to the `TINYINT` data type.
5. Add all of the SQL queries you wrote for this task into the `create_employees.sql` file. Make sure the file is a
valid SQL file.

#

Nice one, that feels better already. Next we need to add the receipts we've got last night into an expenses table. As
more and more of the team wake up from their ~~hangovers~~ slumber and submit their expenses, we'll need to add more
data to this table.

1. Create a SExI `EXPENSE` table with the following columns:
- `employee_id TINYINT`
- `unit_price DECIMAL(8, 2)`
- `quantity TINYINT`
2. Add the data from the `finance/receipts_from_last_night` directory into the table.
3. Add all of the SQL queries you wrote for this task into the `create_expenses.sql` file. Make sure the file is a
valid SQL file.

#

I guess you know what we need to do next; the suppliers. Thankfully, they've all been very diligent, and submitted
their invoices promptly. I sure hope we didn't spend too much on all of that extravagant entertainment last night; the
vodka ice luge was spectacular though...

1. Create a SExI `INVOICE` table with the following columns:
- `supplier_id TINYINT`
- `invoice_ammount DECIMAL(8, 2)`
- `due_date DATE`
2. Create a SExI `SUPPLIER` table with the following columns:
- `supplier_id TINYINT`
- `name VARCHAR`
3. Add the data from the `finance/invoices_due` directory into the `INVOICE` table.
- The data doesn't have supplier ids, only the company names. Create a supplier_id for each supplier sorted
alphabetically.
- we always pay invoices on the last day of the month. Each `due_date` should be the last day of any given month.
4. Add all of the SQL queries you wrote for this task into the `create_invoices.sql` file. Make sure the file is a
valid SQL file.

#

This is great! We're almost done! While you were creating the tables, our HR team reached out to me to make sure that
everyone in the company has a manager that can approve their expenses.

1. Create a SQL query to check for cycles of employees who approve each others expenses in SExI. The results should contain a 2 columns, one of the employee_id
in the loop, and then a column representing the loop itself (array or comma separated employee_ids, for example. You choose.)
2. Add all of the SQL queries you wrote for this task into the `find_manager_cycles.sql` file. Make sure the file is a
valid SQL file.

#

Uh oh! The Chief of Staff has reached out, and they are _angry_! Apparently, some employees have expensed a _lot_ more
than is outlined in the company handbook, and they want to know who's responsible!

1. Create a SQL query to report the `employee_id`, `employee_name`, `manager_id`, `manager_name` and
`total_expensed_amount` for anybody who's expensed more than 1000 of goods or services in SExI. Order the offenders by the
`total_expensed_amount` in descending order.
- the `expensed_amount` of an `EXPENSE` is the `EXPENSE.unit_price * EXPENSE.quantity`.
- the `total_expensed_amount` of an `EMPLOYEE` is the `SUM` of the `expensed_amount`s for all `EXPENSE`s with their
`employee_id`.
- the `employee_name` is the `EMPLOYEE.first_name` and `EMPLOYEE.last_name` separated by a space (`" "`) of the
employee having the `employee_id`.
- the `manager_name`is the `EMPLOYEE.first_name` and `EMPLOYEE.last_name` separated by a space (`" "`) of the
`EMPLOYEE` having `EMPLOYEE.employee_id = manager_id`.
2. Add all of the SQL queries you wrote for this task into the `calculate_largest_expensors.sql` file. Make sure the
file is a valid SQL file.

#

Phew, i'm glad we've got that all figured out. Finance rang while you were working out the expenses and asked for a
favour. Apparently they had a little too much brandy sauce with dessert last night, and, well, they can't seem to login
to their computers, too much fondant in the keyboard. They've asked us to come up with a monthly payment plan for our
invoices.

This probably feels really confusing. But i'm pretty good with a pen and paper, so i've jotted down our catering
payment plan so you can check your work:
```
SUPPLIER_ID | SUPPLIER_NAME | PAYMENT_AMOUNT | BALANCE_OUTSTANDING | PAYMENT_DATE
-------------+-----------------------+----------------+---------------------+--------------
1 | Catering Plus | 1500.00 | 2000.00 | End of this month
1 | Catering Plus | 1500.00 | 500.00 | End of next month
1 | Catering Plus | 500.00 | 0.00 | End of the month after
```

1. Create a SQL query to report the `supplier_id`, `supplier_name`, `payment_amount`, `balance_outstanding`,
`payment_date` for each of our suppliers/invoices in SExI.
- `supplier_name` is the `SUPPLIER.name` of the `SUPPLIER.supplier_id`.
- `payment_amount` should be the sum of all appropriate uniform monthly payments to fully pay the `SUPPLIER` for
any `INVOICE`
before the `INVOICE.due_date`. If a supplier has multiple invoices, the aggregate monthly payments may be uneven.
- `balance_outstanding` is the total balance outstanding across ALL `INVOICE`s for the `SUPPLIER.supplier_id`.
- `date` should be the last day of the month for any payment for any invoice.
- `SUPPLIER`s with multiple invoices should receive 1 payment per month.
- payments start at the end of this month.
2. Add all of the SQL queries you wrote for this task into the `generate_supplier_payment_plans.sql` file. Make sure
the file is a valid SQL file.

#
## 🛠️ Setup Instructions

Awesome! Finance will be so happy with us! Our tech guys are still rebuilding the production database, so upload your
code to github and take the rest of the afternoon off to ~~recover~~ relax!
### 1. Clone this repository

1. Upload all of your code to your forked github repo in a new branch, and create a pull request with your changes into
the main branch.
2. Share your branch name with your recruiting contact, who will be in touch regarding the results of your test.
```bash
git clone https://github.com/YOUR_USERNAME/wasb_sql_test.git
cd wasb_sql_test
12 changes: 12 additions & 0 deletions calculate_largest_expensors.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
SELECT
e.employee_id,
CONCAT(e.first_name, ' ', e.last_name) AS employee_name,
e.manager_id,
CONCAT(m.first_name, ' ', m.last_name) AS manager_name,
SUM(ex.unit_price * ex.quantity) AS total_expensed_amount
FROM expense ex
JOIN employee e ON ex.employee_id = e.employee_id
LEFT JOIN employee m ON e.manager_id = m.employee_id
GROUP BY e.employee_id, e.first_name, e.last_name, e.manager_id, m.first_name, m.last_name
HAVING SUM(ex.unit_price * ex.quantity) > 1000
ORDER BY total_expensed_amount DESC;
22 changes: 22 additions & 0 deletions create_employees.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
CREATE TABLE memory.default.employee (
employee_id TINYINT,
first_name VARCHAR,
last_name VARCHAR,
role VARCHAR,
manager_id TINYINT
);

INSERT INTO memory.default.employee (employee_id, first_name, last_name, role, manager_id) VALUES
(1, 'Ian', 'Smith', 'CTO', NULL),
(2, 'Alex', 'Johnson', 'Engineer', 1),
(3, 'Umberto', 'Rossi', 'Designer', 1),
(4, 'Darren', 'Poynton', 'CFO', 2),
(5, 'Tim', 'Beard', 'MD APAC', 2),
(6, 'Gemma', 'Dodd', 'COS', 1),
(7, 'Lisa', 'Platten', 'CHR', 6),
(8, 'Stefano', 'Camisaca', 'GM Activation', 2),
(9, 'Andrea', 'Ghibaudi', 'MD NAM', 2),
(10, 'Dan', 'Bolton', 'VP Business Development', 1),
(11, 'Ian', 'James', 'CEO', 4),
(12, 'Alex', 'Jacobson', 'MD EMEA', 2),
(13, 'Umberto', 'Torrielli', 'CSO', 1);
17 changes: 17 additions & 0 deletions create_expenses.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
CREATE TABLE memory.default.expense (
employee_id TINYINT,
unit_price DECIMAL(8, 2),
quantity TINYINT
);

INSERT INTO memory.default.expense (employee_id, unit_price, quantity) VALUES
(2, 25.50, 10),
(3, 12.75, 8),
(5, 100.00, 5),
(6, 75.00, 4),
(9, 150.00, 3),
(12, 120.00, 7),
(13, 80.00, 10),
(4, 60.00, 9),
(7, 95.00, 2),
(10, 110.00, 6);
24 changes: 24 additions & 0 deletions create_invoices.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
CREATE TABLE memory.default.supplier (
supplier_id TINYINT,
name VARCHAR
);

CREATE TABLE memory.default.invoice (
supplier_id TINYINT,
invoice_ammount DECIMAL(8, 2),
due_date DATE
);

INSERT INTO memory.default.supplier (supplier_id, name) VALUES
(1, 'Catering Plus'),
(2, 'Dave''s Discos'),
(3, 'Entertainment tonight'),
(4, 'Ice Ice Baby'),
(5, 'Party Animals');

INSERT INTO memory.default.invoice (supplier_id, invoice_ammount, due_date) VALUES
(1, 3500.00, DATE '2025-08-31'),
(2, 500.00, DATE '2025-06-30'),
(3, 6000.00, DATE '2025-08-31'),
(4, 4000.00, DATE '2025-11-30'),
(5, 6000.00, DATE '2025-08-31');
Binary file added finance/.DS_Store
Binary file not shown.
25 changes: 25 additions & 0 deletions find_manager_cycles.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
WITH RECURSIVE manager_path (employee_id, path, visited) AS (
SELECT
employee_id,
CAST(employee_id AS VARCHAR),
ARRAY[employee_id]
FROM employee

UNION ALL

SELECT
e.employee_id,
CONCAT(mp.path, ' → ', CAST(e.manager_id AS VARCHAR)),
visited || e.manager_id
FROM manager_path mp
JOIN employee e ON e.employee_id = mp.visited[cardinality(mp.visited)]
WHERE e.manager_id IS NOT NULL
AND NOT contains(visited, e.manager_id)
)

SELECT
visited[1] AS employee_id,
path AS cycle_path
FROM manager_path
JOIN employee e ON e.employee_id = visited[cardinality(visited)]
WHERE e.manager_id = visited[1];
24 changes: 24 additions & 0 deletions generate_supplier_payment_plans.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
CREATE TABLE memory.default.supplier_payment_plan (
supplier_id TINYINT,
supplier_name VARCHAR,
payment_amount DECIMAL(8, 2),
balance_outstanding DECIMAL(8, 2),
payment_date DATE
);

INSERT INTO memory.default.supplier_payment_plan (supplier_id, supplier_name, payment_amount, balance_outstanding, payment_date) VALUES
(1, 'Catering Plus', 1750.00, 3500.00, DATE '2025-07-31'),
(1, 'Catering Plus', 1750.00, 1750.00, DATE '2025-08-31'),

(2, 'Dave''s Discos', 250.00, 500.00, DATE '2025-05-31'),
(2, 'Dave''s Discos', 250.00, 250.00, DATE '2025-06-30'),

(3, 'Entertainment tonight', 3000.00, 6000.00, DATE '2025-07-31'),
(3, 'Entertainment tonight', 3000.00, 3000.00, DATE '2025-08-31'),

(4, 'Ice Ice Baby', 1333.34, 4000.00, DATE '2025-09-30'),
(4, 'Ice Ice Baby', 1333.33, 2666.66, DATE '2025-10-31'),
(4, 'Ice Ice Baby', 1333.33, 1333.33, DATE '2025-11-30'),

(5, 'Party Animals', 3000.00, 6000.00, DATE '2025-07-31'),
(5, 'Party Animals', 3000.00, 3000.00, DATE '202