src/main/java/io/budgetapp/model/Group.java
public class Group implements Serializable {

    private static final long serialVersionUID = -3295824982209236728L;

    private Long id;
    private String name;
    private CategoryType type;
    private double budget;
    private double spent;
    private List<Budget> budgets;

    public Group(Long id, String name) {
        setId(id);
        setName(name);
    }

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public CategoryType getType() {
        return type;
    }

    public void setType(CategoryType type) {
        this.type = type;
    }

    public double getBudget() {
        return budget;
    }

    public void setBudget(double budget) {
        this.budget = budget;
    }

    public double getSpent() {
        return spent;
    }

    public void setSpent(double spent) {
        this.spent = spent;
    }

    public List<Budget> getBudgets() {
        return budgets;
    }

    public void setBudgets(List<Budget> budgets) {
        this.budgets = budgets;
    }
}
src/main/java/io/budgetapp/dao/CategoryDAO.java
public class CategoryDAO extends DefaultDAO<Category> {

    private static final Logger LOGGER = LoggerFactory.getLogger(CategoryDAO.class);

    private final AppConfiguration configuration;

    public CategoryDAO(SessionFactory sessionFactory, AppConfiguration configuration) {
        super(sessionFactory);
        this.configuration = configuration;
    }

    public List<Category> findCategories(User user) {
        Criteria criteria = userCriteria(user);
        criteria.addOrder(Order.desc("type"));
        return list(criteria);
    }

    public Collection<Category> addDefaultCategories(User user) {
        Collection<Category> categories = new ArrayList<>(configuration.getCategories().size());
        configuration.getCategories()
                .forEach(c -> categories.add(addCategory(user, category(c))));
        return categories;
    }

    public Category addCategory(User user, Category category) {
        LOGGER.debug("Add new category {}", category);
        category.setUser(user);
        return persist(category);
    }

    public Category findById(long categoryId) {
        Category category = get(categoryId);
        if(category == null) {
            throw new NotFoundException();
        }

        return category;
    }

    private Category category(Category ori) {
        Category category = new Category();
        category.setName(ori.getName());
        category.setType(ori.getType());
        return category;
    }

    public Category find(User user, long categoryId) {
        Criteria criteria = userCriteria(user);
        criteria.add(Restrictions.eq("id", categoryId));

        return singleResult(criteria);
    }

    private Criteria defaultCriteria() {
        return criteria();
    }

    private Criteria userCriteria(User user) {
        Criteria criteria = defaultCriteria();
        criteria.add(Restrictions.eq("user", user));
        return criteria;
    }

    public void delete(Category category) {
        currentSession().delete(category);
    }

    public List<String> findSuggestions(User user, String q) {
        q = q == null? "": q.toLowerCase();

        Query<String> query = currentSession().createQuery("SELECT c.name FROM Category c WHERE c.user != :user AND LOWER(c.name) LIKE :q", String.class);
        query
                .setParameter("user", user)
                .setParameter("q", "%" + q + "%");

        return query.list();
    }
}
src/main/java/io/budgetapp/dao/BudgetDAO.java
public class BudgetDAO extends AbstractDAO<Budget> {

    private static final Logger LOGGER = LoggerFactory.getLogger(BudgetDAO.class);

    private final AppConfiguration configuration;

    public BudgetDAO(SessionFactory sessionFactory, AppConfiguration configuration) {
        super(sessionFactory);
        this.configuration = configuration;
    }

    /**
     * add budget for a given user
     * @param user owner
     * @param budget new budget
     * @return new budget
     */
    public Budget addBudget(User user, Budget budget) {
        LOGGER.debug("User {} add budget {}", user, budget);
        if(budget.getPeriod() == null) {
            budget.setPeriod(Util.currentYearMonth());
        }
        budget.setUser(user);
        return persist(budget);
    }

    /**
     * find budgets for a given user for current month-year
     * @param user
     * @return
     */
    public List<Budget> findBudgets(User user) {
        LocalDate now = LocalDate.now();
        return findBudgets(user, now.getMonthValue(), now.getYear(), false);
    }

