diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000..5ece1b2 Binary files /dev/null and b/.DS_Store differ diff --git a/README.md b/README.md index ffe80bb..d5796ab 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/calculate_largest_expensors.sql b/calculate_largest_expensors.sql index e69de29..963ed3b 100644 --- a/calculate_largest_expensors.sql +++ b/calculate_largest_expensors.sql @@ -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; diff --git a/create_employees.sql b/create_employees.sql index e69de29..4fd96c4 100644 --- a/create_employees.sql +++ b/create_employees.sql @@ -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); diff --git a/create_expenses.sql b/create_expenses.sql index e69de29..218c0e3 100644 --- a/create_expenses.sql +++ b/create_expenses.sql @@ -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); diff --git a/create_invoices.sql b/create_invoices.sql index e69de29..6f934c9 100644 --- a/create_invoices.sql +++ b/create_invoices.sql @@ -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'); diff --git a/finance/.DS_Store b/finance/.DS_Store new file mode 100644 index 0000000..33d0144 Binary files /dev/null and b/finance/.DS_Store differ diff --git a/find_manager_cycles.sql b/find_manager_cycles.sql index e69de29..bb89ff4 100644 --- a/find_manager_cycles.sql +++ b/find_manager_cycles.sql @@ -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]; diff --git a/generate_supplier_payment_plans.sql b/generate_supplier_payment_plans.sql index e69de29..7933a05 100644 --- a/generate_supplier_payment_plans.sql +++ b/generate_supplier_payment_plans.sql @@ -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