Compare commits

...

23 Commits

Author SHA1 Message Date
6ba57049df Install and uninstall targets 2023-01-31 19:05:52 -06:00
0d6a22fb8b Payments and earns now store their receipts.
The database tables are now
created on startup
2023-01-31 18:51:22 -06:00
c81bddf79a cacheAccountValue(string,sqlite3) not selecting where accountId= 2023-01-28 12:29:09 -06:00
8071bae4d9 All queries are now in a transaction 2023-01-28 12:28:43 -06:00
30290fc317 Earn and pay now recache value for account 2023-01-28 12:05:35 -06:00
b02994d041 Account pay functional 2023-01-27 20:47:33 -06:00
6413749d76 Add build status to README 2023-01-27 20:13:35 -06:00
c610a0572c Account earn functional 2023-01-27 19:47:50 -06:00
2c3436ee94 Code cleanup 2023-01-27 19:06:19 -06:00
5a88299f9e Account creation with and without description 2023-01-27 19:02:18 -06:00
d3f3ebac3f Implemented account --description 2023-01-26 18:54:38 -06:00
e635d80869 Database throws error when delete fail. Wrapped sqlitedb* in RAII to
close
2023-01-22 18:09:30 -06:00
434eaf0ce0 Added documentation for non private functions. 2023-01-22 17:21:31 -06:00
3941e43e7f Account getValue and cacheing 2023-01-22 17:05:29 -06:00
983f1827e6 account -d and --force-delete now functional 2023-01-21 18:28:28 -06:00
c3dbabe42d accountOperation check if account exists in database 2023-01-21 17:50:07 -06:00
98db5681d8 Operations can now store account 2023-01-17 14:33:09 -06:00
c036cc43dd Code cleanup 2023-01-17 13:47:36 -06:00
ea057e7401 Update readme with correct spacing 2023-01-17 13:42:09 -06:00
b6967e665e Rest of the operations boilerplate 2023-01-17 13:39:00 -06:00
287f9f4751 Operations and AccountOperations boilerplate 2023-01-17 11:58:31 -06:00
94e63e8f76 mainOptHandler handles all paths 2023-01-15 19:37:49 -06:00
365b24e651 MainOptHandler help 2023-01-15 17:50:01 -06:00
49 changed files with 1310 additions and 1019 deletions

15
.idea/dataSources.xml generated Normal file
View File

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="DataSourceManagerImpl" format="xml" multifile-model="true">
<data-source source="LOCAL" name="budget.sqlite" uuid="bba32b4d-f424-4d0f-8ee6-c6e2f73d9010">
<driver-ref>sqlite.xerial</driver-ref>
<synchronize>true</synchronize>
<jdbc-driver>org.sqlite.JDBC</jdbc-driver>
<jdbc-url>jdbc:sqlite:$USER_HOME$/.local/share/budget/budget.sqlite</jdbc-url>
<working-dir>$ProjectFileDir$</working-dir>
<driver-properties>
<property name="foreign_keys" value="true" />
</driver-properties>
</data-source>
</component>
</project>

6
.idea/discord.xml generated Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="DiscordProjectSettings">
<option name="show" value="PROJECT_FILES" />
</component>
</project>

View File

@ -3,16 +3,38 @@ project(budget)
set(CMAKE_CXX_STANDARD 20)
add_executable(${PROJECT_NAME} src/main.cpp src/main.h
src/money/account.cpp src/money/account.h
src/money/payment.cpp src/money/payment.h
src/money/recept.cpp src/money/recept.h
src/data/data.tpp src/data/data.h
src/data/accountData.cpp src/data/accountData.h
src/money/earning.cpp src/money/earning.h
src/data/dateMoney.cpp src/data/dateMoney.h
src/optHandlers/optHandler.cpp src/optHandlers/optHandler.h
src/optHandlers/accountOptHandler.cpp src/optHandlers/accountOptHandler.h
src/optHandlers/mainOptHandler.cpp src/optHandlers/mainOptHandler.h
utilities/math.cpp utilities/math.h
utilities/polynomialFunction.cpp utilities/polynomialFunction.h src/money/transaction.cpp src/money/transaction.h)
set(SOURCES
src/main.cpp
src/optHandlers/mainOptHandler.cpp
src/optHandlers/operation.cpp
src/optHandlers/accountOperation.cpp
src/optHandlers/createOperation.cpp
src/optHandlers/earnOperation.cpp
src/optHandlers/PaymentOperation.cpp
src/database.cpp
src/utilities.cpp)
set(HEADERS
src/optHandlers/mainOptHandler.h
src/optHandlers/operation.h
src/optHandlers/accountOperation.h
src/optHandlers/createOperation.h
src/optHandlers/earnOperation.h
src/optHandlers/PaymentOperation.h
src/database.h
src/exceptions/helpRequested.h
src/exceptions/badValue.h
src/utilities.h
src/sqliteDb.h
src/main.h)
add_executable(${PROJECT_NAME} ${SOURCES} ${HEADERS})
install(TARGETS ${PROJECT_NAME}
RUNTIME DESTINATION bin)
add_custom_target(uninstall
COMMAND xargs rm -v < install_manifest.txt)
target_link_libraries(${PROJECT_NAME} sqlite3)

48
README.md Normal file
View File