    /**
     * find budgets for a given user for given month-year
     * @param user
     * @param month
     * @param year
     * @param lazy
     * @return
     */
    public List<Budget> findBudgets(User user, int month, int year, boolean lazy) {
        LOGGER.debug("Find budgets by user {} by date {}-{}", user, month, year);
        Date yearMonth = Util.yearMonthDate(month, year);
        Criteria criteria = criteria();
        if(!lazy) {
            criteria.setFetchMode("category", FetchMode.JOIN);
        }
        criteria.add(Restrictions.eq("user", user));
        criteria.add(Restrictions.eq("period", yearMonth));
        criteria.addOrder(Order.asc("id"));
        return list(criteria);
    }

    public Date findLatestBudget(User user) {
        LOGGER.debug("Find latest budget by user {}", user);
        Criteria criteria = criteria();
        criteria.add(Restrictions.eq("user", user));
        criteria.setProjection(Projections.max("createdAt"));
        criteria.setMaxResults(1);
        return (Date)criteria.uniqueResult();
    }

    /**
     * Throws an error if the budget does not belong to the user
     * @param user
     * @param budget
     * @throws AccessDeniedException
     */
    private void checkBudget(User user, Budget budget) throws AccessDeniedException {
        if(!Objects.equals(user.getId(), budget.getUser().getId())) {
            throw new AccessDeniedException();
        }
    }

    /**
     * find budget by given id
     * @param budgetId
     * @return
     */
    public Budget findById(long budgetId) {
        Budget budget = get(budgetId);

        if(budget == null) {
            throw new NotFoundException();
        }
        return budget;
    }

    /**
     * find budget for given user and id
     * @param user
     * @param budgetId
     * @return
     */
    public Budget findById(User user, Long budgetId) {
        Budget budget = findById(budgetId);
        checkBudget(user, budget);
        return budget;
    }

    /**
     * find all budgets for given ids
     * @param budgetIds
     * @return
     */
    public List<Budget> findByIds(Collection<Long> budgetIds) {
        Criteria criteria = criteria();
        criteria.add(Restrictions.in("id", budgetIds));
        return list(criteria);
    }

    /**
     * find all budgets for given user and ids
     * @param user
     * @param budgetIds
     * @return
     */
    public List<Budget> findByIds(User user, Collection<Long> budgetIds) {
        List<Budget> budgets = findByIds(budgetIds);
        for (Budget budget : budgets) {
            checkBudget(user, budget);
        }
        return budgets;
    }

    public void update(Budget budget) {
        persist(budget);
    }

    public void delete(Budget budget) {
        currentSession().delete(budget);
    }
    public Map<String, List<Budget>> findDefaultBudgets() {
        return configuration.getBudgets();
    }

    public List<Budget> findByRange(User user, int startMonth, int startYear, int endMonth, int endYear) {
        Date start = Util.yearMonthDate(startMonth, startYear);
        Date end = Util.yearMonthDate(endMonth, endYear);
        Query<Budget> query = query("FROM Budget b WHERE b.user = :user AND b.period BETWEEN :start AND :end");
        query
                .setParameter("user", user)
                .setParameter("start", start)
                .setParameter("end", end);

        return list(query);
    }

    public Budget findByBudgetType(Long budgetTypeId) {
        Date now = Util.currentYearMonth();
        Query<Budget> query = query("FROM Budget b WHERE b.budgetType.id = :budgetTypeId AND b.period = :period");
        query
                .setParameter("budgetTypeId", budgetTypeId)
                .setParameter("period", now);
        return uniqueResult(query);
    }

    public List<Budget> findByUserAndCategory(User user, long categoryId) {
        Criteria criteria = userCriteria(user);
        criteria.add(Restrictions.eq("category.id", categoryId));
        criteria.add(Restrictions.eq("period", Util.currentYearMonth()));
        return list(criteria);
    }

    public List<String> findSuggestions(User user, String q) {
        q = q == null? "": q.toLowerCase();

        Query<String> query = currentSession().createQuery("SELECT b.name FROM Budget l WHERE b.user != :user AND LOWER(b.name) LIKE :q", String.class);
        query
                .setParameter("user", user)
                .setParameter("q", "%" + q + "%");

        return query.list();
    }

    private Criteria defaultCriteria() {
        return criteria();
    }

