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

Submission of Wave Software Challenge #175

Open
wants to merge 1 commit 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
63 changes: 13 additions & 50 deletions README.markdown
Original file line number Diff line number Diff line change
@@ -1,59 +1,22 @@
# Wave Software Development Challenge
Applicants for the [Software developer](https://wave.bamboohr.co.uk/jobs/view.php?id=1) role at Wave must complete the following challenge, and submit a solution prior to the onsite interview.

The purpose of this exercise is to create something that we can work on together during the onsite. We do this so that you get a chance to collaborate with Wavers during the interview in a situation where you know something better than us (it's your code, after all!)
### Overview:
For this challenge I have utilized Java and Spring Boot. If you are new to these technologies, or need some direction, a good place to start reading my code is in the CsvController.java file.

There isn't a hard deadline for this exercise; take as long as you need to complete it. However, in terms of total time spent actively working on the challenge, we ask that you not spend more than a few hours, as we value your time and are happy to leave things open to discussion in the onsite interview.
### Setup:

Please use whatever programming language and framework you feel the most comfortable with.
1. Install Java (I have run and tested the application using Java 8)
1. Install Maven (Maven is a build and dependency resolution tool)
1. In the root directory of the project, run:
`mvn spring-boot:run`
1. After all dependencies are downloaded and the application starts, it will be available at [http://localhost:8080](http://localhost:8080)

Feel free to email [[email protected]]([email protected]) if you have any questions.
### Accomplishments:

## Project Description
Imagine that Wave has just acquired a new company. Unfortunately, the company has never stored their data in a database, and instead uses a comma separated text file. We need to create a way for the new subsidiary to import their data into a database. Your task is to create a web interface that accepts file uploads, and then stores them in a relational database.
This is a minimal setup for a Spring Boot web application that connects to a relational database. If the requirements were expanded to store more types of data in the database, it would be straight forward to add in more Java classes that represent new tables. The included JUnit test demonstrates how to use Spring repositories for simple CRUD.

### What your web-based application must do:
For the monthly report, it seemed natural to use a Group By query which deviated a little bit from the standard ORM. A SQL database will be more optimized than a web server for aggregating a monthly report such as this, and will reduce the in-memory requirements of the web server.

1. Your app must accept (via a form) a comma separated file with the following columns: date, category, employee name, employee address, expense description, pre-tax amount, tax name, and tax amount.
1. You can make the following assumptions:
1. Columns will always be in that order.
2. There will always be data in each column.
3. There will always be a header line.
The project includes an in-memory relational database, which makes it very easy to test and setup for new developers. Connecting to a remote database should be as easy as adding in the connection details.

An example input file named `data_example.csv` is included in this repo.

1. Your app must parse the given file, and store the information in a relational database.
1. After upload, your application should display a table of the total expenses amount per-month represented by the uploaded file.

Your application should be easy to set up, and should run on either Linux or Mac OS X. It should not require any non open-source software.

There are many ways that this application could be built; we ask that you build it in a way that showcases one of your strengths. If you you enjoy front-end development, do something interesting with the interface. If you like object-oriented design, feel free to dive deeper into the domain model of this problem. We're happy to tweak the requirements slightly if it helps you show off one of your strengths.

### Documentation:

Please modify `README.md` to add:

1. Instructions on how to build/run your application
1. A paragraph or two about what you are particularly proud of in your implementation, and why.

## Submission Instructions

1. Fork this project on github. You will need to create an account if you don't already have one.
1. Complete the project as described below within your fork.
1. Push all of your changes to your fork on github and submit a pull request.
1. You should also email [[email protected]]([email protected]) and your recruiter to let them know you have submitted a solution. Make sure to include your github username in your email (so we can match applicants with pull requests.)

## Alternate Submission Instructions (if you don't want to publicize completing the challenge)
1. Clone the repository.
1. Complete your project as described below within your local repository.
1. Email a patch file to [[email protected]]([email protected])

## Evaluation
Evaluation of your submission will be based on the following criteria.

1. Did you follow the instructions for submission?
1. Did you document your build/deploy instructions and your explanation of what you did well?
1. Were models/entities and other components easily identifiable to the reviewer?
1. What design decisions did you make when designing your models/entities? Why (i.e. were they explained?)
1. Did you separate any concerns in your application? Why or why not?
1. Does your solution use appropriate datatypes for the problem as described?
The front-end is a simple one page application that uses AJAX and a 3rd party table library [handsontable](https://handsontable.com/). For a more complicated web application, it would be appropriate to add in a separate front end framework such as React.
66 changes: 66 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.wave</groupId>
<artifactId>challenge</artifactId>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.5.6.RELEASE</version>
</parent>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-csv</artifactId>
<version>1.5</version>
</dependency>

</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>exec-maven-plugin</artifactId>
<configuration>
<mainClass>com.wave.challenge.Application</mainClass>
<classpathScope>compile</classpathScope>
</configuration>
</plugin>
</plugins>
</build>
</project>
11 changes: 11 additions & 0 deletions src/main/java/com/wave/challenge/Application.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.wave.challenge;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
63 changes: 63 additions & 0 deletions src/main/java/com/wave/challenge/controllers/CsvController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package com.wave.challenge.controllers;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.List;

import org.apache.commons.csv.CSVFormat;
import org.apache.commons.csv.CSVRecord;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.multipart.MultipartFile;

import com.wave.challenge.db.EmployeeExpense;
import com.wave.challenge.db.EmployeeExpenseMonthlyReport;
import com.wave.challenge.db.EmployeeExpenseRepository;

@Controller
public class CsvController {

@Autowired
private EmployeeExpenseRepository employeeExpenseRepository;

@PostMapping(value = "/csv", produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
@ResponseBody
public List<EmployeeExpenseMonthlyReport> handleCsvFileUpload(@RequestParam("csvfile") MultipartFile file) {
saveCsvToDB(file);
return employeeExpenseRepository.getMonthlyExpenseReport();
}

// It would be appropriate to split out CSV to DB Entity mapping logic in another class if the requirements expanded
// It might also be appropriate to parse the CSV on another thread
private void saveCsvToDB(MultipartFile file) {
try {
BufferedReader reader = new BufferedReader(new InputStreamReader(file.getInputStream()));
Iterable<CSVRecord> records = CSVFormat.EXCEL.withFirstRecordAsHeader().parse(reader);
for (CSVRecord record : records) {
String date = record.get("date");
String category = record.get("category");
String employeeName = record.get("employee name");
String employeeAddress = record.get("employee address");
String expenseDescription = record.get("expense description");
String preTaxAmount = record.get("pre-tax amount").replaceAll(",", "");
String taxName = record.get("tax name");
String taxAmount = record.get("tax amount").replaceAll(",", "");
LocalDate parsedDate = LocalDate.parse(date, DateTimeFormatter.ofPattern("M/d/yyyy"));
//TODO: Double types are not appropriate for storing money values
EmployeeExpense expense = new EmployeeExpense(parsedDate, category, employeeName, employeeAddress,
expenseDescription, Double.parseDouble(preTaxAmount), taxName, Double.parseDouble(taxAmount));
employeeExpenseRepository.save(expense);
}
} catch (IOException e) {
//TODO: Provide feedback to user that something went wrong
e.printStackTrace();
}
}
}
12 changes: 12 additions & 0 deletions src/main/java/com/wave/challenge/controllers/IndexController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.wave.challenge.controllers;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;

@Controller
public class IndexController {
@GetMapping("/")
public String index() {
return "index.html";
}
}
46 changes: 46 additions & 0 deletions src/main/java/com/wave/challenge/db/EmployeeExpense.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package com.wave.challenge.db;

import java.time.LocalDate;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;

@Entity
public class EmployeeExpense {
@Id
@GeneratedValue
Long id;

public EmployeeExpense() {}

public EmployeeExpense(LocalDate date, String category, String employeeName, String employeeAddress,
String expenseDescription, Double preTaxAmount, String taxName, Double taxAmount) {
this.date = date;
this.category = category;
this.employeeName = employeeName;
this.employeeAddress = employeeAddress;
this.expenseDescription = expenseDescription;
this.preTaxAmount = preTaxAmount;
this.taxName = taxName;
this.taxAmount = taxAmount;
}

@Column
LocalDate date;
@Column
String category;
@Column
String employeeName;
@Column
String employeeAddress;
@Column
String expenseDescription;
@Column
Double preTaxAmount;
@Column
String taxName;
@Column
Double taxAmount;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package com.wave.challenge.db;

public class EmployeeExpenseMonthlyReport {

private int year;
private int month;
private Double sumPreTaxAmount;
private Double sumTaxAmount;
private Double sumTotal;

public EmployeeExpenseMonthlyReport(int year, int month, Double sumPreTaxAmount,
Double sumTaxAmount) {
this.year = year;
this.month = month;
this.sumPreTaxAmount = sumPreTaxAmount;
this.sumTaxAmount = sumTaxAmount;
this.sumTotal = sumPreTaxAmount + sumTaxAmount;
}

public int getYear() {
return year;
}

public int getMonth() {
return month;
}

public Double getSumPreTaxAmount() {
return sumPreTaxAmount;
}

public Double getSumTaxAmount() {
return sumTaxAmount;
}

public Double getSumTotal() {
return sumTotal;
}

}
17 changes: 17 additions & 0 deletions src/main/java/com/wave/challenge/db/EmployeeExpenseRepository.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.wave.challenge.db;

import java.util.List;

import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.CrudRepository;
import org.springframework.stereotype.Component;

@Component
public interface EmployeeExpenseRepository extends CrudRepository<EmployeeExpense, Long> {
List<EmployeeExpense> findByEmployeeName(String name);

@Query(value = "SELECT "
+ "NEW com.wave.challenge.db.EmployeeExpenseMonthlyReport(YEAR(view.date), MONTH(view.date), SUM(view.preTaxAmount), SUM(view.taxAmount)) "
+ "FROM EmployeeExpense view GROUP BY YEAR(view.date), MONTH(view.date) ORDER BY YEAR(view.date), MONTH(view.date)")
List<EmployeeExpenseMonthlyReport> getMonthlyExpenseReport();
}
21 changes: 21 additions & 0 deletions src/main/java/com/wave/challenge/db/LocalDateConverter.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.wave.challenge.db;

import java.sql.Timestamp;
import java.time.LocalDate;

import javax.persistence.AttributeConverter;
import javax.persistence.Converter;

@Converter(autoApply = true)
public class LocalDateConverter implements AttributeConverter<LocalDate, Timestamp> {

@Override
public Timestamp convertToDatabaseColumn(LocalDate localDate) {
return Timestamp.valueOf(localDate.atStartOfDay());
}

@Override
public LocalDate convertToEntityAttribute(Timestamp sqlTimestamp) {
return sqlTimestamp.toLocalDateTime().toLocalDate();
}
}
4 changes: 4 additions & 0 deletions src/main/resources/application.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Fill in other DB details
# spring.datasource.url=jdbc:mysql://localhost:3306/db_example
# spring.datasource.username=user
# spring.datasource.password=password
17 changes: 17 additions & 0 deletions src/main/resources/static/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<html>
<head>
<link rel="stylesheet" type="text/css" href="https://cdnjs.cloudflare.com/ajax/libs/handsontable/0.38.1/handsontable.full.min.css">
<link rel="stylesheet" type="text/css" href="https://handsontable.com/static/css/main.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/handsontable/0.38.1/handsontable.full.min.js"></script>
<script src="script.js"></script>
</head>
<body>
<h2 id="title">Upload file form</h2>
<form id="csvform" action="csv" method="post" enctype="multipart/form-data" onsubmit="buttonOnClick">
Select CSV to upload:
<input id="fileinput" name="csvFile" type="file">
<input id="submitbutton" name="submit" type="submit" value="Upload CSV" hidden>
</form>
<div id="table"></div>
</body>
</html>
Loading