@ -0,0 +1,48 @@
Latest Build:
[![TeamCity build status](https://themissingcrowbar.com/teamcity/app/rest/builds/buildType:id:Budget_Build/statusIcon.svg)](https://themissingcrowbar.com/teamcity/repository/downloadAll/Budget_Build/lastFinished/artifacts.zip)
BuildServer:
[![TeamCity](https://themissingcrowbar.com/teamcity/favicon.ico)](https://themissingcrowbar.com/teamcity/buildConfiguration/Budget_Build#all-projects)
Output of budget.
```
Usage:
budget <action> [options] ...
Actions:
-h --help Prints this.
-a --account<=STRING> Management tools for an account.
-c --create<=STRING> Creates a new account with NAME.
-e --earn<=STRING> Add an earning to an account.
-p --payment<=STRING> Add a payment to an account.
Account Options: [-dvD]
-d --delete Deletes specified account.
--force-delete Deletes the specified account without confirmation.
-v --value Gets the current value of the account.
-D --description<=STRING> Changes the description of the account.
Create Options: [-d]
-d --description<=STRING> Sets a description for an account.
Earn Options: -v [-drD]
-v --value=<FlOAT> Value for earning.
-d --description=<STRING> Description for earning.
-r --receipt=<PATH> Path to file to store in DB as receipt.
-D --date=<mm/dd/yyyyTHH:MM:SS> Date as dd/mm/yyyyTHH:MM:SS. Default will be today.
Payment Options: -v [-drD]
-v --value=<FlOAT> Value for payment.
-d --description=<STRING> Description for payment.
-r --receipt=<PATH> Path to file to store in DB as receipt.
-D --date=<mm/dd/yyyyTHH:MM:SS> Date as dd/mm/yyyyTHH:MM:SS. Default will be today.
```
Arguments are processed like blocks with each one terminated by the next Action. For example
```
budget -cAcct -eAcct -v10.00 -r"./receipt.pdf" -pAcct -v5.50 -r"./payment.pdf"
````
Does the following in order:
Creates an account named Acct with no description.
Earns 10.00 to it with a receipt.
Pays 5.50 to it with a receipt.

View File

@ -1,99 +0,0 @@
//
// Created by quentin on 8/4/22.
//
#include "accountData.h"
#include <iostream>
AccountData::AccountData(const std::string &file) : Data(file) {
account = AccountData::createObject();
}
AccountData::AccountData(const std::string &file, const std::string &name) : Data(file) {
std::string json = R"({"name":")" + name + R"(","payments":[],"earnings":[]})";
document.Parse(json.c_str());
account = AccountData::createObject();
std::cout << "Created account " << name << std::endl;
}
Account AccountData::createObject() {
if (isJsonCorrect()) {
std::string name = document["name"].GetString();
std::list<Payment> payments;
for (auto &domPayment : document["payments"].GetArray()) {
Receipt receipt(domPayment["receipt"]["file"].GetString());
double value = domPayment["value"].GetDouble();
std::time_t date = domPayment["date"].GetInt64();
std::tm tmTime{};
std::memcpy(&tmTime, std::localtime(&date), sizeof(struct tm));
payments.emplace_back(value, receipt, tmTime);
}
std::list<Earning> earnings;
for (auto &domEarning : document["earnings"].GetArray()) {
double value = domEarning["value"].GetDouble();
std::time_t date = domEarning["date"].GetInt64();
std::tm tmTime{};
std::memcpy(&tmTime, std::localtime(&date), sizeof(struct tm));
earnings.emplace_back(value, tmTime);
}
return Account(payments, earnings, name);
}
std::string strAnswer;
bool answer;
while (true) {
std::cout << "Account " + getFilePath() + " Is malformed, would you like to remove it? (Y/n): ";
std::cin >> strAnswer;
std::transform(strAnswer.begin(), strAnswer.end(), strAnswer.begin(), ::toupper);
if (strAnswer == "Y" || strAnswer == "YES") {
answer = true;
break;
}
if (strAnswer == "N" || strAnswer == "NO") {
answer = false;
break;
}
std::cout << "Sorry, answer " + strAnswer + " not understood." << std::endl;
}
return Account();
}
bool AccountData::isJsonCorrect() {
if (document.IsObject() &&
document.HasMember("name") && document["name"].IsString() &&
document.HasMember("payments") && document["payments"].IsArray() &&
document.HasMember("earnings") && document["earnings"].IsArray()) {
for (auto &domPayment : document["payments"].GetArray()) {
if (!(domPayment.IsObject() &&
domPayment.HasMember("receipt") && domPayment["receipt"].IsObject() &&
domPayment["receipt"].HasMember("file") && domPayment["receipt"]["file"].IsString() &&
domPayment.HasMember("value") && domPayment["value"].IsDouble() &&
domPayment.HasMember("date") && domPayment["date"].IsInt64())) {
return false;
}
}
for (auto &domEarning : document["earnings"].GetArray()) {
if (!(domEarning.IsObject() &&
domEarning.HasMember("value") && domEarning["value"].IsDouble() &&
domEarning.HasMember("date") && domEarning["date"].IsInt64())) {
return false;
}
}
return true;
}
return false;
}
Account *AccountData::getAccount() {
return &account;
}
AccountData::AccountData() : Data("/dev/null") {}

View File

@ -1,36 +0,0 @@
//
// Created by quentin on 8/4/22.
//
#ifndef BUDGET_ACCOUNTDATA_H
#define BUDGET_ACCOUNTDATA_H
#include "data.h"
#include "../money/account.h"
static const char *jsonTemplate = "";
class AccountData : private Data<Account> {
public:
AccountData();
AccountData(const AccountData &) = default;
explicit AccountData(const std::string &file);
explicit AccountData(const std::string &file, const std::string &name);
Account *getAccount();
using Data<Account>::deleteObject;
private:
Account account;
Account createObject() override;
bool isJsonCorrect() override;
};
#endif //BUDGET_ACCOUNTDATA_H

View File

@ -1,43 +0,0 @@
//
// Created by quentin on 8/4/22.
//
#ifndef BUDGET_DATA_H
#define BUDGET_DATA_H
#include <pwd.h>
#include <unistd.h>
#include <string>
#include <rapidjson/document.h>
template<class T>
class Data {
public:
rapidjson::Document document;
virtual ~Data();
Data(const Data &data);
explicit Data(std::string file);
virtual T createObject() = 0;
std::string getFilePath();
void flushToFile();
void deleteObject();
private:
const std::string fileName;
const std::string homeDirectory = getpwuid(getuid())->pw_dir;
std::string fileDirectory;
bool flush = true;
virtual bool isJsonCorrect() = 0;
};
#include "data.tpp"
#endif //BUDGET_DATA_H

View File

@ -1,85 +0,0 @@
//
// Created by quentin on 8/4/22.
//
#include "data.h"
#include <utility>
#include <fstream>
#include <vector>
#include <rapidjson/stringbuffer.h>
#include <rapidjson/writer.h>
#include <sstream>
template<class T>
Data<T>::Data(std::string file) : fileName(std::move(file)) {
// Create file if it doesnt exist
std::ifstream chkExistIfs(getFilePath(), std::ios::in | std::ios::binary | std::ios::ate);
if (chkExistIfs) {
// File exists, were not creating one
std::ifstream::pos_type fileSize = chkExistIfs.tellg();
chkExistIfs.seekg(0, std::ios::beg);
std::vector<char> bytes(fileSize);
chkExistIfs.read(bytes.data(), fileSize);
document.Parse(std::string(bytes.data(), fileSize).c_str());
chkExistIfs.close();
} else {
// File doesnt exist we need to create one
// This is the job of the derives constructor.
chkExistIfs.close();
};
}
template<class T>
std::string Data<T>::getFilePath() {
return fileName;
}
template<class T>
void Data<T>::flushToFile() {
rapidjson::StringBuffer buffer;
rapidjson::Writer<rapidjson::StringBuffer> writer(buffer);
document.Accept(writer);
std::ofstream file(getFilePath());
file << buffer.GetString();
file.close();
}
template<class T>
Data<T>::~Data() {
if (flush) {
flushToFile();
}
}
template<class T>
void Data<T>::deleteObject() {
remove(getFilePath().c_str());
flush = false;
}
template<class T>
Data<T>::Data(const Data &data) : fileName(data.fileName) {
// Create file if it doesnt exist
std::ifstream chkExistIfs(getFilePath(), std::ios::in | std::ios::binary | std::ios::ate);
if (chkExistIfs) {
// File exists, were not creating one
std::ifstream::pos_type fileSize = chkExistIfs.tellg();
chkExistIfs.seekg(0, std::ios::beg);
std::vector<char> bytes(fileSize);
chkExistIfs.read(bytes.data(), fileSize);
document.Parse(std::string(bytes.data(), fileSize).c_str());
chkExistIfs.close();
} else {
// File doesnt exist we need to create one
// This is the job of the derives constructor.
chkExistIfs.close();
};
}

View File

@ -1,38 +0,0 @@
//
// Created by quentin on 8/12/22.
//
#include "dateMoney.h"
DateMoney::DateMoney(const double *value, tm *date) : value(value), date(date) {}
bool DateMoney::operator<(const DateMoney &rhs) const {
return mktime(date) < mktime(rhs.date);
}
bool DateMoney::operator>(const DateMoney &rhs) const {
return rhs < *this;
}
bool DateMoney::operator<=(const DateMoney &rhs) const {
return !(rhs < *this);
}
bool DateMoney::operator>=(const DateMoney &rhs) const {
return !(*this < rhs);
}
std::ostream &operator<<(std::ostream &os, const DateMoney &money) {
os << "Value: " << *money.value << " Date: " << money.date->tm_mon + 1 << "/" << money.date->tm_mday << "/"
<< money.date->tm_year+1900;
return os;
}
const double *DateMoney::getValue() const {
return value;
}
tm *DateMoney::getDate() const {
return date;
}

View File

@ -1,36 +0,0 @@
//
// Created by quentin on 8/12/22.
//
#ifndef BUDGET_DATEMONEY_H
#define BUDGET_DATEMONEY_H
#include <ctime>
#include <ostream>
class DateMoney {
public:
DateMoney(const double *value, tm *date);
[[nodiscard]] const double *getValue() const;
[[nodiscard]] tm *getDate() const;
bool operator<(const DateMoney &rhs) const;
bool operator>(const DateMoney &rhs) const;
bool operator<=(const DateMoney &rhs) const;
bool operator>=(const DateMoney &rhs) const;
friend std::ostream &operator<<(std::ostream &os, const DateMoney &money);
private:
const double *value;
tm *date;
};
#endif //BUDGET_DATEMONEY_H

250
src/database.cpp Normal file
View File

@ -0,0 +1,250 @@
//
// Created by quentin on 1/18/23.
//
#include "database.h"
#include <sqlite3.h>
#include <stdexcept>
bool Database::doesAccountExist(const std::string &account, sqlite3 *db) {
sqlite3_stmt *stmt;
int rc = sqlite3_prepare_v2(db, "SELECT * FROM account WHERE name = ?", -1, &stmt, nullptr);
if (rc != SQLITE_OK)
throw std::runtime_error("Failed to prepare account exist query.");
sqlite3_bind_text(stmt, 1, account.c_str(), -1, SQLITE_TRANSIENT);
rc = sqlite3_step(stmt);
sqlite3_finalize(stmt);
if (rc == SQLITE_ROW) {
return true;
} else if (rc == SQLITE_DONE) {
return false;
} else {
throw std::runtime_error("Failed to step account existing statement.");
}
}
void Database::deleteAccount(const std::string &account, sqlite3 *db) {
sqlite3_stmt *stmt;
int rc = sqlite3_prepare_v2(db, "DELETE FROM account WHERE name = ?", -1, &stmt, nullptr);
if (rc != SQLITE_OK)
throw std::runtime_error("Failed to delete account " + account);
sqlite3_bind_text(stmt, 1, account.c_str(), -1, SQLITE_TRANSIENT);
rc = sqlite3_step(stmt);
sqlite3_finalize(stmt);
if (rc != SQLITE_DONE)
throw std::runtime_error("Failed to delete account " + account);
}
double Database::getValue(const std::string &account, sqlite3 *db) {
sqlite3_stmt *stmt;
int rc = sqlite3_prepare_v2(db, "SELECT id, cachedValue FROM account WHERE name = ?", -1, &stmt, nullptr);
if (rc != SQLITE_OK)
throw std::runtime_error("Failed preparing cashedValue " + account);
sqlite3_bind_text(stmt, 1, account.c_str(), -1, SQLITE_TRANSIENT);
rc = sqlite3_step(stmt);
if (rc == SQLITE_ROW) {
double value;
if (sqlite3_column_type(stmt, 1) == SQLITE_NULL) {
value = cacheAccountValue(sqlite3_column_int64(stmt, 0), db);
sqlite3_finalize(stmt);
return value;
}
value = sqlite3_column_double(stmt, 1);
sqlite3_finalize(stmt);
return value;
} else if (rc == SQLITE_DONE)
throw std::runtime_error("Account doesnt exist? Shouldn't be possible in this call.");
else
throw std::runtime_error("Failed to step getValue statement.");
}
double Database::cacheAccountValue(long long accountId, sqlite3 *db) {
sqlite3_stmt *stmt;
int rc = sqlite3_prepare_v2(db, "SELECT SUM(value) FROM ("
"SELECT value * -1 as value FROM payment WHERE accountId = ? "
"UNION ALL "
"SELECT value FROM earning WHERE accountId = ?);", -1, &stmt, nullptr);
if (rc != SQLITE_OK)
throw std::runtime_error("Failed preparing get cashedValue " + std::to_string(accountId));
sqlite3_bind_int64(stmt, 1, accountId);
sqlite3_bind_int64(stmt, 2, accountId);
rc = sqlite3_step(stmt);
double value;
if (rc == SQLITE_ROW)
value = sqlite3_column_double(stmt, 0);
else if (rc == SQLITE_DONE)
value = 0;
else
throw std::runtime_error("Failed get cashedValue " + std::to_string(accountId));
sqlite3_reset(stmt);
rc = sqlite3_prepare_v2(db, "UPDATE account SET cachedValue = ? WHERE id = ?", -1, &stmt, nullptr);
if (rc != SQLITE_OK)
throw std::runtime_error("Failed preparing set cashedValue " + std::to_string(accountId));
sqlite3_bind_double(stmt, 1, value);
sqlite3_bind_int64(stmt, 2, accountId);
rc = sqlite3_step(stmt);
if (rc == SQLITE_DONE)
return value;
throw std::runtime_error("Failed to set cashedValue for " + std::to_string(accountId));
}
double Database::cacheAccountValue(const std::string &account, sqlite3 *db) {
sqlite3_stmt *stmt;
int rc = sqlite3_prepare_v2(db, "SELECT SUM(value) FROM ("
"SELECT value * -1 as value FROM payment WHERE accountId = "
"(SELECT id FROM account where name = ?) "
"UNION ALL "
"SELECT value FROM earning WHERE accountId = "
"(SELECT id FROM account where name = ?));", -1, &stmt, nullptr);
if (rc != SQLITE_OK)
throw std::runtime_error("Failed preparing get cashedValue " + account);
sqlite3_bind_text(stmt, 1, account.c_str(), -1, SQLITE_TRANSIENT);
sqlite3_bind_text(stmt, 2, account.c_str(), -1, SQLITE_TRANSIENT);
rc = sqlite3_step(stmt);
double value;
if (rc == SQLITE_ROW)
value = sqlite3_column_double(stmt, 0);
else if (rc == SQLITE_DONE)
value = 0;
else
throw std::runtime_error("Failed get cashedValue " + account);
sqlite3_reset(stmt);
rc = sqlite3_prepare_v2(db, "UPDATE account SET cachedValue = ? WHERE name = ?", -1, &stmt, nullptr);
if (rc != SQLITE_OK)
throw std::runtime_error("Failed preparing set cashedValue " + account);
sqlite3_bind_double(stmt, 1, value);
sqlite3_bind_text(stmt, 2, account.c_str(), -1, SQLITE_TRANSIENT);
rc = sqlite3_step(stmt);
if (rc == SQLITE_DONE)
return value;
throw std::runtime_error("Failed to set cashedValue for " + account);
}
void Database::accountDescription(const std::string &account, const std::string &description, sqlite3 *db) {
sqlite3_stmt *stmt;
int rc = sqlite3_prepare_v2(db, "UPDATE account SET description = ? WHERE name = ?", -1, &stmt, nullptr);
if (rc != SQLITE_OK)
throw std::runtime_error("Failed preparing accountDescription statement");
sqlite3_bind_text(stmt, 1, description.c_str(), -1, SQLITE_TRANSIENT);
sqlite3_bind_text(stmt, 2, account.c_str(), -1, SQLITE_TRANSIENT);
rc = sqlite3_step(stmt);
sqlite3_finalize(stmt);
if (rc != SQLITE_DONE)
throw std::runtime_error("Failed to set description on " + account);
}
void Database::createAccount(const std::string &account, const std::string &description, sqlite3 *db) {
sqlite3_stmt *stmt;
int rc = sqlite3_prepare_v2(db, "INSERT INTO account (name, description) VALUES (?, ?)", -1, &stmt, nullptr);
if (rc != SQLITE_OK)
throw std::runtime_error("Failed preparing createAccount statement");
sqlite3_bind_text(stmt, 1, account.c_str(), -1, SQLITE_TRANSIENT);
sqlite3_bind_text(stmt, 2, description.c_str(), -1, SQLITE_TRANSIENT);
rc = sqlite3_step(stmt);
sqlite3_finalize(stmt);
if (rc != SQLITE_DONE)
throw std::runtime_error("Failed to create account " + account);
}
void Database::createAccount(const std::string &account, sqlite3 *db) {
sqlite3_stmt *stmt;
int rc = sqlite3_prepare_v2(db, "INSERT INTO account (name) VALUES (?)", -1, &stmt, nullptr);
if (rc != SQLITE_OK)
throw std::runtime_error("Failed preparing createAccount statement");
sqlite3_bind_text(stmt, 1, account.c_str(), -1, SQLITE_TRANSIENT);
rc = sqlite3_step(stmt);
sqlite3_finalize(stmt);
if (rc != SQLITE_DONE)
throw std::runtime_error("Failed to create account " + account);
}
long long int Database::earn(const std::string &account, long double &value, std::string &description, std::string &receipt,
long long date, sqlite3 *db) {
sqlite3_stmt *stmt;
int rc = sqlite3_prepare_v2(db, "INSERT INTO earning (value, description, receipt, accountId, date) VALUES "
"(?, ?, ?, (SELECT id FROM account WHERE name = ?), ?);",
-1, &stmt, nullptr);
if (rc != SQLITE_OK)
throw std::runtime_error("Failed preparing earn statement");
sqlite3_bind_double(stmt, 1, value);
sqlite3_bind_text(stmt, 2, (description.empty() ? nullptr : description.c_str()), -1, SQLITE_TRANSIENT);
sqlite3_bind_text(stmt, 3, (receipt.empty() ? nullptr : receipt.c_str()), -1, SQLITE_TRANSIENT);
sqlite3_bind_text(stmt, 4, (account.empty() ? nullptr : account.c_str()), -1, SQLITE_TRANSIENT);
sqlite3_bind_int64(stmt, 5, date);
rc = sqlite3_step(stmt);
sqlite3_finalize(stmt);
if (rc != SQLITE_DONE)
throw std::runtime_error("Failed to create earning");
cacheAccountValue(account, db);
return sqlite3_last_insert_rowid(db);
}
long long int Database::pay(const std::string &account, long double &value, std::string &description, std::string &receipt,
long long date, sqlite3 *db) {
sqlite3_stmt *stmt;
int rc = sqlite3_prepare_v2(db, "INSERT INTO payment (value, description, receipt, accountId, date) VALUES "
"(?, ?, ?, (SELECT id FROM account WHERE name = ?), ?);",
-1, &stmt, nullptr);
if (rc != SQLITE_OK)
throw std::runtime_error("Failed preparing pay statement");
sqlite3_bind_double(stmt, 1, value);
sqlite3_bind_text(stmt, 2, (description.empty() ? nullptr : description.c_str()), -1, SQLITE_TRANSIENT);
sqlite3_bind_text(stmt, 3, (receipt.empty() ? nullptr : receipt.c_str()), -1, SQLITE_TRANSIENT);
sqlite3_bind_text(stmt, 4, (account.empty() ? nullptr : account.c_str()), -1, SQLITE_TRANSIENT);
sqlite3_bind_int64(stmt, 5, date);
rc = sqlite3_step(stmt);
sqlite3_finalize(stmt);
if (rc != SQLITE_DONE)
throw std::runtime_error("Failed to create payment");
cacheAccountValue(account, db);
return sqlite3_last_insert_rowid(db);
}

150
src/database.h Normal file
View File

@ -0,0 +1,150 @@
//
// Created by quentin on 1/18/23.
//
#ifndef BUDGET_DATABASE_H
#define BUDGET_DATABASE_H
#include <string>
#include <sqlite3.h>
class Database {
public:
/**
* @brief Checks if an account exists in the database
*
* @param account The name of the account to check for
* @param db The database connection to use
*
* @return True if the account exists, false otherwise
*
* @throws std::runtime_error If the database query fails
*/
static bool doesAccountExist(const std::string &account, sqlite3 *db);
/**
* @brief Deletes an account from the database
*
* @param account The name of the account to delete
* @param db The database connection to use
*
* @return True if the account was successfully deleted, false otherwise
*
* @throws std::runtime_error If the database query fails
*/
static void deleteAccount(const std::string &account, sqlite3 *db);
/**
* @brief Retrieves the cached value of an account from the database
*
* @param account The name of the account to retrieve the value of
* @param db The database connection to use
*
* @return The cached value of the account
*
* @throws std::runtime_error If the database query fails or the account doesn't exist
*/
static double getValue(const std::string &account, sqlite3 *db);
/**
* @brief Caches the value of an account by calculating the sum of all payments and earnings
*
* @param accountId The ID of the account to cache the value of
* @param db The database connection to use
*
* @return The cached value of the account
*
* @throws std::runtime_error If the database query fails
*/
static double cacheAccountValue(long long accountId, sqlite3 *db);
/**
* @brief Caches the value of an account by calculating the sum of all payments and earnings
*
* @param account The name of the account to cache the value of
* @param db The database connection to use
*
* @return The cached value of the account
*
* @throws std::runtime_error If the database query fails
*/
static double cacheAccountValue(const std::string &account, sqlite3 *db);
/**
* @brief Sets a new description for an account
*
* @param account The account name
* @param description The new description
* @param db The database connection to use
*
* @throws std::runtime_error If the statement fails to prepare or the step fails to execute
*/
static void accountDescription(const std::string &account, const std::string &description, sqlite3 *db);
/**
* @brief Creates a new account in the database with the given name and description
*
* @param account The name of the account to create
* @param description The description of the account to create
* @param db The sqlite3 database connection to use
*
* @throws std::runtime_error If the account could not be created in the database
*/
static void createAccount(const std::string &account, const std::string &description, sqlite3 *db);
/**
* @brief Creates a new account in the database with the given name
*
* @param account The name of the account to create
* @param db The sqlite3 database connection to use
*
* @throws std::runtime_error If the account could not be created in the database
*/
static void createAccount(const std::string &account, sqlite3 *db);
/**
* @brief The function records an earning in the database
*
* @param account The name of the account to record the earning in
* @param value The value of the earning
* @param description A description of the earning, can be an empty string
* @param receipt A receipt of the earning, can be an empty string
* @param date The date of the earning in UNIX time
* @param db The sqlite3 database connection
*
* @throws std::runtime_error If the earning could not be created in the database
*
* This function records an earning in the database by inserting a new row in the 'earning' table with the provided information.
* The accountId is retrieved from the 'account' table using the provided account name.
* If the query fails to prepare or execute, the function throws a runtime_error.
*/
static long long int
earn(const std::string &account, long double &value, std::string &description, std::string &receipt,
long long int date,
sqlite3 *db);
/**
* @brief The function records an payment in the database
*
* @param account The name of the account to record the payment in
* @param value The value of the payment
* @param description A description of the payment, can be an empty string
* @param receipt A receipt of the payment, can be an empty string
* @param date The date of the payment in UNIX time
* @param db The sqlite3 database connection
*
* @throws std::runtime_error If the payment could not be created in the database
*
* This function records an payment in the database by inserting a new row in the 'payment' table with the provided information.
* The accountId is retrieved from the 'account' table using the provided account name.
* If the query fails to prepare or execute, the function throws a runtime_error.
*/
static long long int
pay(const std::string &account, long double &value, std::string &description, std::string &receipt,
long long int date,
sqlite3 *db);
};
#endif //BUDGET_DATABASE_H

17
src/exceptions/badValue.h Normal file
View File

@ -0,0 +1,17 @@
//
// Created by quentin on 1/18/23.
//
#ifndef BUDGET_BADVALUE_H
#define BUDGET_BADVALUE_H
#include <stdexcept>
namespace Budget::Exceptions {
class BadValue : public std::runtime_error {
public:
explicit BadValue(const std::string &what_arg) : std::runtime_error(what_arg) {}
};
}
#endif //BUDGET_BADVALUE_H

View File

@ -0,0 +1,18 @@
//
// Created by quentin on 1/18/23.
//
#ifndef BUDGET_HELPREQUESTED_H
#define BUDGET_HELPREQUESTED_H
#include <stdexcept>
namespace Budget::Exceptions {
class HelpRequested : public std::runtime_error {
public:
explicit HelpRequested(const std::string &what_arg) : std::runtime_error(what_arg) {}
};
}
#endif //BUDGET_HELPREQUESTED_H

View File

@ -2,110 +2,79 @@
// Created by quentin on 8/3/22.
//
#include "main.h"
#include "data/accountData.h"
#include "optHandlers/accountOptHandler.h"
#include "optHandlers/mainOptHandler.h"
#include "../utilities/math.h"
#include "exceptions/helpRequested.h"
#include "exceptions/badValue.h"
#include "sqliteDb.h"
#include "main.h"
#include <pwd.h>
#include <unistd.h>
#include <string>
#include <filesystem>
#include <unordered_map>
#include <sqlite3.h>
#include <iostream>
using namespace Budget;
const std::string homeDirectory = getpwuid(getuid())->pw_dir;
const std::string configD = homeDirectory + "/.config/budget/";
const std::string storageD = homeDirectory + "/.local/share/budget/";
const char* createTables = "CREATE TABLE account (id INTEGER CONSTRAINT account_pk PRIMARY KEY AUTOINCREMENT, name TEXT, description TEXT, cachedValue DOUBLE);"
"CREATE UNIQUE INDEX account_name_uindex ON account (name);"
"CREATE TABLE earning (id INTEGER CONSTRAINT earning_pk PRIMARY KEY AUTOINCREMENT, value DOUBLE NOT NULL, description TEXT, receipt TEXT, accountId INT NOT NULL REFERENCES account ON UPDATE CASCADE ON DELETE CASCADE, date INTEGER NOT NULL);"
"CREATE INDEX earning_date_index ON earning (date DESC);"
"CREATE TABLE payment (id INTEGER CONSTRAINT payment_pk PRIMARY KEY AUTOINCREMENT, value DOUBLE NOT NULL, description TEXT, receipt TEXT, accountId INT NOT NULL REFERENCES account ON UPDATE CASCADE ON DELETE CASCADE, date INTEGER NOT NULL);"
"CREATE INDEX payment_date_index ON payment (date DESC);";
void createRequiredFolders() {
std::filesystem::create_directory(configD);
std::filesystem::create_directories(storageD);
std::filesystem::create_directories(storageD + "accounts");
std::filesystem::create_directories(storageD + "receipts");
std::filesystem::create_directories(storageD + "receipts/payment");
std::filesystem::create_directories(storageD + "receipts/earn");
}
int main(int argc, char *argv[]) {
std::vector<char *> args(argv, argv + argc);
// Parse main options (-ah)
OptHandlers::MainOptHandler mainOptHandler(args);
mainOptHandler.parse();
if (mainOptHandler.getSetOpts()->help)
mainOptHandler.help();
createRequiredFolders();
// Read all accounts saved and store them in accounts
std::unordered_map<std::string, AccountData> accounts;
for (const auto &file : std::filesystem::directory_iterator(
homeDirectory + "/.local/share/budget/accounts")) {
AccountData account(file.path());
accounts.insert(std::pair(account.getAccount()->getName(), account));
sqlite3 *db;
SqliteDb dbRAII(db);
int rc;
rc = sqlite3_open(databaseFile.c_str(), &db);
if (rc != SQLITE_OK)
throw std::runtime_error("Error opening database connection.");
rc = sqlite3_exec(db, "PRAGMA foreign_keys = 1;", nullptr, nullptr, nullptr);
if (rc != SQLITE_OK)
throw std::runtime_error("Error enabling foreign_keys. Database might be malformed.");
rc = sqlite3_exec(db, "BEGIN", nullptr, nullptr, nullptr);
if (rc != SQLITE_OK)
throw std::runtime_error("Couldn't begin transaction");
rc = sqlite3_exec(db, createTables, nullptr, nullptr, nullptr);
if (rc != SQLITE_OK)
throw std::runtime_error("Couldn't create the tables");
std::vector<char *> args(argv, argv + argc);
try {
OptHandlers::MainOptHandler moh(args, db);
std::queue<std::unique_ptr<OptHandlers::Operation>> *opts = &moh.operations;
while (!opts->empty()) {
opts->front()->commit();
opts->pop();
}
} catch (const Budget::Exceptions::HelpRequested &e) {
return 0;
} catch (const Budget::Exceptions::BadValue &e) {
std::cout << e.what() << std::endl;
return 1;
}
// Parse account options if main options tells us to.
if (mainOptHandler.getSetOpts()->account) {
std::vector<char *> vec{argv[0]};
vec.insert(vec.end(), args.begin() + mainOptHandler.getSetOpts()->accountArgStart, args.end());
OptHandlers::AccountOptHandler accountOptHandler(vec);
accountOptHandler.parse();
rc = sqlite3_exec(db, "COMMIT", nullptr, nullptr, nullptr);
if (rc != SQLITE_OK)
throw std::runtime_error("Couldn't commit transaction");
// Do what we need to do for parsed options
if (accountOptHandler.getSetOpts()->help) {
accountOptHandler.help();
}
if (accountOptHandler.getSetOpts()->create) {
auto a = accounts.find(accountOptHandler.getSetOpts()->delAccount);
if (a == accounts.end()) {
accounts.emplace(std::piecewise_construct,
std::make_tuple(accountOptHandler.getSetOpts()->createAccount), std::make_tuple(
storageD + "accounts/" + accountOptHandler.getSetOpts()->createAccount + ".json",
accountOptHandler.getSetOpts()->createAccount));
} else {
std::cout << "Account " << accountOptHandler.getSetOpts()->delAccount << " already exists."
<< std::endl;
}
}
if (accountOptHandler.getSetOpts()->value) {
auto a = accounts.find(accountOptHandler.getSetOpts()->valueAccount);
if (a != accounts.end()) {
int value = a->second.getAccount()->getValue();
std::vector<DateMoney> timeline = a->second.getAccount()->getTimeline();
printf("Account value: %d\n", value);
printf("Last 10 transactions:\n");
for (int i = 0; i < timeline.size() && i < 10; i++) {
const double *amount = timeline[i].getValue();
const tm *date = timeline[i].getDate();
if (*amount <= 0) {
// Red
printf("Value: \033[31m%.2f\033[0m, ", *amount);
}
else {
// Green
printf("Value: \033[32m%.2f\033[0m, ", *amount);
}
printf("Date: %d/%d/%d\n", date->tm_mon+1, date->tm_mday, date->tm_year+1900);
}
std::cout << std::endl;
}
}
if (accountOptHandler.getSetOpts()->del) {
auto a = accounts.find(accountOptHandler.getSetOpts()->delAccount);
if (a != accounts.end()) {
accounts.erase(a);
std::cout << "Deleted account: " << accountOptHandler.getSetOpts()->delAccount << std::endl;
}
}
if (accountOptHandler.getSetOpts()->list) {
std::cout << "Accounts: " << std::endl;
for (auto &accountPair : accounts) {
std::cout << accountPair.second.getAccount()->getName() << ": " << std::endl;
std::cout << " Value: " << accountPair.second.getAccount()->getValue() << std::endl;
}
}
}
return 0;
}

View File

@ -1,8 +1,17 @@
//
// Created by quentin on 8/12/22.
// Created by quentin on 1/31/23.
//
#ifndef BUDGET_MAIN_H
#define BUDGET_MAIN_H
#include <unistd.h>
#include <pwd.h>
const static std::string homeDirectory = getpwuid(getuid())->pw_dir;
const static std::string configD = homeDirectory + "/.config/budget/";
const static std::string storageD = homeDirectory + "/.local/share/budget/";
const static std::string databaseFile = homeDirectory + "/.local/share/budget/budget.sqlite";
#endif //BUDGET_MAIN_H

View File

@ -1,44 +0,0 @@
//
// Created by quentin on 8/4/22.
//
#include "account.h"
#include <utility>
Account::Account(std::list<Payment> payments, std::list<Earning> earnings,
std::string name) : payments(std::move(payments)), earnings(std::move(earnings)),
name(std::move(name)) {}
Account::Account() {
name = "";
}
int Account::getValue() {
int total = 0;
for (auto &payment : payments) {
total -= payment.value;
}
for (auto &earning : earnings) {
total += earning.value;
}
return total;
}
std::vector<DateMoney> Account::getTimeline() {
std::vector<DateMoney> timeline;
for (auto &payment : payments) {
timeline.emplace_back(payment.getValue(), payment.getDate());
}
for (auto &earning : earnings) {
timeline.emplace_back(&earning.value, earning.getDate());
}
std::sort(timeline.begin(), timeline.end(), std::greater<>());
return timeline;
}
const std::string &Account::getName() const {
return name;
}

View File

@ -1,35 +0,0 @@
//
// Created by quentin on 8/4/22.
//
#ifndef BUDGET_ACCOUNT_H
#define BUDGET_ACCOUNT_H
#include "payment.h"
#include "earning.h"
#include "../data/dateMoney.h"
#include <list>
#include <string>
#include <vector>
class Account {
public:
Account(std::list<Payment> payments, std::list<Earning> earnings, std::string name);
Account();
int getValue();
std::vector<DateMoney> getTimeline();
[[nodiscard]] const std::string &getName() const;
private:
std::list<Payment> payments;
std::list<Earning> earnings;
std::string name;
};
#endif //BUDGET_ACCOUNT_H

View File

@ -1,7 +0,0 @@
//
// Created by quentin on 8/11/22.
//
#include "earning.h"
Earning::Earning(const double value, std::tm date) : Transaction(value, date) {}

View File

@ -1,17 +0,0 @@
//
// Created by quentin on 8/11/22.
//
#ifndef BUDGET_EARNING_H
#define BUDGET_EARNING_H
#include <ctime>
#include "transaction.h"
class Earning : public Transaction {
public:
explicit Earning(double value, std::tm date);
};
#endif //BUDGET_EARNING_H

View File

@ -1,19 +0,0 @@
//
// Created by quentin on 8/4/22.
//
#include "payment.h"
#include <utility>
Payment::Payment(const double value, Receipt receipt, std::tm date) : Transaction(value, date), receipt(std::move(receipt)) {
negativeValue = -value;
}
Receipt &Payment::getReceipt() {
return receipt;
}
const double *Payment::getValue() {
return &negativeValue;
}

View File

@ -1,27 +0,0 @@
//
// Created by quentin on 8/4/22.
//
#ifndef BUDGET_PAYMENT_H
#define BUDGET_PAYMENT_H
#include "recept.h"
#include "transaction.h"
class Payment : public Transaction {
public:
Payment(double value, Receipt receipt, std::tm date);
Receipt &getReceipt();
const double *getValue();
private:
Receipt receipt;
double negativeValue;
};
#endif //BUDGET_PAYMENT_H

View File

@ -1,9 +0,0 @@
//
// Created by quentin on 8/4/22.
//
#include "recept.h"
#include <utility>
Receipt::Receipt(std::string file) : file(std::move(file)) {}

View File

@ -1,21 +0,0 @@
//
// Created by quentin on 8/4/22.
//
#ifndef BUDGET_RECEPT_H
#define BUDGET_RECEPT_H
#include <string>
class Receipt {
private:
public:
explicit Receipt(std::string file);
private:
std::string file;
};
#endif //BUDGET_RECEPT_H

View File

@ -1,11 +0,0 @@
//
// Created by quentin on 9/16/22.
//
#include "transaction.h"
Transaction::Transaction(const double value, const tm &date) : value(value), date(date) {}
tm *Transaction::getDate() {
return &date;
}

View File

@ -1,23 +0,0 @@
//
// Created by quentin on 9/16/22.
//
#ifndef BUDGET_TRANSACTION_H
#define BUDGET_TRANSACTION_H
#include <ctime>
class Transaction {
public:
Transaction(double value, const tm &date);
const double value;
tm *getDate();
private:
std::tm date;
};
#endif //BUDGET_TRANSACTION_H

View File

@ -0,0 +1,34 @@
//
// Created by quentin on 1/17/23.
//
#include <fstream>
#include "PaymentOperation.h"
#include "../database.h"
#include "../exceptions/badValue.h"
#include "../main.h"
using namespace Budget::OptHandlers;
void PaymentOperation::commit() {
if (!Database::doesAccountExist(account, db))
throw Budget::Exceptions::BadValue("Account " + account + " doesn't exist");
long long id = Database::pay(account, flags.value, flags.description, flags.receipt, flags.date, db);
if (!flags.receipt.empty()) {
std::string extension;
size_t pos = flags.receipt.find_last_of('.');
if (pos != std::string::npos) {
extension = flags.receipt.substr(pos);
}
std::ifstream source(flags.receipt, std::ios::binary);
std::ofstream dest(storageD + "receipts/payment/" + std::to_string(id) + extension, std::ios::binary);
dest << source.rdbuf();
source.close();
dest.close();
}
}
PaymentOperation::PaymentOperation(sqlite3 *db, std::string account) : Operation(db), account(std::move(account)) {}

View File

@ -0,0 +1,36 @@
//
// Created by quentin on 1/17/23.
//
#ifndef BUDGET_PAYMENTOPERATION_H
#define BUDGET_PAYMENTOPERATION_H
#include <string>
#include <ctime>
#include "operation.h"
namespace Budget::OptHandlers {
class PaymentOperation : public Operation {
public:
void commit() override;
explicit PaymentOperation(sqlite3 *db, std::string account);
struct Flags : public Operation::Flags {
long double value;
std::string description;
std::string receipt;
long long date = std::time(nullptr);
};
Flags flags;
private:
std::string account;
};
}
#endif //BUDGET_PAYMENTOPERATION_H

View File

@ -0,0 +1,35 @@
//
// Created by quentin on 1/17/23.
//
#include "accountOperation.h"
#include "../database.h"
#include "../exceptions/badValue.h"
#include "../utilities.h"
#include <utility>
#include <iostream>
using namespace Budget::OptHandlers;
void AccountOperation::commit() {
if (!Database::doesAccountExist(account, db))
throw Budget::Exceptions::BadValue("Account " + account + " doesn't exist");
if (flags.del) {
if (flags.forceDel || Utilities::confirm("Are you sure you'd like to delete " + account)) {
Database::deleteAccount(account, db);
return;
}
}
if (flags.value) {
std::cout << Database::getValue(account, db) << std::endl;
}
if (!flags.description.empty()) {
Database::accountDescription(account, flags.description, db);
}
}
AccountOperation::AccountOperation(sqlite3 *db, std::string account) : Operation(db), account(std::move(account)) {}

View File

@ -0,0 +1,32 @@
//
// Created by quentin on 1/17/23.
//
#ifndef BUDGET_ACCOUNTOPERATION_H
#define BUDGET_ACCOUNTOPERATION_H
#include <string>
#include <utility>
#include "operation.h"
namespace Budget::OptHandlers {
class AccountOperation : public Operation {
public:
void commit() override;
explicit AccountOperation(sqlite3 *db, std::string account);
struct Flags : public Operation::Flags {
bool del = false;
bool forceDel = false;
bool value = false;
std::string description;
};
Flags flags;
private:
std::string account;
};
}
#endif //BUDGET_ACCOUNTOPERATION_H

View File

@ -1,70 +0,0 @@
//
// Created by quentin on 8/13/22.
//
#include <iostream>
#include "accountOptHandler.h"
using namespace Budget::OptHandlers;
void AccountOptHandler::parse() {
struct option longOpts[] = {
{"help", no_argument, nullptr, 'h'},
{"list", no_argument, nullptr, 'l'},
{"delete", required_argument, nullptr, 'd'},
{"create", required_argument, nullptr, 'c'},
{"value", required_argument, nullptr, 'v'},
};
while (true) {
int opt = getopt_long(getArgc(), getArgv(), "hld:c:v:", longOpts, nullptr);
if (opt == -1) {
break;
}
switch (opt) {
case 'h':
setOpts.help = true;
break;
case 'l':
setOpts.list = true;
break;
case 'd':
setOpts.del = true;
setOpts.delAccount = optarg;
break;
case 'c':
setOpts.create = true;
setOpts.createAccount = optarg;
break;
case 'v':
setOpts.value = true;
setOpts.valueAccount = optarg;
break;
case '?':
setOpts.help = true;
setOpts.helpOut = stderr;
break;
default:
break;
}
}
}
void AccountOptHandler::help() {
fprintf(setOpts.helpOut, "Help budget -a {-cvd account|-hl}\n");
fprintf(setOpts.helpOut, " -h --help Output this message.\n");
fprintf(setOpts.helpOut, " -l --list List available accounts.\n");
fprintf(setOpts.helpOut, " -d --delete account Delete the specified account.\n");
fprintf(setOpts.helpOut, " -c --create account Create a new account.\n");
fprintf(setOpts.helpOut, " -v --value account Print the current value of account.\n");
}
const AccountOptHandler::SetOpts *AccountOptHandler::getSetOpts() const {
return &setOpts;
}
AccountOptHandler::AccountOptHandler(const std::vector<char *> &argv) : OptHandler(argv) {}

View File

@ -1,41 +0,0 @@
//
// Created by quentin on 8/13/22.
//
#ifndef BUDGET_ACCOUNTOPTHANDLER_H
#define BUDGET_ACCOUNTOPTHANDLER_H
#include "optHandler.h"
namespace Budget::OptHandlers {
class AccountOptHandler : public OptHandler {
struct SetOpts {
FILE *helpOut = stdout;
bool help = false;
bool list = false;
bool del = false;
char *delAccount{};
bool create = false;
char *createAccount{};
bool value = false;
char *valueAccount{};
};
public:
explicit AccountOptHandler(const std::vector<char *> &argv);
void parse() override;
void help() override;
[[nodiscard]] const SetOpts *getSetOpts() const;
private:
SetOpts setOpts;
};
}
#endif //BUDGET_ACCOUNTOPTHANDLER_H

View File

@ -0,0 +1,22 @@
//
// Created by quentin on 1/17/23.
//
#include "createOperation.h"
#include "../database.h"
#include "../exceptions/badValue.h"
using namespace Budget::OptHandlers;
void CreateOperation::commit() {
if (Database::doesAccountExist(account, db)) {
throw Budget::Exceptions::BadValue("Account already exists, cant create " + account);
}
if (flags.description.empty()) {
Database::createAccount(account, db);
return;
}
Database::createAccount(account, flags.description, db);
}
CreateOperation::CreateOperation(sqlite3 *db, std::string account) : Operation(db), account(std::move(account)) {}

View File

@ -0,0 +1,29 @@
//
// Created by quentin on 1/17/23.
//
#ifndef BUDGET_CREATEOPERATION_H
#define BUDGET_CREATEOPERATION_H
#include <string>
#include "operation.h"
namespace Budget::OptHandlers {
class CreateOperation : public Operation {
public:
void commit() override;
explicit CreateOperation(sqlite3 *db, std::string account);
struct Flags : public Operation::Flags {
std::string description;
};
Flags flags;
private:
std::string account;
};
}
#endif //BUDGET_CREATEOPERATION_H

View File

@ -0,0 +1,34 @@
//
// Created by quentin on 1/17/23.
//
#include <fstream>
#include "earnOperation.h"
#include "../database.h"
#include "../exceptions/badValue.h"
#include "../main.h"
using namespace Budget::OptHandlers;
void EarnOperation::commit() {
if (!Database::doesAccountExist(account, db))
throw Budget::Exceptions::BadValue("Account " + account + " doesn't exist");
long long id = Database::pay(account, flags.value, flags.description, flags.receipt, flags.date, db);
if (!flags.receipt.empty()) {
std::string extension;
size_t pos = flags.receipt.find_last_of('.');
if (pos != std::string::npos) {
extension = flags.receipt.substr(pos);
}
std::ifstream source(flags.receipt, std::ios::binary);
std::ofstream dest(storageD + "receipts/receipt/" + std::to_string(id) + extension, std::ios::binary);
dest << source.rdbuf();
source.close();
dest.close();
}
}
EarnOperation::EarnOperation(sqlite3 *db, std::string account) : Operation(db), account(std::move(account)) {}

View File

@ -0,0 +1,34 @@
//
// Created by quentin on 1/17/23.
//
#ifndef BUDGET_EARNOPERATION_H
#define BUDGET_EARNOPERATION_H
#include "operation.h"
#include <string>
#include <ctime>
namespace Budget::OptHandlers {
class EarnOperation : public Operation {
public:
void commit() override;
explicit EarnOperation(sqlite3 *db, std::string account);
struct Flags : public Operation::Flags {
long double value;
std::string description;
std::string receipt;
long long date = std::time(nullptr);
};
Flags flags;
private:
std::string account;
};
}
#endif //BUDGET_EARNOPERATION_H

View File

@ -1,51 +1,331 @@
//
// Created by quentin on 8/13/22.
// Created by quentin on 1/8/23.
//
#include <iostream>
#include "mainOptHandler.h"
#include "accountOperation.h"
#include "createOperation.h"
#include "earnOperation.h"
#include "PaymentOperation.h"
#include "../exceptions/helpRequested.h"
#include "../exceptions/badValue.h"
#include <iostream>
#include <getopt.h>
#include <cstring>
using namespace Budget::OptHandlers;
MainOptHandler::MainOptHandler(const std::vector<char *> &argv) : OptHandler(argv) {}
void MainOptHandler::parse() {
struct option longOpts[] = {
{"help", no_argument, nullptr, 'h'},
{"account", no_argument, nullptr, 'a'},
MainOptHandler::MainOptHandler(const std::vector<char *> &_argv, sqlite3 *db) : argv(_argv), db(db) {
struct option actionLongOpts[] = {
{"help", no_argument, nullptr, 'h'},
{"account", required_argument, nullptr, 'a'},
{"create", required_argument, nullptr, 'c'},
{"earn", required_argument, nullptr, 'e'},
{"payment", required_argument, nullptr, 'p'}
};
while (true) {
int opt = getopt_long(getArgc(), getArgv(), "ha", longOpts, nullptr);
if (opt == -1) {
int opt = getopt_long(argv.size(), argv.data(), "ha:c:e:p:", actionLongOpts, nullptr);
if (opt == -1)
break;
}
switch (opt) {
case 'h':
setOpts.help = true;
break;
help();
throw Budget::Exceptions::HelpRequested("Help requested at main");
case 'a':
setOpts.account = true;
setOpts.accountArgStart = optind - 1;
return;
case '?':
setOpts.help = true;
setOpts.helpOut = stderr;
accountOptHandler(optarg);
break;
case 'c':
createOptHandler(optarg);
break;
case 'e':
earnOptHandler(optarg);
break;
case 'p':
paymentOptHandler(optarg);
break;
case '?':
help();
throw Budget::Exceptions::HelpRequested("Help requested at main, unknown argument.");
default:
break;
}
}
}
void MainOptHandler::help() {
fprintf(setOpts.helpOut, "Help budget {-ha}\n");
fprintf(setOpts.helpOut, " -h --help Output this message.\n");
fprintf(setOpts.helpOut, " -a --account Do budget -a -h for more info.\n");
void MainOptHandler::accountOptHandler(std::string account) {
struct option accountLongOpts[] = {
{"help", no_argument, nullptr, 'h'},
{"account", required_argument, nullptr, 'a'},
{"create", required_argument, nullptr, 'c'},
{"earn", required_argument, nullptr, 'e'},
{"payment", required_argument, nullptr, 'p'},
{"delete", no_argument, nullptr, 'd'},
{"force-delete", no_argument, nullptr, 'F'},
{"value", no_argument, nullptr, 'v'},
{"description", required_argument, nullptr, 'D'},
};
auto acctOperation = std::make_unique<AccountOperation>(db, account);
while (true) {
int opt = getopt_long(argv.size(), argv.data(), "ha:c:e:p:dFvD:", accountLongOpts, nullptr);
if (opt == -1)
break;
switch (opt) {
case 'h':
help();
throw Budget::Exceptions::HelpRequested("Help requested at account");
case 'a':
case 'c':
case 'e':
case 'p':
optind--;
operations.push(std::move(acctOperation));
return;
case 'd':
acctOperation->flags.del = true;
break;
case 'F':
acctOperation->flags.del = true;
acctOperation->flags.forceDel = true;
break;
case 'v':
acctOperation->flags.value = true;
break;
case 'D':
acctOperation->flags.description = optarg;
break;
case '?':
help();
throw Budget::Exceptions::HelpRequested("Help requested at account, unknown argument.");
default:
break;
}
}
operations.push(std::move(acctOperation));
}
const MainOptHandler::SetOpts *MainOptHandler::getSetOpts() const {
return &setOpts;
void MainOptHandler::createOptHandler(std::string account) {
struct option createLongOpts[] = {
{"help", no_argument, nullptr, 'h'},
{"account", required_argument, nullptr, 'a'},
{"create", required_argument, nullptr, 'c'},
{"earn", required_argument, nullptr, 'e'},
{"payment", required_argument, nullptr, 'p'},
{"description", required_argument, nullptr, 'd'},
};
auto createOperation = std::make_unique<CreateOperation>(db, account);
while (true) {
int opt = getopt_long(argv.size(), argv.data(), "ha:c:e:p:d:", createLongOpts, nullptr);
if (opt == -1)
break;
switch (opt) {
case 'h':
help();
throw Budget::Exceptions::HelpRequested("Help requested at create");
case 'a':
case 'c':
case 'e':
case 'p':
optind--;
operations.push(std::move(createOperation));
return;
case 'd':
createOperation->flags.description = optarg;
break;
case '?':
help();
throw Budget::Exceptions::HelpRequested("Help requested at create, unknown argument.");
default:
break;
}
}
operations.push(std::move(createOperation));
}
void MainOptHandler::earnOptHandler(std::string account) {
struct option earnLongOpts[] = {
{"help", no_argument, nullptr, 'h'},
{"account", required_argument, nullptr, 'a'},
{"create", required_argument, nullptr, 'c'},
{"earn", required_argument, nullptr, 'e'},
{"payment", required_argument, nullptr, 'p'},
{"value", required_argument, nullptr, 'v'},
{"description", required_argument, nullptr, 'd'},
{"receipt", required_argument, nullptr, 'r'},
{"date", required_argument, nullptr, 'D'},
};
auto earnOperation = std::make_unique<EarnOperation>(db, account);
while (true) {
int opt = getopt_long(argv.size(), argv.data(), "ha:c:e:p:v:d:r:D:", earnLongOpts, nullptr);
if (opt == -1)
break;
switch (opt) {
case 'h':
help();
throw Budget::Exceptions::HelpRequested("Help requested at earn");
case 'a':
case 'c':
case 'e':
case 'p':
optind--;
operations.push(std::move(earnOperation));
return;
case 'v': {
try {
earnOperation->flags.value = std::stod(optarg);
} catch (std::exception const &e) {
help();
std::cout << "Bad value value" << std::endl;
throw Budget::Exceptions::BadValue("Bad value, cannot parse to decimal.");
}
break;
}
case 'd':
earnOperation->flags.description = optarg;
break;
case 'r':
earnOperation->flags.receipt = optarg;
break;
case 'D': {
struct tm t{};
time_t ts;
memset(&t, 0, sizeof(struct tm));
if (strptime(optarg, "%m/%d/%YT%H:%M:%S", &t) == nullptr) {
help();
std::cout << "Bad time value" << std::endl;
throw Budget::Exceptions::BadValue("Bad Value, cannot parse to int.");
}
ts = mktime(&t);
earnOperation->flags.date = (long long) ts;
break;
}
case '?':
help();
throw Budget::Exceptions::HelpRequested("Help requested at earn, unknown argument.");
default:
break;
}
}
operations.push(std::move(earnOperation));
}
void MainOptHandler::paymentOptHandler(std::string account) {
struct option paymentLongOpts[] = {
{"help", no_argument, nullptr, 'h'},
{"account", required_argument, nullptr, 'a'},
{"create", required_argument, nullptr, 'c'},
{"earn", required_argument, nullptr, 'e'},
{"payment", required_argument, nullptr, 'p'},
{"value", required_argument, nullptr, 'v'},
{"description", required_argument, nullptr, 'd'},
{"receipt", required_argument, nullptr, 'r'},
{"date", required_argument, nullptr, 'D'},
};
auto payOperation = std::make_unique<PaymentOperation>(db, account);
while (true) {
int opt = getopt_long(argv.size(), argv.data(), "ha:c:e:p:v:d:r:D:", paymentLongOpts, nullptr);
if (opt == -1)
break;
switch (opt) {
case 'h':
help();
throw Budget::Exceptions::HelpRequested("Help requested at payment");
case 'a':
case 'c':
case 'e':
case 'p':
optind--;
operations.push(std::move(payOperation));
return;
case 'v': {
try {
payOperation->flags.value = std::stod(optarg);
} catch (std::exception const &e) {
help();
std::cout << "Bad value value" << std::endl;
throw Budget::Exceptions::BadValue("Bad value, cannot parse to decimal.");
}
break;
}
case 'd':
payOperation->flags.description = optarg;
break;
case 'r':
payOperation->flags.receipt = optarg;
break;
case 'D': {
struct tm t{};
time_t ts;
memset(&t, 0, sizeof(struct tm));
if (strptime(optarg, "%m/%d/%YT%H:%M:%S", &t) == nullptr) {
help();
std::cout << "Bad time value" << std::endl;
throw Budget::Exceptions::BadValue("Bad value, time invalid.");
}
ts = mktime(&t);
payOperation->flags.date = (long long) ts;
break;
}
case '?':
help();
throw Budget::Exceptions::HelpRequested("Help requested at payment");
default:
break;
}
}
operations.push(std::move(payOperation));
}
void MainOptHandler::help() {
std::cout << "Output of budget.\n"
"Usage:\n"
" budget <action> [options] ...\n"
"Actions:\n"
" -h --help Prints this.\n"
" -a --account<=STRING> Management tools for an account.\n"
" -c --create<=STRING> Creates a new account with NAME.\n"
" -e --earn<=STRING> Add an earning to an account.\n"
" -p --payment<=STRING> Add a payment to an account.\n"
"Account Options: [-dvD]\n"
" -d --delete Deletes specified account.\n"
" --force-delete Deletes the specified account without confirmation.\n"
" -v --value Gets the current value of the account.\n"
" -D --description<=STRING> Changes the description of the account.\n"
"Create Options: [-d]\n"
" -d --description<=STRING> Sets a description for an account.\n"
"Earn Options: -v [-drD]\n"
" -v --value=<FlOAT> Value for earning.\n"
" -d --description=<STRING> Description for earning.\n"
" -r --receipt=<PATH> Path to file to store in DB as receipt.\n"
" -D --date=<mm/dd/yyyyTHH:MM:SS> Date as dd/mm/yyyyTHH:MM:SS. Default will be today.\n"
"Payment Options: -v [-drD]\n"
" -v --value=<FlOAT> Value for payment.\n"
" -d --description=<STRING> Description for payment.\n"
" -r --receipt=<PATH> Path to file to store in DB as receipt.\n"
" -D --date=<mm/dd/yyyyTHH:MM:SS> Date as dd/mm/yyyyTHH:MM:SS. Default will be today.\n"
"\n"
"Arguments are processed like blocks with each one terminated by the next Action. For example\n"
"\n"
"budget -cAcct -eAcct -v10.00 -r\"./receipt.pdf\" -pAcct -v5.50 -r\"./payment.pdf\"\n"
"\n"
"Does the following in order:\n"
"Creates an account named Acct with no description.\n"
"Earns 10.00 to it with a receipt.\n"
"Pays 5.50 to it with a receipt." << std::endl;
}

View File

@ -1,32 +1,50 @@
//
// Created by quentin on 8/13/22.
// Created by quentin on 1/8/23.
//
#ifndef BUDGET_MAINOPTHANDLER_H
#define BUDGET_MAINOPTHANDLER_H
#include "optHandler.h"
#include <vector>
#include <string>
#include <queue>
#include <memory>
#include "operation.h"
namespace Budget::OptHandlers {
class MainOptHandler : public OptHandler {
struct SetOpts {
FILE *helpOut = stdout;
bool help = false;
bool account = false;
int accountArgStart = -1;
};
/**
* @class MainOptHandler
* @brief Handles command line options for the main menu of the budget application
*
* The MainOptHandler class handles command line options passed to the budget application. It uses the getopt_long function
* to parse the options and perform the corresponding actions.
*/
class MainOptHandler {
public:
explicit MainOptHandler(const std::vector<char *> &argv);
/**
* @brief Constructor for MainOptHandler
*
* @param argv Vector of arguments passed to the application
* @param pSqlite3 The database connection to use
*/
explicit MainOptHandler(const std::vector<char *> &argv, sqlite3 *pSqlite3);
void parse() override;
void help();
void help() override;
[[nodiscard]] const SetOpts *getSetOpts() const;
std::queue<std::unique_ptr<Operation>> operations;
private:
SetOpts setOpts;
void accountOptHandler(std::string account);
void createOptHandler(std::string account);
void earnOptHandler(std::string account);
void paymentOptHandler(std::string account);
const std::vector<char *> &argv;
sqlite3 *db;
};
}

View File

@ -0,0 +1,7 @@
//
// Created by quentin on 1/17/23.
//
#include "operation.h"
Budget::OptHandlers::Operation::Operation(sqlite3 *db) : db(db) {}

View File

@ -0,0 +1,26 @@
//
// Created by quentin on 1/17/23.
//
#ifndef BUDGET_OPERATION_H
#define BUDGET_OPERATION_H
#include <sqlite3.h>
namespace Budget::OptHandlers {
class Operation {
public:
explicit Operation(sqlite3 *db);
virtual void commit() = 0;
struct Flags {
};
protected:
sqlite3 *db{};
};
}
#endif //BUDGET_OPERATION_H

View File

@ -1,20 +0,0 @@
//
// Created by quentin on 8/13/22.
//
#include "optHandler.h"
#include <utility>
using namespace Budget::OptHandlers;
OptHandler::OptHandler(std::vector<char *> argv) : argv(std::move(argv)) {}
int OptHandler::getArgc() {
return argv.size();
}
char **OptHandler::getArgv() {
return argv.data();
}

View File

@ -1,33 +0,0 @@
//
// Created by quentin on 8/13/22.
//
#ifndef BUDGET_OPTHANDLER_H
#define BUDGET_OPTHANDLER_H
#include <getopt.h>
#include <cstdio>
#include <string>
#include <vector>
namespace Budget::OptHandlers {
class OptHandler {
public:
explicit OptHandler(std::vector<char *> argv);
virtual void parse() = 0;
int getArgc();
char **getArgv();
private:
std::vector<char *> argv;
virtual void help() = 0;
};
}
#endif //BUDGET_OPTHANDLER_H

26
src/sqliteDb.h Normal file
View File

@ -0,0 +1,26 @@
//
// Created by quentin on 1/22/23.
//
#ifndef BUDGET_SQLITEDB_H
#define BUDGET_SQLITEDB_H
#include <sqlite3.h>
class SqliteDb {
public:
explicit SqliteDb(sqlite3 *db) : m_db(db) {}
~SqliteDb() {
sqlite3_close(m_db);
}
explicit operator sqlite3 *() {
return m_db;
}
private:
sqlite3 *m_db;
};
#endif //BUDGET_SQLITEDB_H

26
src/utilities.cpp Normal file
View File

@ -0,0 +1,26 @@
//
// Created by quentin on 1/21/23.
//
#include <iostream>
#include "utilities.h"
bool Utilities::confirm(const std::string &question) {
std::string input;
std::cout << question << std::endl;
std::cout << "Please enter 'y' or 'n': ";
std::cin >> input;
std::transform(input.begin(), input.end(), input.begin(), ::tolower);
while (input != "y" && input != "n" && input != "yes" && input != "no") {
std::cout << "Invalid input. Please enter 'y' or 'n': ";
std::cin >> input;
std::transform(input.begin(), input.end(), input.begin(), ::tolower);
}
if (input == "y" || input == "yes") {
return true;
} else {
return false;
}
}

24
src/utilities.h Normal file
View File

@ -0,0 +1,24 @@
//
// Created by quentin on 1/21/23.
//
#ifndef BUDGET_UTILITIES_H
#define BUDGET_UTILITIES_H
#include <string>
class Utilities {
public:
/**
* @brief Asks the user to confirm an action with a yes or no question
*
* @param question The question to ask the user
*
* @return True if the user confirms the action, false otherwise
*/
static bool confirm(const std::string &question);
};
#endif //BUDGET_UTILITIES_H

View File

@ -1,97 +0,0 @@
//
// Created by quentin on 9/13/22.
//
#include "math.h"
//Polynomial Fit
#include <iostream>
#include <cmath>
using namespace Budget::Utilities;
PolynomialFunction Math::polynomialFit(int degree, int numberOfPoints, const double x[], const double y[]) {
//Array that will store the values of sigma(xi),sigma(xi^2),sigma(xi^3)....sigma(xi^2numberOfPoints)
double X[2 * degree + 1];
for (int i = 0; i < 2 * degree + 1; i++) {
X[i] = 0;
for (int j = 0; j < numberOfPoints; j++) {
//consecutive positions of the array will store numberOfPoints,sigma(xi),sigma(xi^2),sigma(xi^3)....sigma(xi^2n)
X[i] = X[i] + pow(x[j], i);
}
}
//B is the Normal matrix(augmented) that will store the equations, 'a' is for value of the final coefficients
double B[degree + 1][degree + 2], a[degree + 1];
for (int i = 0; i <= degree; i++) {
for (int j = 0; j <= degree; j++) {
//Build the Normal matrix by storing the corresponding coefficients at the right positions except the last column of the matrix
B[i][j] = X[i + j];
}
}
//Array to store the values of sigma(yi),sigma(xi*yi),sigma(xi^2*yi)...sigma(xi^degree*yi)
double Y[degree + 1];
for (int i = 0; i < degree + 1; i++) {
Y[i] = 0;
for (int j = 0; j < numberOfPoints; j++) {
//consecutive positions will store sigma(yi),sigma(xi*yi),sigma(xi^2*yi)...sigma(xi^degree*yi)
Y[i] = Y[i] + pow(x[j], i) * y[j];
}
}
for (int i = 0; i <= degree; i++) {
//load the values of Y as the last column of B(Normal Matrix but augmented)
B[i][degree + 1] = Y[i];
}
//degree is made degree+1 because the Gaussian Elimination part below was for degree equations, but here degree is the degree of polynomial and for degree degree we get degree+1 equations
degree = degree + 1;
//From now Gaussian Elimination starts(can be ignored) to solve the set of linear equations (Pivotisation)
for (int i = 0; i < degree; i++) {
for (int k = i + 1; k < degree; k++) {
if (B[i][i] < B[k][i]) {
for (int j = 0; j <= degree; j++) {
double temp = B[i][j];
B[i][j] = B[k][j];
B[k][j] = temp;
}
}
}
}
//loop to perform the gauss elimination
for (int i = 0; i < degree - 1; i++) {
for (int k = i + 1; k < degree; k++) {
double t = B[k][i] / B[i][i];
for (int j = 0; j <= degree; j++) {
//make the elements below the pivot elements equal to zero or elimnate the variables
B[k][j] = B[k][j] - t * B[i][j];
}
}
}
//back-substitution
for (int i = degree - 1; i >= 0; i--) {
//x is an array whose values correspond to the values of x,y,z..
//make the variable to be calculated equal to the rhs of the last equation
a[i] = B[i][degree];
for (int j = 0; j < degree; j++) {
//then subtract all the lhs values except the coefficient of the variable whose value is being calculated
if (j != i) {
a[i] = a[i] - B[i][j] * a[j];
}
}
//now finally divide the rhs by the coefficient of the variable to be calculated
a[i] = a[i] / B[i][i];
}
std::cout << "\nThe values of the coefficients are as follows:\n";
for (int i = 0; i < degree; i++)
std::cout << "x^" << i << "=" << a[i] << std::endl; // Print the values of x^0,x^1,x^2,x^3,....
std::cout << "\nHence the fitted Polynomial is given by:\ny=";
for (int i = 0; i < degree; i++)
std::cout << " + (" << a[i] << ")" << "x^" << i;
std::cout << "\n";
std::vector<double> va(a, a + degree);
return PolynomialFunction(va);
}

View File

@ -1,19 +0,0 @@
//
// Created by quentin on 9/13/22.
//
#ifndef BUDGET_MATH_H
#define BUDGET_MATH_H
#include "polynomialFunction.h"
namespace Budget::Utilities {
class Math {
public:
static PolynomialFunction polynomialFit(int degree, int numberOfPoints, const double x[], const double y[]);
};
}
#endif //BUDGET_MATH_H

View File

@ -1,21 +0,0 @@
//
// Created by quentin on 9/13/22.
//
#include "polynomialFunction.h"
#include <utility>
#include <cmath>
using namespace Budget::Utilities;
PolynomialFunction::PolynomialFunction(std::vector<double> a) : a(std::move(a)) {}
double PolynomialFunction::get(double x) {
double ret = 0;
for (int i = 0; i < a.size(); ++i) {
ret += a[i] * std::pow(x, i);
}
return ret;
}

View File

@ -1,25 +0,0 @@
//
// Created by quentin on 9/13/22.
//
#ifndef BUDGET_POLYNOMIALFUNCTION_H
#define BUDGET_POLYNOMIALFUNCTION_H
#include <vector>
namespace Budget::Utilities {
class PolynomialFunction {
private:
public:
explicit PolynomialFunction(std::vector<double> a);
double get(double x);
private:
std::vector<double> a;
};
}
#endif //BUDGET_POLYNOMIALFUNCTION_H