    private Criteria userCriteria(User user) {
        Criteria criteria = defaultCriteria();
        criteria.add(Restrictions.eq("user", user));
        return criteria;
    }

}
src/main/java/io/budgetapp/model/form/LoginForm.java
@JsonIgnoreProperties(ignoreUnknown = true)
public class LoginForm implements Serializable {

    private static final long serialVersionUID = -1155392015861302100L;

    private String username;
    private String password;

    // Jackson required
    public LoginForm() {
    }

    public LoginForm(String username, String password) {
        this.username = username;
        this.password = password;
    }

    @NotBlank(message = "{validation.username.required}")
    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    @NotBlank(message = "{validation.password.required}")
    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    @Override
    public String toString() {
        final StringBuilder sb = new StringBuilder("LoginForm{");
        sb.append("username='").append(username).append('\'');
        sb.append(", password='").append(password).append('\'');
        sb.append('}');
        return sb.toString();
    }
}
src/main/java/io/budgetapp/model/User.java
@Entity
@Table(name = "users")
@JsonIgnoreProperties(ignoreUnknown = true)
public class User implements Principal, Serializable {

    private static final long serialVersionUID = 3868269731826822792L;
    private Long id;
    private String username;
    private String password;
    private String name;
    private Date createdAt;
    private String currency;

    // not in DB
    private String token;
    // end

    public User() {
    }

    public User(long id) {
        this.id = id;
    }

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    @JsonIgnore
    public String getPassword() {
        return password;
    }

    @Override
    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    @Temporal(TemporalType.TIMESTAMP)
    @Column(name = "created_at", insertable = false, nullable = false, updatable = false)
    public Date getCreatedAt() {
        return createdAt;
    }

    public void setCreatedAt(Date createdAt) {
        this.createdAt = createdAt;
    }

    public String getCurrency() {
        return currency;
    }

    public void setCurrency(String currency) {
        this.currency = currency;
    }

    // not in DB
    @Transient
    @JsonProperty("avatar")
    public String getAvatar() {
        return "https://www.gravatar.com/avatar/" + MD5Util.computeMD5(getUsername());
    }

    @Transient
    @JsonProperty("token")
    public String getToken() {
        return token;
    }

    public void setToken(String token) {
        this.token = token;
    }
    // end

    @Override
    public String toString() {
        final StringBuilder sb = new StringBuilder("User{");
        sb.append("id=").append(id);
        sb.append(", username='").append(username).append('\'');
        sb.append('}');
        return sb.toString();
    }
}
src/main/java/io/budgetapp/dao/AuthTokenDAO.java
public class AuthTokenDAO extends AbstractDAO<AuthToken> {

    public AuthTokenDAO(SessionFactory sessionFactory) {
        super(sessionFactory);
    }

    public AuthToken add(User user) {
        AuthToken authToken = new AuthToken();
        authToken.setUser(user);
        authToken.setToken(newToken());
        return persist(authToken);
    }

    public Optional<AuthToken> find(String token) {
        Criteria criteria = criteria();
        criteria.add(Restrictions.eq("token", token));
        return Optional.ofNullable(uniqueResult(criteria));
    }

    public List<AuthToken> findByUser(User user) {
        Criteria criteria = criteria();
        criteria.add(Restrictions.eq("user", user));
        return list(criteria);
    }

    private String newToken() {
        return UUID.randomUUID().toString();
    }
}
src/main/java/io/budgetapp/crypto/PasswordEncoder.java
public class PasswordEncoder {

    private static final int DEFAULT_ITERATIONS = 1024;
    private static final String DEFAULT_ALGORITHM = "SHA-256";

    private final SaltGenerator saltGenerator;
    private final Digester digester;
    private final byte[] secret;

    public PasswordEncoder() {
        this("");
    }

    public PasswordEncoder(String secret) {
        this.digester = new Digester(DEFAULT_ALGORITHM, DEFAULT_ITERATIONS);
        this.secret = Utf8.encode(secret);
        this.saltGenerator = new SaltGenerator();
    }

    public String encode(CharSequence rawPassword) {
        return encode(rawPassword, saltGenerator.generateKey());
    }

