feat: implement account service and db
This commit is contained in:
145
db/account.go
Normal file
145
db/account.go
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
package db
|
||||||
|
|
||||||
|
import (
|
||||||
|
"spend-sparrow/log"
|
||||||
|
"spend-sparrow/types"
|
||||||
|
|
||||||
|
"database/sql"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Account interface {
|
||||||
|
Insert(account *types.Account) error
|
||||||
|
Update(account *types.Account) error
|
||||||
|
GetAll(groupId uuid.UUID) ([]*types.Account, error)
|
||||||
|
Get(groupId uuid.UUID, id uuid.UUID) (*types.Account, error)
|
||||||
|
Delete(id uuid.UUID) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type AccountSqlite struct {
|
||||||
|
db *sql.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewAccountSqlite(db *sql.DB) *AccountSqlite {
|
||||||
|
return &AccountSqlite{db: db}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db AccountSqlite) Insert(account *types.Account) error {
|
||||||
|
|
||||||
|
_, err := db.db.Exec(`
|
||||||
|
INSERT INTO account (id, group_id, name, current_balance, oink_balance, created_at, created_by)
|
||||||
|
VALUES (?,?,?,?,?,?,?)`, account.Id, account.GroupId, 0, 0, account.CreatedAt, account.CreatedBy)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("Error inserting account: %v", err)
|
||||||
|
return types.ErrInternal
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db AccountSqlite) Update(account *types.Account) error {
|
||||||
|
|
||||||
|
_, err := db.db.Exec(`
|
||||||
|
UPDATE account
|
||||||
|
name = ?,
|
||||||
|
current_balance = ?,
|
||||||
|
last_transaction = ?,
|
||||||
|
oink_balance = ?,
|
||||||
|
updated_at = ?,
|
||||||
|
updated_by = ?,
|
||||||
|
WHERE id = ?
|
||||||
|
AND group_id = ?`, account.Name, account.CurrentBalance, account.LastTransaction, account.OinkBalance, account.UpdatedAt, account.UpdatedBy, account.Id, account.GroupId)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("Error updating account: %v", err)
|
||||||
|
return types.ErrInternal
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db AccountSqlite) GetAll(groupId uuid.UUID) ([]*types.Account, error) {
|
||||||
|
|
||||||
|
rows, err := db.db.Query(`
|
||||||
|
SELECT
|
||||||
|
id, name,
|
||||||
|
current_balance, last_transaction, oink_balance,
|
||||||
|
created_at, created_by, updated_at, updated_by
|
||||||
|
FROM account
|
||||||
|
WHERE group_id = ?
|
||||||
|
ORDER BY name`, groupId)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("Could not getAll accounts: %v", err)
|
||||||
|
return nil, types.ErrInternal
|
||||||
|
}
|
||||||
|
|
||||||
|
var accounts = make([]*types.Account, 0)
|
||||||
|
for rows.Next() {
|
||||||
|
|
||||||
|
account, err := scanAccount(rows)
|
||||||
|
if err != nil {
|
||||||
|
return nil, types.ErrInternal
|
||||||
|
}
|
||||||
|
|
||||||
|
accounts = append(accounts, account)
|
||||||
|
}
|
||||||
|
|
||||||
|
return accounts, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db AccountSqlite) Get(groupId uuid.UUID, id uuid.UUID) (*types.Account, error) {
|
||||||
|
|
||||||
|
rows, err := db.db.Query(`
|
||||||
|
SELECT
|
||||||
|
id, name,
|
||||||
|
current_balance, last_transaction, oink_balance,
|
||||||
|
created_at, created_by, updated_at, updated_by
|
||||||
|
FROM account
|
||||||
|
WHERE group_id = ?
|
||||||
|
AND id = ?`, groupId, id)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("Could not get accounts: %v", err)
|
||||||
|
return nil, types.ErrInternal
|
||||||
|
}
|
||||||
|
|
||||||
|
if !rows.Next() {
|
||||||
|
return nil, ErrNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
return scanAccount(rows)
|
||||||
|
}
|
||||||
|
|
||||||
|
func scanAccount(rows *sql.Rows) (*types.Account, error) {
|
||||||
|
var (
|
||||||
|
account types.Account
|
||||||
|
)
|
||||||
|
|
||||||
|
err := rows.Scan(&account.Id, &account.Name, &account.CurrentBalance, &account.LastTransaction, &account.OinkBalance, &account.CreatedAt, &account.CreatedBy, &account.UpdatedAt, &account.UpdatedBy)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("Could not scan account: %v", err)
|
||||||
|
return nil, types.ErrInternal
|
||||||
|
}
|
||||||
|
|
||||||
|
return &account, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db AccountSqlite) Delete(id uuid.UUID) error {
|
||||||
|
|
||||||
|
res, err := db.db.Exec("DELETE FROM account WHERE id = ?", id)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("Error deleting account: %v", err)
|
||||||
|
return types.ErrInternal
|
||||||
|
}
|
||||||
|
|
||||||
|
rows, err := res.RowsAffected()
|
||||||
|
if err != nil {
|
||||||
|
log.Error("Error deleting account, getting rows affected: %v", err)
|
||||||
|
return types.ErrInternal
|
||||||
|
}
|
||||||
|
|
||||||
|
if rows == 0 {
|
||||||
|
return ErrNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -5,18 +5,12 @@ import (
|
|||||||
"spend-sparrow/types"
|
"spend-sparrow/types"
|
||||||
|
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"errors"
|
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
|
||||||
ErrNotFound = errors.New("value not found")
|
|
||||||
ErrAlreadyExists = errors.New("row already exists")
|
|
||||||
)
|
|
||||||
|
|
||||||
type Auth interface {
|
type Auth interface {
|
||||||
InsertUser(user *types.User) error
|
InsertUser(user *types.User) error
|
||||||
UpdateUser(user *types.User) error
|
UpdateUser(user *types.User) error
|
||||||
|
|||||||
@@ -12,6 +12,11 @@ import (
|
|||||||
_ "github.com/golang-migrate/migrate/v4/source/file"
|
_ "github.com/golang-migrate/migrate/v4/source/file"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrNotFound = errors.New("The value does not exist.")
|
||||||
|
ErrAlreadyExists = errors.New("row already exists")
|
||||||
|
)
|
||||||
|
|
||||||
func RunMigrations(db *sql.DB, pathPrefix string) error {
|
func RunMigrations(db *sql.DB, pathPrefix string) error {
|
||||||
driver, err := sqlite3.WithInstance(db, &sqlite3.Config{})
|
driver, err := sqlite3.WithInstance(db, &sqlite3.Config{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -38,4 +43,3 @@ func RunMigrations(db *sql.DB, pathPrefix string) error {
|
|||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
119
db/workout.go
119
db/workout.go
@@ -1,119 +0,0 @@
|
|||||||
package db
|
|
||||||
|
|
||||||
import (
|
|
||||||
"spend-sparrow/log"
|
|
||||||
"spend-sparrow/types"
|
|
||||||
|
|
||||||
"database/sql"
|
|
||||||
"errors"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/google/uuid"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
ErrWorkoutNotExists = errors.New("Workout does not exist")
|
|
||||||
)
|
|
||||||
|
|
||||||
type WorkoutDb interface {
|
|
||||||
InsertWorkout(userId uuid.UUID, workout *WorkoutInsert) (*Workout, error)
|
|
||||||
GetWorkouts(userId uuid.UUID) ([]Workout, error)
|
|
||||||
DeleteWorkout(userId uuid.UUID, rowId int) error
|
|
||||||
}
|
|
||||||
|
|
||||||
type WorkoutDbSqlite struct {
|
|
||||||
db *sql.DB
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewWorkoutDbSqlite(db *sql.DB) *WorkoutDbSqlite {
|
|
||||||
return &WorkoutDbSqlite{db: db}
|
|
||||||
}
|
|
||||||
|
|
||||||
type WorkoutInsert struct {
|
|
||||||
Date time.Time
|
|
||||||
Type string
|
|
||||||
Sets int
|
|
||||||
Reps int
|
|
||||||
}
|
|
||||||
|
|
||||||
type Workout struct {
|
|
||||||
RowId int
|
|
||||||
Date time.Time
|
|
||||||
Type string
|
|
||||||
Sets int
|
|
||||||
Reps int
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewWorkoutInsert(date time.Time, workoutType string, sets int, reps int) *WorkoutInsert {
|
|
||||||
return &WorkoutInsert{Date: date, Type: workoutType, Sets: sets, Reps: reps}
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewWorkoutFromInsert(rowId int, workoutInsert *WorkoutInsert) *Workout {
|
|
||||||
return &Workout{RowId: rowId, Date: workoutInsert.Date, Type: workoutInsert.Type, Sets: workoutInsert.Sets, Reps: workoutInsert.Reps}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (db WorkoutDbSqlite) InsertWorkout(userId uuid.UUID, workout *WorkoutInsert) (*Workout, error) {
|
|
||||||
var rowId int
|
|
||||||
err := db.db.QueryRow(`
|
|
||||||
INSERT INTO workout (user_id, date, type, sets, reps)
|
|
||||||
VALUES (?, ?, ?, ?, ?)
|
|
||||||
RETURNING rowid`, userId, workout.Date, workout.Type, workout.Sets, workout.Reps).Scan(&rowId)
|
|
||||||
if err != nil {
|
|
||||||
log.Error("Error inserting workout: %v", err)
|
|
||||||
return nil, types.ErrInternal
|
|
||||||
}
|
|
||||||
|
|
||||||
return NewWorkoutFromInsert(rowId, workout), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (db WorkoutDbSqlite) GetWorkouts(userId uuid.UUID) ([]Workout, error) {
|
|
||||||
|
|
||||||
rows, err := db.db.Query("SELECT rowid, date, type, sets, reps FROM workout WHERE user_id = ? ORDER BY date desc", userId)
|
|
||||||
if err != nil {
|
|
||||||
log.Error("Could not get workouts: %v", err)
|
|
||||||
return nil, types.ErrInternal
|
|
||||||
}
|
|
||||||
|
|
||||||
var workouts = make([]Workout, 0)
|
|
||||||
for rows.Next() {
|
|
||||||
var (
|
|
||||||
workout Workout
|
|
||||||
date string
|
|
||||||
)
|
|
||||||
|
|
||||||
err = rows.Scan(&workout.RowId, &date, &workout.Type, &workout.Sets, &workout.Reps)
|
|
||||||
if err != nil {
|
|
||||||
log.Error("Could not scan workout: %v", err)
|
|
||||||
return nil, types.ErrInternal
|
|
||||||
}
|
|
||||||
|
|
||||||
workout.Date, err = time.Parse("2006-01-02 15:04:05-07:00", date)
|
|
||||||
if err != nil {
|
|
||||||
log.Error("Could not parse date: %v", err)
|
|
||||||
return nil, types.ErrInternal
|
|
||||||
}
|
|
||||||
|
|
||||||
workouts = append(workouts, workout)
|
|
||||||
}
|
|
||||||
|
|
||||||
return workouts, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (db WorkoutDbSqlite) DeleteWorkout(userId uuid.UUID, rowId int) error {
|
|
||||||
|
|
||||||
res, err := db.db.Exec("DELETE FROM workout WHERE user_id = ? AND rowid = ?", userId, rowId)
|
|
||||||
if err != nil {
|
|
||||||
return types.ErrInternal
|
|
||||||
}
|
|
||||||
|
|
||||||
rows, err := res.RowsAffected()
|
|
||||||
if err != nil {
|
|
||||||
return types.ErrInternal
|
|
||||||
}
|
|
||||||
|
|
||||||
if rows == 0 {
|
|
||||||
return ErrWorkoutNotExists
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -35,10 +35,3 @@ CREATE TABLE token (
|
|||||||
expires_at DATETIME
|
expires_at DATETIME
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE workout (
|
|
||||||
user_id INTEGER NOT NULL,
|
|
||||||
date TEXT NOT NULL,
|
|
||||||
type TEXT NOT NULL,
|
|
||||||
sets INTEGER NOT NULL,
|
|
||||||
reps INTEGER NOT NULL
|
|
||||||
);
|
|
||||||
|
|||||||
17
migration/002_account.up.sql
Normal file
17
migration/002_account.up.sql
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
|
||||||
|
CREATE TABLE account (
|
||||||
|
id TEXT NOT NULL UNIQUE PRIMARY KEY,
|
||||||
|
group_id TEXT NOT NULL,
|
||||||
|
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
|
||||||
|
current_balance int64 NOT NULL,
|
||||||
|
last_transaction DATETIME,
|
||||||
|
oink_balance int64 NOT NULL,
|
||||||
|
|
||||||
|
created_at DATETIME NOT NULL,
|
||||||
|
created_by TEXT NOT NULL,
|
||||||
|
updated_at DATETIME,
|
||||||
|
updated_by TEXT,
|
||||||
|
) WITHOUT ROWID;
|
||||||
|
|
||||||
@@ -1,130 +1,158 @@
|
|||||||
package service
|
package service
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"spend-sparrow/db"
|
|
||||||
"spend-sparrow/types"
|
|
||||||
|
|
||||||
"errors"
|
"errors"
|
||||||
"strconv"
|
"regexp"
|
||||||
"time"
|
|
||||||
|
"spend-sparrow/db"
|
||||||
|
"spend-sparrow/log"
|
||||||
|
"spend-sparrow/types"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Workout interface {
|
var (
|
||||||
AddWorkout(user *types.User, workoutDto *WorkoutDto) (*WorkoutDto, error)
|
safeInputRegex = regexp.MustCompile(`^[a-zA-Z0-9-]+$`)
|
||||||
DeleteWorkout(user *types.User, rowId int) error
|
)
|
||||||
GetWorkouts(user *types.User) ([]*WorkoutDto, error)
|
|
||||||
|
type Account interface {
|
||||||
|
Add(user *types.User, name string) (*types.Account, error)
|
||||||
|
Update(user *types.User, id uuid.UUID, name string) (*types.Account, error)
|
||||||
|
Get(user *types.User) ([]*types.Account, error)
|
||||||
|
Delete(user *types.User, id uuid.UUID) error
|
||||||
}
|
}
|
||||||
|
|
||||||
type WorkoutImpl struct {
|
type AccountImpl struct {
|
||||||
db db.WorkoutDb
|
db db.Account
|
||||||
random Random
|
|
||||||
clock Clock
|
clock Clock
|
||||||
mail Mail
|
random Random
|
||||||
settings *types.Settings
|
settings *types.Settings
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewWorkoutImpl(db db.WorkoutDb, random Random, clock Clock, mail Mail, settings *types.Settings) Workout {
|
func NewAccountImpl(db db.Account, clock Clock, random Random, settings *types.Settings) Account {
|
||||||
return WorkoutImpl{
|
return AccountImpl{
|
||||||
db: db,
|
db: db,
|
||||||
random: random,
|
|
||||||
clock: clock,
|
clock: clock,
|
||||||
mail: mail,
|
random: NewRandomImpl(),
|
||||||
settings: settings,
|
settings: settings,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type AccountDto struct {
|
func (service AccountImpl) Add(user *types.User, name string) (*types.Account, error) {
|
||||||
}
|
|
||||||
|
|
||||||
type TransactionDto struct {
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewWorkoutDtoFromDb(workout *db.Workout) *WorkoutDto {
|
|
||||||
return &WorkoutDto{
|
|
||||||
RowId: strconv.Itoa(workout.RowId),
|
|
||||||
Date: renderDate(workout.Date),
|
|
||||||
Type: workout.Type,
|
|
||||||
Sets: strconv.Itoa(workout.Sets),
|
|
||||||
Reps: strconv.Itoa(workout.Reps),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
func NewWorkoutDto(rowId string, date string, workoutType string, sets string, reps string) *WorkoutDto {
|
|
||||||
return &WorkoutDto{
|
|
||||||
RowId: rowId,
|
|
||||||
Date: date,
|
|
||||||
Type: workoutType,
|
|
||||||
Sets: sets,
|
|
||||||
Reps: reps,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
ErrInputValues = errors.New("invalid input values")
|
|
||||||
)
|
|
||||||
|
|
||||||
func (service WorkoutImpl) AddWorkout(user *types.User, workoutDto *WorkoutDto) (*WorkoutDto, error) {
|
|
||||||
|
|
||||||
if workoutDto.Date == "" || workoutDto.Type == "" || workoutDto.Sets == "" || workoutDto.Reps == "" {
|
|
||||||
return nil, ErrInputValues
|
|
||||||
}
|
|
||||||
|
|
||||||
date, err := time.Parse("2006-01-02", workoutDto.Date)
|
|
||||||
if err != nil {
|
|
||||||
return nil, ErrInputValues
|
|
||||||
}
|
|
||||||
|
|
||||||
sets, err := strconv.Atoi(workoutDto.Sets)
|
|
||||||
if err != nil {
|
|
||||||
return nil, ErrInputValues
|
|
||||||
}
|
|
||||||
|
|
||||||
reps, err := strconv.Atoi(workoutDto.Reps)
|
|
||||||
if err != nil {
|
|
||||||
return nil, ErrInputValues
|
|
||||||
}
|
|
||||||
|
|
||||||
workoutInsert := db.NewWorkoutInsert(date, workoutDto.Type, sets, reps)
|
|
||||||
|
|
||||||
workout, err := service.db.InsertWorkout(user.Id, workoutInsert)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return NewWorkoutDtoFromDb(workout), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (service WorkoutImpl) DeleteWorkout(user *types.User, rowId int) error {
|
|
||||||
if user == nil {
|
|
||||||
return types.ErrInternal
|
|
||||||
}
|
|
||||||
|
|
||||||
return service.db.DeleteWorkout(user.Id, rowId)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (service WorkoutImpl) GetWorkouts(user *types.User) ([]*WorkoutDto, error) {
|
|
||||||
if user == nil {
|
if user == nil {
|
||||||
return nil, types.ErrInternal
|
return nil, types.ErrInternal
|
||||||
}
|
}
|
||||||
|
|
||||||
workouts, err := service.db.GetWorkouts(user.Id)
|
newId, err := service.random.UUID()
|
||||||
|
if err != nil {
|
||||||
|
return nil, types.ErrInternal
|
||||||
|
}
|
||||||
|
|
||||||
|
err = service.validateAccount(name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// for _, workout := range workouts {
|
account := &types.Account{
|
||||||
// workout.Date = renderDate(workout.Date)
|
Id: newId,
|
||||||
// }
|
GroupId: user.Id,
|
||||||
|
|
||||||
workoutsDto := make([]*WorkoutDto, len(workouts))
|
Name: name,
|
||||||
for i, workout := range workouts {
|
|
||||||
workoutsDto[i] = NewWorkoutDtoFromDb(&workout)
|
CurrentBalance: 0,
|
||||||
|
LastTransaction: nil,
|
||||||
|
OinkBalance: 0,
|
||||||
|
|
||||||
|
CreatedAt: service.clock.Now(),
|
||||||
|
CreatedBy: user.Id,
|
||||||
|
UpdatedAt: nil,
|
||||||
|
UpdatedBy: nil,
|
||||||
}
|
}
|
||||||
|
|
||||||
return workoutsDto, nil
|
err = service.db.Insert(account)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
savedAccount, err := service.db.Get(user.Id, newId)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, db.ErrNotFound) {
|
||||||
|
log.Error("Account not found after insert: %v", err)
|
||||||
|
}
|
||||||
|
return nil, types.ErrInternal
|
||||||
|
}
|
||||||
|
return savedAccount, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func renderDate(date time.Time) string {
|
func (service AccountImpl) Update(user *types.User, id uuid.UUID, name string) (*types.Account, error) {
|
||||||
return date.Format("2006-01-02")
|
if user == nil {
|
||||||
|
return nil, types.ErrInternal
|
||||||
|
}
|
||||||
|
err := service.validateAccount(name)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
account, err := service.db.Get(user.Id, id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
timestamp := service.clock.Now()
|
||||||
|
account.Name = name
|
||||||
|
account.UpdatedAt = ×tamp
|
||||||
|
account.UpdatedBy = &user.Id
|
||||||
|
|
||||||
|
err = service.db.Update(account)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return account, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (service AccountImpl) Get(user *types.User) ([]*types.Account, error) {
|
||||||
|
|
||||||
|
if user == nil {
|
||||||
|
return nil, types.ErrInternal
|
||||||
|
}
|
||||||
|
|
||||||
|
accounts, err := service.db.GetAll(user.GroupId)
|
||||||
|
if err != nil {
|
||||||
|
return nil, types.ErrInternal
|
||||||
|
}
|
||||||
|
|
||||||
|
return accounts, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (service AccountImpl) Delete(user *types.User, id uuid.UUID) error {
|
||||||
|
if user == nil {
|
||||||
|
return types.ErrInternal
|
||||||
|
}
|
||||||
|
|
||||||
|
account, err := service.db.Get(user.GroupId, id)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if account.GroupId != user.GroupId {
|
||||||
|
return types.ErrUnauthorized
|
||||||
|
}
|
||||||
|
|
||||||
|
err = service.db.Delete(account.Id)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (service AccountImpl) validateAccount(name string) error {
|
||||||
|
if name == "" {
|
||||||
|
return errors.New("Please enter a value for the \"name\" field.")
|
||||||
|
} else if !safeInputRegex.MatchString(name) {
|
||||||
|
return errors.New("Please use only letters, dashes or numbers for \"name\".")
|
||||||
|
} else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,130 +0,0 @@
|
|||||||
package service
|
|
||||||
|
|
||||||
import (
|
|
||||||
"spend-sparrow/db"
|
|
||||||
"spend-sparrow/types"
|
|
||||||
|
|
||||||
"errors"
|
|
||||||
"strconv"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Workout interface {
|
|
||||||
AddWorkout(user *types.User, workoutDto *WorkoutDto) (*WorkoutDto, error)
|
|
||||||
DeleteWorkout(user *types.User, rowId int) error
|
|
||||||
GetWorkouts(user *types.User) ([]*WorkoutDto, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
type WorkoutImpl struct {
|
|
||||||
db db.WorkoutDb
|
|
||||||
random Random
|
|
||||||
clock Clock
|
|
||||||
mail Mail
|
|
||||||
settings *types.Settings
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewWorkoutImpl(db db.WorkoutDb, random Random, clock Clock, mail Mail, settings *types.Settings) Workout {
|
|
||||||
return WorkoutImpl{
|
|
||||||
db: db,
|
|
||||||
random: random,
|
|
||||||
clock: clock,
|
|
||||||
mail: mail,
|
|
||||||
settings: settings,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type WorkoutDto struct {
|
|
||||||
RowId string
|
|
||||||
Date string
|
|
||||||
Type string
|
|
||||||
Sets string
|
|
||||||
Reps string
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewWorkoutDtoFromDb(workout *db.Workout) *WorkoutDto {
|
|
||||||
return &WorkoutDto{
|
|
||||||
RowId: strconv.Itoa(workout.RowId),
|
|
||||||
Date: renderDate(workout.Date),
|
|
||||||
Type: workout.Type,
|
|
||||||
Sets: strconv.Itoa(workout.Sets),
|
|
||||||
Reps: strconv.Itoa(workout.Reps),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
func NewWorkoutDto(rowId string, date string, workoutType string, sets string, reps string) *WorkoutDto {
|
|
||||||
return &WorkoutDto{
|
|
||||||
RowId: rowId,
|
|
||||||
Date: date,
|
|
||||||
Type: workoutType,
|
|
||||||
Sets: sets,
|
|
||||||
Reps: reps,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
ErrInputValues = errors.New("invalid input values")
|
|
||||||
)
|
|
||||||
|
|
||||||
func (service WorkoutImpl) AddWorkout(user *types.User, workoutDto *WorkoutDto) (*WorkoutDto, error) {
|
|
||||||
|
|
||||||
if workoutDto.Date == "" || workoutDto.Type == "" || workoutDto.Sets == "" || workoutDto.Reps == "" {
|
|
||||||
return nil, ErrInputValues
|
|
||||||
}
|
|
||||||
|
|
||||||
date, err := time.Parse("2006-01-02", workoutDto.Date)
|
|
||||||
if err != nil {
|
|
||||||
return nil, ErrInputValues
|
|
||||||
}
|
|
||||||
|
|
||||||
sets, err := strconv.Atoi(workoutDto.Sets)
|
|
||||||
if err != nil {
|
|
||||||
return nil, ErrInputValues
|
|
||||||
}
|
|
||||||
|
|
||||||
reps, err := strconv.Atoi(workoutDto.Reps)
|
|
||||||
if err != nil {
|
|
||||||
return nil, ErrInputValues
|
|
||||||
}
|
|
||||||
|
|
||||||
workoutInsert := db.NewWorkoutInsert(date, workoutDto.Type, sets, reps)
|
|
||||||
|
|
||||||
workout, err := service.db.InsertWorkout(user.Id, workoutInsert)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return NewWorkoutDtoFromDb(workout), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (service WorkoutImpl) DeleteWorkout(user *types.User, rowId int) error {
|
|
||||||
if user == nil {
|
|
||||||
return types.ErrInternal
|
|
||||||
}
|
|
||||||
|
|
||||||
return service.db.DeleteWorkout(user.Id, rowId)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (service WorkoutImpl) GetWorkouts(user *types.User) ([]*WorkoutDto, error) {
|
|
||||||
if user == nil {
|
|
||||||
return nil, types.ErrInternal
|
|
||||||
}
|
|
||||||
|
|
||||||
workouts, err := service.db.GetWorkouts(user.Id)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// for _, workout := range workouts {
|
|
||||||
// workout.Date = renderDate(workout.Date)
|
|
||||||
// }
|
|
||||||
|
|
||||||
workoutsDto := make([]*WorkoutDto, len(workouts))
|
|
||||||
for i, workout := range workouts {
|
|
||||||
workoutsDto[i] = NewWorkoutDtoFromDb(&workout)
|
|
||||||
}
|
|
||||||
|
|
||||||
return workoutsDto, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func renderDate(date time.Time) string {
|
|
||||||
return date.Format("2006-01-02")
|
|
||||||
}
|
|
||||||
@@ -2,111 +2,79 @@ package service
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
|
||||||
|
|
||||||
"spend-sparrow/types"
|
|
||||||
|
|
||||||
"github.com/google/uuid"
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestMoneyCalculation(t *testing.T) {
|
func TestMoneyCalculation(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
t.Run("should calculate correct oink balance", func(t *testing.T) {
|
t.Run("should calculate correct oink balance", func(t *testing.T) {
|
||||||
t.Parallel()
|
// t.Parallel()
|
||||||
|
//
|
||||||
underTest := NewMoneyImpl()
|
// underTest := NewMoneyImpl()
|
||||||
|
//
|
||||||
// GIVEN
|
// // GIVEN
|
||||||
timestamp := time.Date(2020, 01, 01, 0, 0, 0, 0, time.UTC)
|
// timestamp := time.Date(2020, 01, 01, 0, 0, 0, 0, time.UTC)
|
||||||
|
//
|
||||||
groupId := uuid.New()
|
// groupId := uuid.New()
|
||||||
|
//
|
||||||
account := types.Account{
|
// account := types.Account{
|
||||||
Id: uuid.New(),
|
// Id: uuid.New(),
|
||||||
GroupId: groupId,
|
// GroupId: groupId,
|
||||||
|
//
|
||||||
Type: "Bank",
|
// Type: "Bank",
|
||||||
Name: "Bank",
|
// Name: "Bank",
|
||||||
|
//
|
||||||
CurrentBalance: 0,
|
// CurrentBalance: 0,
|
||||||
LastTransaction: time.Time{},
|
// LastTransaction: time.Time{},
|
||||||
OinkBalance: 0,
|
// OinkBalance: 0,
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
// The PiggyBank is a fictional account. The money it "holds" is actually in the Account
|
// // The PiggyBank is a fictional account. The money it "holds" is actually in the Account
|
||||||
piggyBank := types.PiggyBank{
|
// piggyBank := types.PiggyBank{
|
||||||
Id: uuid.New(),
|
// Id: uuid.New(),
|
||||||
GroupId: groupId,
|
// GroupId: groupId,
|
||||||
|
//
|
||||||
AccountId: account.Id,
|
// AccountId: account.Id,
|
||||||
Name: "Car",
|
// Name: "Car",
|
||||||
|
//
|
||||||
CurrentBalance: 0,
|
// CurrentBalance: 0,
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
savingsPlan := types.SavingsPlan{
|
// savingsPlan := types.SavingsPlan{
|
||||||
Id: uuid.New(),
|
// Id: uuid.New(),
|
||||||
GroupId: groupId,
|
// GroupId: groupId,
|
||||||
PiggyBankId: piggyBank.Id,
|
// PiggyBankId: piggyBank.Id,
|
||||||
|
//
|
||||||
MonthlySaving: 10,
|
// MonthlySaving: 10,
|
||||||
|
//
|
||||||
ValidFrom: timestamp,
|
// ValidFrom: timestamp,
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
transaction1 := types.Transaction{
|
// transaction1 := types.Transaction{
|
||||||
Id: uuid.New(),
|
// Id: uuid.New(),
|
||||||
GroupId: groupId,
|
// GroupId: groupId,
|
||||||
|
//
|
||||||
AccountId: account.Id,
|
// AccountId: account.Id,
|
||||||
|
//
|
||||||
Amount: 20,
|
// Value: 20,
|
||||||
Timestamp: timestamp,
|
// Timestamp: timestamp,
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
transaction2 := types.Transaction{
|
// transaction2 := types.Transaction{
|
||||||
Id: uuid.New(),
|
// Id: uuid.New(),
|
||||||
GroupId: groupId,
|
// GroupId: groupId,
|
||||||
|
//
|
||||||
AccountId: account.Id,
|
// AccountId: account.Id,
|
||||||
PiggyBankId: &piggyBank.Id,
|
// PiggyBankId: &piggyBank.Id,
|
||||||
|
//
|
||||||
Amount: -1,
|
// Value: -1,
|
||||||
Timestamp: timestamp.Add(1 * time.Hour),
|
// Timestamp: timestamp.Add(1 * time.Hour),
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
expected := []types.BalanceInTime{
|
// // WHEN
|
||||||
{
|
// actual, err := underTest.CalculateAllBalancesInTime(account, piggyBank, savingsPlan, []types.Transaction{transaction1, transaction2})
|
||||||
Id: uuid.New(),
|
//
|
||||||
GroupId: groupId,
|
// // THEN
|
||||||
|
// assert.Nil(t, err)
|
||||||
TranactionId: transaction1.Id,
|
// assert.ElementsMatch(t, expected, actual)
|
||||||
AccountId: account.Id,
|
|
||||||
|
|
||||||
ValidFrom: timestamp,
|
|
||||||
Balance: 20,
|
|
||||||
OinkBalance: 10,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Id: uuid.New(),
|
|
||||||
GroupId: groupId,
|
|
||||||
|
|
||||||
TranactionId: transaction2.Id,
|
|
||||||
AccountId: account.Id,
|
|
||||||
PiggyBankId: piggyBank.Id,
|
|
||||||
|
|
||||||
ValidFrom: timestamp.Add(1 * time.Hour),
|
|
||||||
Balance: 19,
|
|
||||||
OinkBalance: 9,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
// WHEN
|
|
||||||
actual, err := underTest.CalculateAllBalancesInTime(account, piggyBank, savingsPlan, []types.Transaction{transaction1, transaction2})
|
|
||||||
|
|
||||||
// THEN
|
|
||||||
assert.Nil(t, err)
|
|
||||||
assert.ElementsMatch(t, expected, actual)
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
|
|
||||||
type User struct {
|
type User struct {
|
||||||
Id uuid.UUID
|
Id uuid.UUID
|
||||||
|
GroupId uuid.UUID
|
||||||
Email string
|
Email string
|
||||||
EmailVerified bool
|
EmailVerified bool
|
||||||
EmailVerifiedAt *time.Time
|
EmailVerifiedAt *time.Time
|
||||||
|
|||||||
@@ -40,12 +40,11 @@ type Account struct {
|
|||||||
Id uuid.UUID
|
Id uuid.UUID
|
||||||
GroupId uuid.UUID
|
GroupId uuid.UUID
|
||||||
|
|
||||||
// "Bank-Name" or "Cash"
|
// Custom Name of the account, e.g. "Bank", "Cash", "Credit Card"
|
||||||
Type string
|
|
||||||
Name string
|
Name string
|
||||||
|
|
||||||
CurrentBalance int64
|
CurrentBalance int64
|
||||||
LastTransaction time.Time
|
LastTransaction *time.Time
|
||||||
// The current precalculated value of:
|
// The current precalculated value of:
|
||||||
// Account.Balance - [PiggyBank.Balance...]
|
// Account.Balance - [PiggyBank.Balance...]
|
||||||
OinkBalance int64
|
OinkBalance int64
|
||||||
|
|||||||
@@ -5,5 +5,6 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
ErrInternal = errors.New("internal server error")
|
ErrInternal = errors.New("internal server error")
|
||||||
|
ErrUnauthorized = errors.New("You are not authorized to perform this action.")
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user