You've already forked openaccounting-server
mirror of
https://github.com/openaccounting/oa-server.git
synced 2025-12-09 00:50:59 +13:00
add budget feature
This commit is contained in:
@@ -7,6 +7,11 @@ import (
|
||||
/**
|
||||
* Changelog
|
||||
*
|
||||
* 1.4.0
|
||||
* - add `GET /orgs/:orgId/budget`
|
||||
* - add `POST /orgs/:orgId/budget`
|
||||
* - add `DELETE /orgs/:orgId/budget`
|
||||
*
|
||||
* 1.3.0
|
||||
* - add org.timezone
|
||||
*
|
||||
|
||||
149
core/api/budget.go
Normal file
149
core/api/budget.go
Normal file
@@ -0,0 +1,149 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"github.com/ant0ine/go-json-rest/rest"
|
||||
"github.com/openaccounting/oa-server/core/model"
|
||||
"github.com/openaccounting/oa-server/core/model/types"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
/**
|
||||
* @api {get} /orgs/:orgId/budget Get Budget
|
||||
* @apiVersion 1.4.0
|
||||
* @apiName GetBudget
|
||||
* @apiGroup Budget
|
||||
*
|
||||
* @apiHeader {String} Authorization HTTP Basic Auth
|
||||
* @apiHeader {String} Accept-Version ^1.4.0 semver versioning
|
||||
*
|
||||
* @apiSuccess {String} orgId Id of the Org.
|
||||
* @apiSuccess {Date} inserted Date Transaction was created
|
||||
* @apiSuccess {Object[]} items Array of Budget Items
|
||||
*
|
||||
* @apiSuccessExample Success-Response:
|
||||
* HTTP/1.1 200 OK
|
||||
* [
|
||||
* {
|
||||
* "orgId": "11111111111111111111111111111111",
|
||||
* "inserted": "2020-01-13T20:12:29.720Z",
|
||||
* "items": [
|
||||
* {
|
||||
* "accountId": "11111111111111111111111111111111",
|
||||
* "amount": 35000,
|
||||
* },
|
||||
* {
|
||||
* "accountId": "22222222222222222222222222222222",
|
||||
* "amount": 55000
|
||||
* }
|
||||
* ]
|
||||
* }
|
||||
* ]
|
||||
*
|
||||
* @apiUse NotAuthorizedError
|
||||
* @apiUse InternalServerError
|
||||
*/
|
||||
func GetBudget(w rest.ResponseWriter, r *rest.Request) {
|
||||
user := r.Env["USER"].(*types.User)
|
||||
orgId := r.PathParam("orgId")
|
||||
|
||||
budget, err := model.Instance.GetBudget(orgId, user.Id)
|
||||
|
||||
if err != nil {
|
||||
rest.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteJson(&budget)
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {post} /orgs/:orgId/budget Create a Budget
|
||||
* @apiVersion 1.4.0
|
||||
* @apiName PostBudget
|
||||
* @apiGroup Budget
|
||||
*
|
||||
* @apiHeader {String} Authorization HTTP Basic Auth
|
||||
* @apiHeader {String} Accept-Version ^1.4.0 semver versioning
|
||||
*
|
||||
* @apiParam {Object[]} items Array of Budget Items.
|
||||
* @apiParam {String} items.accountId Id of Expense Account
|
||||
* @apiParam {Number} items.amount Amount budgeted
|
||||
*
|
||||
* @apiSuccess {String} orgId Id of the Org.
|
||||
* @apiSuccess {Date} inserted Date Transaction was created
|
||||
* @apiSuccess {Object[]} items Array of Budget Items
|
||||
*
|
||||
* @apiSuccessExample Success-Response:
|
||||
* HTTP/1.1 200 OK
|
||||
* [
|
||||
* {
|
||||
* "orgId": "11111111111111111111111111111111",
|
||||
* "inserted": "2020-01-13T20:12:29.720Z",
|
||||
* "items": [
|
||||
* {
|
||||
* "accountId": "11111111111111111111111111111111",
|
||||
* "amount": 35000,
|
||||
* },
|
||||
* {
|
||||
* "accountId": "22222222222222222222222222222222",
|
||||
* "amount": 55000
|
||||
* }
|
||||
* ]
|
||||
* }
|
||||
* ]
|
||||
*
|
||||
* @apiUse NotAuthorizedError
|
||||
* @apiUse InternalServerError
|
||||
*/
|
||||
func PostBudget(w rest.ResponseWriter, r *rest.Request) {
|
||||
user := r.Env["USER"].(*types.User)
|
||||
orgId := r.PathParam("orgId")
|
||||
|
||||
budget := types.Budget{}
|
||||
err := r.DecodeJsonPayload(&budget)
|
||||
|
||||
if err != nil {
|
||||
rest.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
budget.OrgId = orgId
|
||||
|
||||
err = model.Instance.CreateBudget(&budget, user.Id)
|
||||
|
||||
if err != nil {
|
||||
rest.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteJson(budget)
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {delete} /orgs/:orgId/budget Delete Budget
|
||||
* @apiVersion 1.4.0
|
||||
* @apiName DeleteBudget
|
||||
* @apiGroup Budget
|
||||
*
|
||||
* @apiHeader {String} Authorization HTTP Basic Auth
|
||||
* @apiHeader {String} Accept-Version ^1.4.0 semver versioning
|
||||
*
|
||||
* @apiSuccessExample Success-Response:
|
||||
* HTTP/1.1 200 OK
|
||||
*
|
||||
* @apiUse NotAuthorizedError
|
||||
* @apiUse InternalServerError
|
||||
*/
|
||||
func DeleteBudget(w rest.ResponseWriter, r *rest.Request) {
|
||||
user := r.Env["USER"].(*types.User)
|
||||
orgId := r.PathParam("orgId")
|
||||
|
||||
err := model.Instance.DeleteBudget(orgId, user.Id)
|
||||
|
||||
if err != nil {
|
||||
rest.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
@@ -46,5 +46,8 @@ func GetRouter(auth *AuthMiddleware, prefix string) (rest.App, error) {
|
||||
rest.Put(prefix+"/orgs/:orgId/invites/:inviteId", auth.RequireAuth(PutInvite)),
|
||||
rest.Delete(prefix+"/orgs/:orgId/invites/:inviteId", auth.RequireAuth(DeleteInvite)),
|
||||
rest.Get(prefix+"/health-check", GetSystemHealthStatus),
|
||||
rest.Get(prefix+"/orgs/:orgId/budget", auth.RequireAuth(GetBudget)),
|
||||
rest.Post(prefix+"/orgs/:orgId/budget", auth.RequireAuth(PostBudget)),
|
||||
rest.Delete(prefix+"/orgs/:orgId/budget", auth.RequireAuth(DeleteBudget)),
|
||||
)
|
||||
}
|
||||
|
||||
58
core/model/budget.go
Normal file
58
core/model/budget.go
Normal file
@@ -0,0 +1,58 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"github.com/openaccounting/oa-server/core/model/types"
|
||||
)
|
||||
|
||||
type BudgetInterface interface {
|
||||
GetBudget(string, string) (*types.Budget, error)
|
||||
CreateBudget(*types.Budget, string) error
|
||||
DeleteBudget(string, string) error
|
||||
}
|
||||
|
||||
func (model *Model) GetBudget(orgId string, userId string) (*types.Budget, error) {
|
||||
belongs, err := model.UserBelongsToOrg(userId, orgId)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if belongs == false {
|
||||
return nil, errors.New("User does not belong to org")
|
||||
}
|
||||
|
||||
return model.db.GetBudget(orgId)
|
||||
}
|
||||
|
||||
func (model *Model) CreateBudget(budget *types.Budget, userId string) error {
|
||||
belongs, err := model.UserBelongsToOrg(userId, budget.OrgId)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if belongs == false {
|
||||
return errors.New("User does not belong to org")
|
||||
}
|
||||
|
||||
if budget.OrgId == "" {
|
||||
return errors.New("orgId required")
|
||||
}
|
||||
|
||||
return model.db.InsertAndReplaceBudget(budget)
|
||||
}
|
||||
|
||||
func (model *Model) DeleteBudget(orgId string, userId string) error {
|
||||
belongs, err := model.UserBelongsToOrg(userId, orgId)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if belongs == false {
|
||||
return errors.New("User does not belong to org")
|
||||
}
|
||||
|
||||
return model.db.DeleteBudget(orgId)
|
||||
}
|
||||
117
core/model/db/budget.go
Normal file
117
core/model/db/budget.go
Normal file
@@ -0,0 +1,117 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"github.com/openaccounting/oa-server/core/model/types"
|
||||
"github.com/openaccounting/oa-server/core/util"
|
||||
"time"
|
||||
)
|
||||
|
||||
type BudgetInterface interface {
|
||||
GetBudget(string) (*types.Budget, error)
|
||||
InsertAndReplaceBudget(*types.Budget) error
|
||||
DeleteBudget(string) error
|
||||
}
|
||||
|
||||
const budgetFields = "LOWER(HEX(accountId)),inserted,amount"
|
||||
|
||||
func (db *DB) GetBudget(orgId string) (*types.Budget, error) {
|
||||
var budget types.Budget
|
||||
var inserted int64
|
||||
|
||||
rows, err := db.Query("SELECT "+budgetFields+" FROM budgetitem WHERE orgId = UNHEX(?) ORDER BY HEX(accountId)", orgId)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
defer rows.Close()
|
||||
|
||||
items := make([]*types.BudgetItem, 0)
|
||||
|
||||
for rows.Next() {
|
||||
i := new(types.BudgetItem)
|
||||
err := rows.Scan(&i.AccountId, &inserted, &i.Amount)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
items = append(items, i)
|
||||
}
|
||||
|
||||
err = rows.Err()
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(items) == 0 {
|
||||
return nil, errors.New("Budget not found")
|
||||
}
|
||||
|
||||
budget.OrgId = orgId
|
||||
budget.Inserted = util.MsToTime(inserted)
|
||||
budget.Items = items
|
||||
|
||||
return &budget, nil
|
||||
}
|
||||
|
||||
func (db *DB) InsertAndReplaceBudget(budget *types.Budget) (err error) {
|
||||
budget.Inserted = time.Now()
|
||||
|
||||
// Save to db
|
||||
dbTx, err := db.Begin()
|
||||
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if p := recover(); p != nil {
|
||||
dbTx.Rollback()
|
||||
panic(p) // re-throw panic after Rollback
|
||||
} else if err != nil {
|
||||
dbTx.Rollback()
|
||||
} else {
|
||||
err = dbTx.Commit()
|
||||
}
|
||||
}()
|
||||
|
||||
// delete previous budget
|
||||
query1 := "DELETE FROM budgetitem WHERE orgId = UNHEX(?)"
|
||||
|
||||
_, err = dbTx.Exec(
|
||||
query1,
|
||||
budget.OrgId,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// save items
|
||||
for _, item := range budget.Items {
|
||||
query := "INSERT INTO budgetitem(orgId,accountId,inserted,amount) VALUES (UNHEX(?),UNHEX(?),?,?)"
|
||||
|
||||
_, err = dbTx.Exec(
|
||||
query,
|
||||
budget.OrgId,
|
||||
item.AccountId,
|
||||
util.TimeToMs(budget.Inserted),
|
||||
item.Amount)
|
||||
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (db *DB) DeleteBudget(orgId string) error {
|
||||
query := "DELETE FROM budgetitem WHERE orgId = UNHEX(?)"
|
||||
|
||||
_, err := db.Exec(query, orgId)
|
||||
|
||||
return err
|
||||
}
|
||||
@@ -19,6 +19,7 @@ type Datastore interface {
|
||||
SessionInterface
|
||||
ApiKeyInterface
|
||||
SystemHealthInteface
|
||||
BudgetInterface
|
||||
}
|
||||
|
||||
func NewDB(dataSourceName string) (*DB, error) {
|
||||
|
||||
@@ -23,6 +23,7 @@ type Interface interface {
|
||||
SessionInterface
|
||||
ApiKeyInterface
|
||||
SystemHealthInteface
|
||||
BudgetInterface
|
||||
}
|
||||
|
||||
func NewModel(db db.Datastore, bcrypt util.Bcrypt, config types.Config) *Model {
|
||||
|
||||
17
core/model/types/budget.go
Normal file
17
core/model/types/budget.go
Normal file
@@ -0,0 +1,17 @@
|
||||
package types
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
type Budget struct {
|
||||
OrgId string `json:"orgId"`
|
||||
Inserted time.Time `json:"inserted"`
|
||||
Items []*BudgetItem `json:"items"`
|
||||
}
|
||||
|
||||
type BudgetItem struct {
|
||||
OrgId string `json:"-"`
|
||||
AccountId string `json:"accountId"`
|
||||
Amount int64 `json:"amount"`
|
||||
}
|
||||
Reference in New Issue
Block a user