    public boolean matches(CharSequence rawPassword, String encodedPassword) {
        byte[] digested = decode(encodedPassword);
        byte[] salt = subArray(digested, 0, saltGenerator.getKeyLength());
        return matches(digested, digest(rawPassword, salt));
    }

    /**
     * Constant time comparison to prevent against timing attacks.
     */
    private boolean matches(byte[] expected, byte[] actual) {
        if (expected.length != actual.length) {
            return false;
        }

        int result = 0;
        for (int i = 0; i < expected.length; i++) {
            result |= expected[i] ^ actual[i];
        }
        return result == 0;
    }

    private String encode(CharSequence rawPassword, byte[] salt) {
        byte[] digest = digest(rawPassword, salt);
        return new String(Hex.encode(digest));
    }

    private byte[] decode(CharSequence encodedPassword) {
        return Hex.decode(encodedPassword);
    }


    private byte[] digest(CharSequence rawPassword, byte[] salt) {
        byte[] digest = digester.digest(concatenate(salt, secret, Utf8.encode(rawPassword)));
        return concatenate(salt, digest);
    }

    /**
     * Combine the individual byte arrays into one array.
     */
    public static byte[] concatenate(byte[]... arrays) {
        int length = 0;
        for (byte[] array : arrays) {
            length += array.length;
        }
        byte[] newArray = new byte[length];
        int destPos = 0;
        for (byte[] array : arrays) {
            System.arraycopy(array, 0, newArray, destPos, array.length);
            destPos += array.length;
        }
        return newArray;
    }

    /**
     * Extract a sub array of bytes out of the byte array.
     * @param array the byte array to extract from
     * @param beginIndex the beginning index of the sub array, inclusive
     * @param endIndex the ending index of the sub array, exclusive
     */
    public static byte[] subArray(byte[] array, int beginIndex, int endIndex) {
        int length = endIndex - beginIndex;
        byte[] subarray = new byte[length];
        System.arraycopy(array, beginIndex, subarray, 0, length);
        return subarray;
    }
}
src/main/java/io/budgetapp/model/form/SignUpForm.java
public class SignUpForm implements Serializable {

    private static final long serialVersionUID = -3387043933342205884L;

    private String username;
    private String password;

    @NotBlank(message = "{validation.username.required}")
    @Email(message = "{validation.username.invalid}")
    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    @NotBlank(message = "{validation.password.required}")
    @Size.List({@Size(min = 6, message = "{validation.password.length}")})
    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    @Override
    public String toString() {
        final StringBuilder sb = new StringBuilder("SignUpForm{");
        sb.append("username='").append(username).append('\'');
        sb.append(", password='").append(password).append('\'');
        sb.append('}');
        return sb.toString();
    }
}
src/main/java/io/budgetapp/model/form/recurring/AddRecurringForm.java
@JsonIgnoreProperties(ignoreUnknown = true)
public class AddRecurringForm implements Serializable {

    private static final long serialVersionUID = -3317443535487916735L;

    private Long budgetId;
    private Double amount;
    private Date recurringAt;
    private RecurringType recurringType;
    private String remark;

    @NotNull(message = "{validation.budget.required}")
    public Long getBudgetId() {
        return budgetId;
    }

    public void setBudgetId(Long budgetId) {
        this.budgetId = budgetId;
    }

    @NotNull(message = "{validation.amount.required}")
    public Double getAmount() {
        return amount;
    }

    public void setAmount(Double amount) {
        this.amount = amount;
    }

    @NotNull(message = "{validation.recurringAt.required}")
    public Date getRecurringAt() {
        return recurringAt;
    }

    public void setRecurringAt(Date recurringAt) {
        this.recurringAt = recurringAt;
    }

    @NotNull(message = "{validation.recurringType.required}")
    public RecurringType getRecurringType() {
        return recurringType;
    }

    public void setRecurringType(RecurringType recurringType) {
        this.recurringType = recurringType;
    }

    public String getRemark() {
        return remark;
    }

    public void setRemark(String remark) {
        this.remark = remark;
    }
}
src/main/java/io/budgetapp/model/PointType.java
public enum PointType {

    TRANSACTIONS,
    CATEGORY,
    MONTHLY_SPEND,
    MONTHLY_REFUND,
    MONTHLY_BUDGET

}
src/main/java/io/budgetapp/model/Transaction.java
@Entity
@Table(name = "transactions")
public class Transaction implements Serializable {

    private static final long serialVersionUID = 2625666273036891436L;

    private Long id;
    private String name;
    private double amount;
    private String remark;
    private boolean auto;
    private Date transactionOn;
    private Date createdAt;
    private Budget budget;
    private Recurring recurring;

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    @Column(nullable = false, updatable = false)
    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    @Column(nullable = false, updatable = false)
    public double getAmount() {
        return amount;
    }

    public void setAmount(double amount) {
        this.amount = amount;
    }

    @Column(updatable = false)
    public String getRemark() {
        return remark;
    }

    public void setRemark(String remark) {
        this.remark = remark;
    }

    public boolean isAuto() {
        return auto;
    }

    public void setAuto(boolean auto) {
        this.auto = auto;
    }

    @Temporal(TemporalType.TIMESTAMP)
    @Column(name = "transaction_on", nullable = false, updatable = false)
    public Date getTransactionOn() {
        return transactionOn;
    }

    public void setTransactionOn(Date transactionOn) {
        this.transactionOn = transactionOn;
    }

    @Temporal(TemporalType.TIMESTAMP)
    @Column(name = "created_at", insertable = false, nullable = false, updatable = false)
    public Date getCreatedAt() {
        return createdAt;
    }

    public void setCreatedAt(Date createdAt) {
        this.createdAt = createdAt;
    }

    @JoinColumn(updatable = false)
    @ManyToOne(optional = false, fetch = FetchType.LAZY)
    public Budget getBudget() {
        return budget;
    }

    public void setBudget(Budget budget) {
        this.budget = budget;
    }

    @JoinColumn(updatable = false)
    @ManyToOne(fetch = FetchType.LAZY)
    public Recurring getRecurring() {
        return recurring;
    }

    public void setRecurring(Recurring recurring) {
        this.recurring = recurring;
    }

    @PrePersist
    void perPersist() {
        if (transactionOn == null) {
            transactionOn = new Date();
        }
    }

    @Override
    public String toString() {
        final StringBuilder sb = new StringBuilder("Transaction{");
        sb.append("id=").append(id);
        sb.append(", amount=").append(amount);
        sb.append(", remark='").append(remark).append('\'');
        sb.append(", transactionOn=").append(transactionOn);
        sb.append(", createdAt=").append(createdAt);
        sb.append(", budget=").append(budget);
        sb.append('}');
        return sb.toString();
    }
}
src/main/java/io/budgetapp/model/Budget.java
@Entity
@Table(name = "budgets")
public class Budget implements Serializable {

    private static final long serialVersionUID = 2625666273036891436L;

    private Long id;
    private String name;
    private double projected;
    private double actual;
    private Date period;
    private Date createdAt;
    private User user;
    private Category category;
    private BudgetType budgetType;

    public Budget() {
    }

    public Budget(long budgetId) {
        this.id = budgetId;
    }

    public Budget(AddBudgetForm budgetForm) {
        setName(budgetForm.getName());
        setProjected(budgetForm.getProjected());
        setCategory(new Category(budgetForm.getCategoryId()));
    }

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public double getProjected() {
        return projected;
    }

    public void setProjected(double projected) {
        this.projected = projected;
    }

    public double getActual() {
        return actual;
    }

    public void setActual(double actual) {
        this.actual = actual;
    }


    @Temporal(TemporalType.TIMESTAMP)
    @Column(name = "period_on", nullable = false, updatable = false)
    public Date getPeriod() {
        return period;
    }

    public void setPeriod(Date period) {
        this.period = period;
    }

    @Temporal(TemporalType.TIMESTAMP)
    @Column(name = "created_at", insertable = false, nullable = false, updatable = false)
    public Date getCreatedAt() {
        return createdAt;
    }

    public void setCreatedAt(Date createdAt) {
        this.createdAt = createdAt;
    }

    @JsonIgnore
    @JoinColumn(updatable = false)
    @ManyToOne(optional = false, fetch = FetchType.LAZY)
    public User getUser() {
        return user;
    }

    public void setUser(User user) {
        this.user = user;
    }

    @JoinColumn(updatable = false)
    @ManyToOne(optional = false, fetch = FetchType.LAZY)
    public Category getCategory() {
        return category;
    }

    public void setCategory(Category category) {
        this.category = category;
    }

    @JoinColumn(name = "type_id", updatable = false)
    @ManyToOne(optional = false, fetch = FetchType.LAZY)
    public BudgetType getBudgetType() {
        return budgetType;
    }

    public void setBudgetType(BudgetType budgetType) {
        this.budgetType = budgetType;
    }

    @Override
    public String toString() {
        final StringBuilder sb = new StringBuilder("Budget{");
        sb.append("id=").append(id);
        sb.append(", actual=").append(actual);
        sb.append(", name='").append(name).append('\'');
        sb.append(", createdAt=").append(createdAt);
        sb.append(", user=").append(user);
        sb.append(", category=").append(category);
        sb.append('}');
        return sb.toString();
    }
}
src/main/java/io/budgetapp/dao/BudgetTypeDAO.java
public class BudgetTypeDAO extends AbstractDAO<BudgetType> {

    public BudgetTypeDAO(SessionFactory sessionFactory) {
        super(sessionFactory);
    }

    public BudgetType addBudgetType() {
        BudgetType budgetType = new BudgetType();
        return persist(budgetType);
    }
}
src/main/java/io/budgetapp/application/NotFoundException.java
public class NotFoundException extends RuntimeException {
}
src/main/java/io/budgetapp/dao/TransactionDAO.java
public class TransactionDAO extends AbstractDAO<Transaction> {

    private static final Logger LOGGER = LoggerFactory.getLogger(TransactionDAO.class);

    public TransactionDAO(SessionFactory sessionFactory) {
        super(sessionFactory);
    }

    public Transaction addTransaction(Transaction transaction) {
        LOGGER.debug("Add transaction {}", transaction);
        return persist(transaction);
    }

    public List<Transaction> addTransactions(List<Transaction> transactions) {
        return transactions
                .stream()
                .map(this::addTransaction)
                .collect(Collectors.toList());
    }

    public List<Transaction> find(User user, Integer limit) {
        Query<Transaction> query = query("FROM Transaction t WHERE t.budget.user = :user ORDER BY t.transactionOn DESC, t.id ASC");
        query.setParameter("user", user);
        query.setMaxResults(limit);
        return list(query);
    }

    public Transaction findById(long id) {
        Transaction transaction = get(id);
        if(transaction == null) {
            throw new NotFoundException();
        }

        return transaction;
    }

    public Optional<Transaction> findById(User user, long id) {
        Query<Transaction> query = query("FROM Transaction t WHERE t.id = :id AND t.budget.user = :user");
        query.setParameter("user", user);
        query.setParameter("id", id);
        Transaction result = query.uniqueResult();
        return Optional.ofNullable(result);
    }

    public List<Transaction> findByBudget(User user, long budgetId) {
        Criteria criteria = defaultCriteria();
        criteria.createAlias("t.budget", "budget");

        criteria.add(Restrictions.eq("budget.id", budgetId));
        criteria.add(Restrictions.eq("budget.user", user));
        return list(criteria);
    }

    public List<Transaction> findByRecurring(User user, long recurringId) {
        Query<Transaction> query = query("FROM Transaction t WHERE t.budget.user = :user AND t.recurring.id = :recurringId ORDER BY t.transactionOn DESC, t.id ASC");
        query.setParameter("user", user);
        query.setParameter("recurringId", recurringId);
        return list(query);
    }

    public List<Transaction> findByRange(User user, Date start, Date end) {
        Query<Transaction> query = query("FROM Transaction t WHERE t.budget.user = :user AND t.transactionOn BETWEEN :start AND :end ORDER BY t.transactionOn DESC, t.id ASC");
        query
                .setParameter("user", user)
                .setParameter("start", start)
                .setParameter("end", end);

        return list(query);
    }

    public List<Transaction> findTransactions(User user, SearchFilter filter) {
        Criteria criteria = defaultCriteria();
        criteria.createAlias("t.budget", "budget");

        criteria.add(Restrictions.eq("budget.user", user));

        if(filter.isAmountRange()) {
            criteria.add(Restrictions.between("amount", filter.getMinAmount(), filter.getMaxAmount()));
        } else if(filter.getMinAmount() != null) {
            criteria.add(Restrictions.ge("amount", filter.getMinAmount()));
        } else if(filter.getMaxAmount() != null) {
            criteria.add(Restrictions.le("amount", filter.getMaxAmount()));
        }

        if(filter.isDateRange()) {
            criteria.add(Restrictions.between("transactionOn", filter.getStartOn(), filter.getEndOn()));
        } else if(filter.getStartOn() != null) {
            criteria.add(Restrictions.ge("transactionOn", filter.getStartOn()));
        } else if(filter.getEndOn() != null) {
            criteria.add(Restrictions.le("transactionOn", filter.getEndOn()));
        }

        if(Boolean.TRUE.equals(filter.getAuto())) {
            criteria.add(Restrictions.eq("auto", Boolean.TRUE));
        }

        return list(criteria);
    }


    private Criteria defaultCriteria() {
        Criteria criteria = currentSession().createCriteria(Transaction.class, "t");
        criteria.addOrder(Order.desc("transactionOn"));
        criteria.addOrder(Order.desc("id"));
        return criteria;
    }

    public void delete(Transaction transaction) {
        currentSession().delete(transaction);
    }
}
src/main/java/io/budgetapp/model/form/user/Password.java
@JsonIgnoreProperties(ignoreUnknown = true)
public class Password implements Serializable {

    private static final long serialVersionUID = 8359006914394898221L;

    private String original;
    private String password;
    private String confirm;

    @NotBlank(message = "{validation.password.original.required}")
    public String getOriginal() {
        return original;
    }

    public void setOriginal(String original) {
        this.original = original;
    }

    @NotBlank(message = "{validation.password.required}")
    @Size.List({@Size(min = 6, message = "{validation.password.length}")})
    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    @NotBlank(message = "{validation.password.confirm.required}")
    public String getConfirm() {
        return confirm;
    }

    public void setConfirm(String confirm) {
        this.confirm = confirm;
    }
}
src/main/java/io/budgetapp/model/Category.java
@Entity
@Table(name = "categories")
public class Category implements Serializable {

    private static final long serialVersionUID = -8472850956290096457L;

    private Long id;
    private String name;
    private CategoryType type;
    private Date createdAt;
    private User user;

    public Category() {
    }

    public Category(long id) {
        this.id = id;
    }

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    @NotEmpty(message = "{validation.name.required}")
    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    @NotNull(message = "{validation.categoryType.required}")
    @Enumerated(EnumType.STRING)
    @Column(updatable = false)
    public CategoryType getType() {
        return type;
    }

    public void setType(CategoryType type) {
        this.type = type;
    }

    @Temporal(TemporalType.TIMESTAMP)
    @Column(name = "created_at", insertable = false, nullable = false, updatable = false)
    public Date getCreatedAt() {
        return createdAt;
    }

    public void setCreatedAt(Date createdAt) {
        this.createdAt = createdAt;
    }

    @JoinColumn(updatable = false)
    @ManyToOne(optional = false)
    public User getUser() {
        return user;
    }

    public void setUser(User user) {
        this.user = user;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;

        Category category = (Category) o;

        return id != null ? id.equals(category.id) : category.id == null;
    }

    @Override
    public int hashCode() {
        return id != null ? id.hashCode() : 0;
    }

    @Override
    public String toString() {
        return "Category{" +
                "id=" + id +
                ", name='" + name + '\'' +
                ", type=" + type +
                ", createdAt=" + createdAt +
                ", user=" + user +
                '}';
    }
}
src/main/java/io/budgetapp/model/Point.java
public class Point implements Serializable {

    private static final long serialVersionUID = -4254482540288351126L;
    private String label;
    private long key;
    private double value;
    private PointType pointType;

    public Point(String label, long key, double value, PointType pointType) {
        this.label = label;
        this.key = key;
        this.value = value;
        this.pointType = pointType;
    }

    public String getLabel() {
        return label;
    }

    public long getKey() {
        return key;
    }

    public double getValue() {
        return value;
    }

    public PointType getPointType() {
        return pointType;
    }
}
src/main/java/io/budgetapp/model/CategoryType.java
public enum CategoryType {

    INCOME("Income"),
    EXPENDITURE("Expenditure");

    private final String display;

    CategoryType(String display) {
        this.display = display;
    }

    public String getDisplay() {
        return display;
    }
}
src/main/java/io/budgetapp/application/DataConstraintException.java
public class DataConstraintException extends RuntimeException {

    private static final long serialVersionUID = 4029133736296771145L;

    private final String path;

    public DataConstraintException(String path, String message) {
        super(message);
        this.path = path;
    }

    public String getPath() {
        return path;
    }
}
src/main/java/io/budgetapp/dao/UserDAO.java
public class UserDAO extends AbstractDAO<User> {

    private static final Logger LOGGER = LoggerFactory.getLogger(UserDAO.class);

    public UserDAO(SessionFactory sessionFactory) {
        super(sessionFactory);
    }

    public User findById(Long userId) {
        User user = get(userId);
        if(user == null) {
            throw new NotFoundException();
        }
        return user;
    }

    public User add(SignUpForm signUp) {
        LOGGER.debug("Add new user {}", signUp);
        User user = new User();
        user.setUsername(signUp.getUsername());
        user.setPassword(signUp.getPassword());
        user = persist(user);
        return user;
    }

    public void update(User user) {
        LOGGER.debug("Update user {}", user);
        persist(user);
    }

    public Optional<User> findByUsername(String username) {
        Criteria criteria = criteria();
        criteria.add(Restrictions.eq("username", username).ignoreCase());
        List<User> users = list(criteria);
        if(users.size() == 1) {
            return Optional.of(users.get(0));
        } else {
            return Optional.empty();
        }
    }
}
src/main/java/io/budgetapp/model/form/user/Profile.java
@JsonIgnoreProperties(ignoreUnknown = true)
public class Profile {

    private String name;
    private String currency;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getCurrency() {
        return currency;
    }

    public void setCurrency(String currency) {
        this.currency = currency;
    }
}


Question: package io.budgetapp.service;
import com.google.common.collect.ImmutableList;
import io.budgetapp.application.DataConstraintException;
import io.budgetapp.application.NotFoundException;
import io.budgetapp.crypto.PasswordEncoder;
import io.budgetapp.dao.AuthTokenDAO;
import io.budgetapp.dao.CategoryDAO;
import io.budgetapp.dao.BudgetDAO;
import io.budgetapp.dao.BudgetTypeDAO;
import io.budgetapp.dao.RecurringDAO;
import io.budgetapp.dao.TransactionDAO;
import io.budgetapp.dao.UserDAO;
import io.budgetapp.model.AccountSummary;
import io.budgetapp.model.AuthToken;
import io.budgetapp.model.Budget;
import io.budgetapp.model.BudgetType;
import io.budgetapp.model.Category;
import io.budgetapp.model.CategoryType;
import io.budgetapp.model.Group;
import io.budgetapp.model.Point;
import io.budgetapp.model.PointType;
import io.budgetapp.model.Recurring;
import io.budgetapp.model.Transaction;
import io.budgetapp.model.UsageSummary;
import io.budgetapp.model.User;
import io.budgetapp.model.form.LoginForm;
import io.budgetapp.model.form.SignUpForm;
import io.budgetapp.model.form.TransactionForm;
import io.budgetapp.model.form.budget.AddBudgetForm;
import io.budgetapp.model.form.budget.UpdateBudgetForm;
import io.budgetapp.model.form.recurring.AddRecurringForm;
import io.budgetapp.model.form.report.SearchFilter;
import io.budgetapp.model.form.user.Password;
import io.budgetapp.model.form.user.Profile;
import io.budgetapp.util.Util;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.time.Instant;
import java.time.LocalDate;
import java.time.Period;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.stream.Collectors;



/**
 *
 */
public class FinanceService {

    private static final Logger LOGGER = LoggerFactory.getLogger(FinanceService.class);
    private static final DateTimeFormatter SUMMARY_DATE_FORMATTER = DateTimeFormatter.ofPattern("dd MMM");

Answer:     private final UserDAO userDAO;