...
 
Commits (11)
......@@ -11,7 +11,7 @@ import (
"github.com/lib/pq"
"github.com/muesli/cache2go"
uuid "github.com/nu7hatch/gouuid"
"github.com/satori/go.uuid"
)
var (
......@@ -112,26 +112,30 @@ func InitDatabase() {
user_id int,
parent bigserial,
name text NOT NULL,
description text,
private bool DEFAULT false,
private_balance bool DEFAULT true,
CONSTRAINT uk_budgets_uuid UNIQUE (uuid),
CONSTRAINT fk_budgets_project_id FOREIGN KEY (project_id) REFERENCES projects (id) MATCH SIMPLE ON UPDATE CASCADE ON DELETE CASCADE,
CONSTRAINT fk_budgets_user_id FOREIGN KEY (user_id) REFERENCES users (id) MATCH SIMPLE ON UPDATE CASCADE ON DELETE CASCADE
)`,
`CREATE TABLE IF NOT EXISTS payments
(
id bigserial PRIMARY KEY,
user_id bigserial NOT NULL,
budget_id bigserial NOT NULL,
created_at timestamp NOT NULL,
amount int NOT NULL,
currency text NOT NULL,
code text,
description text,
code text DEFAULT '',
purpose text DEFAULT '',
remote_account text NOT NULL,
remote_name text NOT NULL,
remote_transaction_id text DEFAULT '',
remote_bank_id text DEFAULT '',
source text NOT NULL,
source_id text NOT NULL,
source_payer_id text,
source_transaction_id text NOT NULL,
created_at timestamp NOT NULL,
CONSTRAINT fk_payments_user_id FOREIGN KEY (user_id) REFERENCES users (id) MATCH SIMPLE ON UPDATE CASCADE ON DELETE CASCADE
pending bool DEFAULT true,
CONSTRAINT fk_payments_budget_id FOREIGN KEY (budget_id) REFERENCES budgets (id) MATCH SIMPLE ON UPDATE CASCADE ON DELETE RESTRICT
)`,
`CREATE TABLE IF NOT EXISTS transactions
(
......@@ -141,15 +145,13 @@ func InitDatabase() {
to_budget_id int,
amount int NOT NULL,
created_at timestamp NOT NULL,
pending bool DEFAULT true,
remote_purpose text,
remote_account text NOT NULL,
remote_bank_id text,
remote_name text NOT NULL,
purpose text,
payment_id int,
CONSTRAINT fk_transactions_budget_id FOREIGN KEY (budget_id) REFERENCES budgets (id) MATCH SIMPLE ON UPDATE CASCADE ON DELETE RESTRICT,
CONSTRAINT fk_transactions_from_budget_id FOREIGN KEY (from_budget_id) REFERENCES budgets (id) MATCH SIMPLE ON UPDATE CASCADE ON DELETE RESTRICT,
CONSTRAINT fk_transactions_to_budget_id FOREIGN KEY (to_budget_id) REFERENCES budgets (id) MATCH SIMPLE ON UPDATE CASCADE ON DELETE RESTRICT
)`,
CONSTRAINT fk_transactions_to_budget_id FOREIGN KEY (to_budget_id) REFERENCES budgets (id) MATCH SIMPLE ON UPDATE CASCADE ON DELETE RESTRICT,
CONSTRAINT fk_transactions_payment_id FOREIGN KEY (payment_id) REFERENCES payments (id) MATCH SIMPLE ON UPDATE CASCADE ON DELETE RESTRICT
)`,
`CREATE TABLE IF NOT EXISTS codes
(
id bigserial PRIMARY KEY,
......@@ -185,6 +187,8 @@ func InitDatabase() {
`CREATE INDEX idx_budgets_name ON budgets(name)`,
`CREATE INDEX idx_budgets_project_id ON budgets(project_id)`,
`CREATE INDEX idx_codes_code ON codes(code)`,
`CREATE INDEX idx_payments_budget_id ON payments(budget_id)`,
`CREATE INDEX idx_payments_created_at ON payments(created_at)`,
`CREATE INDEX idx_transactions_budget_id ON transactions(budget_id)`,
`CREATE INDEX idx_transactions_from_budget_id ON transactions(from_budget_id)`,
`CREATE INDEX idx_transactions_created_at ON transactions(created_at)`,
......
This diff is collapsed.
package db
import (
"fmt"
"time"
)
// Transaction represents the db schema of a transaction
type Transaction struct {
ID int64
BudgetID int64
FromBudgetID *int64
ToBudgetID *int64
Amount int64
CreatedAt time.Time
RemotePurpose string
RemoteAccount string
RemoteBankID string
RemoteName string
ID int64
BudgetID int64
FromBudgetID *int64
ToBudgetID *int64
Amount int64
CreatedAt time.Time
Purpose string
PaymentID *int64
}
const (
......@@ -32,70 +29,58 @@ func (context *APIContext) LoadTransactionByID(id int64) (Transaction, error) {
return transaction, ErrInvalidID
}
err := context.QueryRow("SELECT id, budget_id, from_budget_id, to_budget_id, amount, created_at, remote_purpose, remote_account, remote_bank_id, remote_name FROM transactions WHERE id = $1", id).
Scan(&transaction.ID, &transaction.BudgetID, &transaction.FromBudgetID, &transaction.ToBudgetID, &transaction.Amount, &transaction.CreatedAt, &transaction.RemotePurpose, &transaction.RemoteAccount, &transaction.RemoteBankID, &transaction.RemoteName)
err := context.QueryRow("SELECT id, budget_id, from_budget_id, to_budget_id, amount, created_at, purpose, payment_id "+
"FROM transactions "+
"WHERE id = $1", id).
Scan(&transaction.ID, &transaction.BudgetID, &transaction.FromBudgetID, &transaction.ToBudgetID, &transaction.Amount,
&transaction.CreatedAt, &transaction.Purpose, &transaction.PaymentID)
return transaction, err
}
// LoadTransactions loads all transactions for a project
func (project *Project) LoadTransactions(context *APIContext) ([]Transaction, error) {
transactions := []Transaction{}
budget, _ := context.LoadRootBudgetForProject(project)
rows, err := context.Query("SELECT id, budget_id, from_budget_id, to_budget_id, amount, created_at, remote_purpose, remote_account, remote_bank_id, remote_name FROM transactions WHERE budget_id = $1 ORDER BY created_at ASC", budget.ID)
b, err := context.LoadRootBudgetForProject(project)
if err != nil {
return transactions, err
return []Transaction{}, err
}
defer rows.Close()
for rows.Next() {
transaction := Transaction{}
err = rows.Scan(&transaction.ID, &transaction.BudgetID, &transaction.FromBudgetID, &transaction.ToBudgetID, &transaction.Amount, &transaction.CreatedAt, &transaction.RemotePurpose, &transaction.RemoteAccount, &transaction.RemoteBankID, &transaction.RemoteName)
if err != nil {
return transactions, err
}
return b.LoadTransactions(context)
/* transactions := []Transaction{}
transactions = append(transactions, transaction)
}
return transactions, err
}
// LoadTransactionsForDonor loads all transactions for a specific donor
func (context *APIContext) LoadTransactionsForDonor(donor string) ([]Transaction, error) {
transactions := []Transaction{}
rows, err := context.Query("SELECT id, budget_id, from_budget_id, to_budget_id, amount, created_at, remote_purpose, remote_account, remote_bank_id, remote_name FROM transactions WHERE remote_account = $1 ORDER BY created_at ASC", donor)
if err != nil {
return transactions, err
}
rows, err := context.Query("SELECT transactions.id, transactions.budget_id, transactions.from_budget_id, transactions.to_budget_id, transactions.amount, transactions.created_at, transactions.purpose, transactions.payment_id "+
"FROM transactions, budgets "+
"WHERE transactions.budget_id = budgets.id AND budgets.project_id = $1"+
"ORDER BY created_at, id ASC", project.ID)
defer rows.Close()
for rows.Next() {
transaction := Transaction{}
err = rows.Scan(&transaction.ID, &transaction.BudgetID, &transaction.FromBudgetID, &transaction.ToBudgetID, &transaction.Amount, &transaction.CreatedAt, &transaction.RemotePurpose, &transaction.RemoteAccount, &transaction.RemoteBankID, &transaction.RemoteName)
if err != nil {
return transactions, err
}
transactions = append(transactions, transaction)
}
defer rows.Close()
for rows.Next() {
transaction := Transaction{}
err = rows.Scan(&transaction.ID, &transaction.BudgetID, &transaction.FromBudgetID, &transaction.ToBudgetID, &transaction.Amount,
&transaction.CreatedAt, &transaction.Purpose, &transaction.PaymentID)
if err != nil {
return transactions, err
}
return transactions, err
transactions = append(transactions, transaction)
}
return transactions, err*/
}
// LoadPendingTransaction loads all pending transactions
func (context *APIContext) LoadPendingTransactions(direction int) ([]Transaction, error) {
// LoadTransactions loads all transactions for a budget
func (budget *Budget) LoadTransactions(context *APIContext) ([]Transaction, error) {
transactions := []Transaction{}
var filter string
switch direction {
case TRANSACTION_INCOMING:
filter = "AND amount > 0"
case TRANSACTION_OUTGOING:
filter = "AND amount < 0"
}
rows, err := context.Query(fmt.Sprintf("SELECT id, budget_id, from_budget_id, to_budget_id, amount, created_at, remote_purpose, remote_account, remote_bank_id, remote_name FROM transactions WHERE pending = true %s ORDER BY created_at ASC", filter))
rows, err := context.Query("SELECT id, budget_id, from_budget_id, to_budget_id, amount, created_at, purpose, payment_id "+
"FROM transactions "+
"WHERE budget_id = $1 "+
"ORDER BY created_at, id ASC", budget.ID)
if err != nil {
return transactions, err
}
......@@ -103,7 +88,8 @@ func (context *APIContext) LoadPendingTransactions(direction int) ([]Transaction
defer rows.Close()
for rows.Next() {
transaction := Transaction{}
err = rows.Scan(&transaction.ID, &transaction.BudgetID, &transaction.FromBudgetID, &transaction.ToBudgetID, &transaction.Amount, &transaction.CreatedAt, &transaction.RemotePurpose, &transaction.RemoteAccount, &transaction.RemoteBankID, &transaction.RemoteName)
err = rows.Scan(&transaction.ID, &transaction.BudgetID, &transaction.FromBudgetID, &transaction.ToBudgetID, &transaction.Amount,
&transaction.CreatedAt, &transaction.Purpose, &transaction.PaymentID)
if err != nil {
return transactions, err
}
......@@ -116,7 +102,41 @@ func (context *APIContext) LoadPendingTransactions(direction int) ([]Transaction
// Save a transaction to the database
func (transaction *Transaction) Save(context *APIContext) error {
err := context.QueryRow("INSERT INTO transactions (budget_id, from_budget_id, to_budget_id, amount, created_at, remote_purpose, remote_account, remote_bank_id, remote_name) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9) RETURNING id",
transaction.BudgetID, transaction.FromBudgetID, transaction.ToBudgetID, transaction.Amount, transaction.CreatedAt, transaction.RemotePurpose, transaction.RemoteAccount, transaction.RemoteBankID, transaction.RemoteName).Scan(&transaction.ID)
err := context.QueryRow("INSERT INTO transactions (budget_id, from_budget_id, to_budget_id, amount, created_at, purpose, payment_id) VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING id",
transaction.BudgetID, transaction.FromBudgetID, transaction.ToBudgetID, transaction.Amount, transaction.CreatedAt, transaction.Purpose, transaction.PaymentID).Scan(&transaction.ID)
return err
}
func (context *APIContext) Transfer(fromBudget, toBudget int64, amount int64, purpose string, paymentID int64, ts time.Time) (Transaction, error) {
if amount < 0 {
fromBudget, toBudget = toBudget, fromBudget
amount *= -1
}
// FIXME: move to transaction
torig := Transaction{
BudgetID: fromBudget,
ToBudgetID: &toBudget,
Amount: -amount,
CreatedAt: ts, // FIXME: time.Now().UTC(),
Purpose: purpose,
}
if paymentID > 0 {
torig.PaymentID = &paymentID
}
if err := torig.Save(context); err != nil {
return Transaction{}, err
}
t := Transaction{
BudgetID: toBudget,
FromBudgetID: &fromBudget,
Amount: amount,
CreatedAt: ts, // FIXME: time.Now().UTC(),
Purpose: purpose,
}
if paymentID > 0 {
t.PaymentID = &paymentID
}
return torig, t.Save(context)
}
......@@ -2,6 +2,7 @@ package budgets
import (
"gitlab.techcultivation.org/sangha/sangha/db"
"gitlab.techcultivation.org/sangha/sangha/resources/projects"
"github.com/muesli/smolder"
)
......@@ -10,14 +11,20 @@ import (
type BudgetResponse struct {
smolder.Response
Budgets []budgetInfoResponse `json:"budgets,omitempty"`
Budgets []BudgetInfoResponse `json:"budgets,omitempty"`
budgets []db.Budget
Projects []projects.ProjectInfoResponse `json:"projects,omitempty"`
projects []db.Project
}
type budgetInfoResponse struct {
ID string `json:"id"`
ProjectID string `json:"project_id"`
Name string `json:"name"`
type BudgetInfoResponse struct {
ID string `json:"id"`
Project string `json:"project"`
Name string `json:"name"`
Description string `json:"description"`
Balance int64 `json:"balance"`
Code string `json:"code"`
}
// Init a new response
......@@ -25,13 +32,22 @@ func (r *BudgetResponse) Init(context smolder.APIContext) {
r.Parent = r
r.Context = context
r.Budgets = []budgetInfoResponse{}
r.Budgets = []BudgetInfoResponse{}
r.Projects = []projects.ProjectInfoResponse{}
}
// AddBudget adds a budget to the response
func (r *BudgetResponse) AddBudget(budget *db.Budget) {
r.budgets = append(r.budgets, *budget)
r.Budgets = append(r.Budgets, prepareBudgetResponse(r.Context, budget))
r.Budgets = append(r.Budgets, PrepareBudgetResponse(r.Context, budget))
project, err := r.Context.(*db.APIContext).GetProjectByID(*budget.ProjectID)
if err != nil {
panic(err)
}
r.projects = append(r.projects, project)
r.Projects = append(r.Projects, projects.PrepareProjectResponse(r.Context, &project))
}
// EmptyResponse returns an empty API response for this endpoint if there's no data to respond with
......@@ -40,22 +56,30 @@ func (r *BudgetResponse) EmptyResponse() interface{} {
var out struct {
Budgets interface{} `json:"budgets"`
}
out.Budgets = []budgetInfoResponse{}
out.Budgets = []BudgetInfoResponse{}
return out
}
return nil
}
func prepareBudgetResponse(context smolder.APIContext, budget *db.Budget) budgetInfoResponse {
project, err := context.(*db.APIContext).GetProjectByID(*budget.ProjectID)
func PrepareBudgetResponse(context smolder.APIContext, budget *db.Budget) BudgetInfoResponse {
ctx := context.(*db.APIContext)
project, err := ctx.GetProjectByID(*budget.ProjectID)
if err != nil {
panic(err)
}
resp := budgetInfoResponse{
ID: budget.UUID,
ProjectID: project.UUID,
Name: budget.Name,
resp := BudgetInfoResponse{
ID: budget.UUID,
Project: project.UUID,
Name: budget.Name,
Description: budget.Description,
}
resp.Balance, _ = budget.Balance(ctx)
code, err := ctx.LoadCodeByBudgetUUID(budget.UUID)
if err == nil {
resp.Code = code.Code
}
return resp
......
......@@ -29,6 +29,7 @@ func (r *CodeResource) GetParams() []*restful.Parameter {
params = append(params, restful.QueryParameter("user_id", "ID of a user").DataType("string"))
params = append(params, restful.QueryParameter("budget_ids[]", "an array of budget IDs").DataType("string"))
params = append(params, restful.QueryParameter("ratios[]", "an array of ratios").DataType("int"))
params = append(params, restful.QueryParameter("search", "search for name or code of a budget").DataType("string"))
return params
}
......@@ -53,21 +54,25 @@ func (r *CodeResource) GetByIDs(context smolder.APIContext, request *restful.Req
// Get sends out items matching the query parameters
func (r *CodeResource) Get(context smolder.APIContext, request *restful.Request, response *restful.Response, params map[string][]string) {
ctx := context.(*db.APIContext)
resp := CodeResponse{}
resp.Init(context)
userID := params["user_id"]
budgetIDs := params["budget_ids[]"]
ratios := params["ratios[]"]
if len(budgetIDs) > 0 && len(ratios) > 0 {
fmt.Println(budgetIDs)
fmt.Println(ratios)
if len(params["search"]) > 0 {
codes, _ := ctx.SearchCodes(params["search"][0])
for _, code := range codes {
resp.AddCode(&code)
}
} else if len(budgetIDs) > 0 && len(ratios) > 0 {
var uid string
if len(userID) > 0 {
uid = userID[0]
}
code, err := context.(*db.APIContext).LoadCodeByBudgetsAndRatios(budgetIDs, ratios, uid)
code, err := ctx.LoadCodeByBudgetsAndRatios(budgetIDs, ratios, uid)
if err != nil {
r.NotFound(request, response)
return
......
package codes
import (
"strconv"
"gitlab.techcultivation.org/sangha/sangha/db"
"gitlab.techcultivation.org/sangha/sangha/resources/budgets"
"gitlab.techcultivation.org/sangha/sangha/resources/projects"
"github.com/muesli/smolder"
)
......@@ -10,13 +14,20 @@ import (
type CodeResponse struct {
smolder.Response
Codes []codeInfoResponse `json:"codes,omitempty"`
codes []db.Code
Codes []codeInfoResponse `json:"codes,omitempty"`
Budgets []budgets.BudgetInfoResponse `json:"budgets,omitempty"`
Projects []projects.ProjectInfoResponse `json:"projects,omitempty"`
codes []db.Code
budgets []db.Budget
projects []db.Project
}
type codeInfoResponse struct {
ID int64 `json:"id"`
Code string `json:"code"`
ID string `json:"id"`
Code string `json:"token"`
Budgets []string `json:"budgets"`
Ratios []string `json:"ratios"`
}
// Init a new response
......@@ -25,12 +36,38 @@ func (r *CodeResponse) Init(context smolder.APIContext) {
r.Context = context
r.Codes = []codeInfoResponse{}
r.Budgets = []budgets.BudgetInfoResponse{}
}
// AddCode adds a code to the response
func (r *CodeResponse) AddCode(code *db.Code) {
r.codes = append(r.codes, *code)
r.Codes = append(r.Codes, prepareCodeResponse(r.Context, code))
for _, b := range code.BudgetIDs {
bid, _ := strconv.ParseInt(b, 10, 64)
budget, err := r.Context.(*db.APIContext).LoadBudgetByID(bid)
if err != nil {
panic(err)
}
r.budgets = append(r.budgets, budget)
r.Budgets = append(r.Budgets, budgets.PrepareBudgetResponse(r.Context, &budget))
project, err := r.Context.(*db.APIContext).GetProjectByID(*budget.ProjectID)
if err != nil {
panic(err)
}
r.projects = append(r.projects, project)
r.Projects = append(r.Projects, projects.PrepareProjectResponse(r.Context, &project))
}
}
// AddBudget adds a budget to the response
func (r *CodeResponse) AddBudget(budget *db.Budget) {
r.budgets = append(r.budgets, *budget)
r.Budgets = append(r.Budgets, budgets.PrepareBudgetResponse(r.Context, budget))
}
// EmptyResponse returns an empty API response for this endpoint if there's no data to respond with
......@@ -46,9 +83,23 @@ func (r *CodeResponse) EmptyResponse() interface{} {
}
func prepareCodeResponse(context smolder.APIContext, code *db.Code) codeInfoResponse {
ctx := context.(*db.APIContext)
var budgets []string
for _, b := range code.BudgetIDs {
bid, _ := strconv.ParseInt(b, 10, 64)
budget, err := ctx.LoadBudgetByID(bid)
if err != nil {
panic(err)
}
budgets = append(budgets, budget.UUID)
}
resp := codeInfoResponse{
ID: code.ID,
Code: code.Code,
ID: code.Code,
Code: code.Code,
Budgets: budgets,
Ratios: code.Ratios,
}
return resp
......
......@@ -13,7 +13,9 @@ type PaymentResource struct {
}
var (
_ smolder.GetSupported = &PaymentResource{}
_ smolder.PostSupported = &PaymentResource{}
_ smolder.PutSupported = &PaymentResource{}
)
// Register this resource with the container to setup all the routes
......@@ -43,7 +45,7 @@ func (r *PaymentResource) Returns() interface{} {
func (r *PaymentResource) Validate(context smolder.APIContext, data interface{}, request *restful.Request) error {
ups := data.(*PaymentPostStruct)
if ups.Payment.Amount <= 0 {
if ups.Payment.Amount == 0 {
return errors.New("Invalid payment amount")
}
......
package payments
import (
"net/http"
"strconv"
"strings"
"gitlab.techcultivation.org/sangha/sangha/db"
"github.com/emicklei/go-restful"
"github.com/muesli/smolder"
)
// GetAuthRequired returns true because all requests need authentication
func (r *PaymentResource) GetAuthRequired() bool {
return true
}
// GetByIDsAuthRequired returns true because all requests need authentication
func (r *PaymentResource) GetByIDsAuthRequired() bool {
return true
}
// GetDoc returns the description of this API endpoint
func (r *PaymentResource) GetDoc() string {
return "retrieve payments"
}
// GetParams returns the parameters supported by this API endpoint
func (r *PaymentResource) GetParams() []*restful.Parameter {
params := []*restful.Parameter{}
params = append(params, restful.QueryParameter("limit", "returns at most n payments").DataType("int"))
params = append(params, restful.QueryParameter("direction", "returns only 'incoming' or 'outgoing' payments").DataType("string"))
params = append(params, restful.QueryParameter("donor", "returns payments for a specific donor only").DataType("string"))
return params
}
// GetByIDs sends out all items matching a set of IDs
func (r *PaymentResource) GetByIDs(context smolder.APIContext, request *restful.Request, response *restful.Response, ids []string) {
resp := PaymentResponse{}
resp.Init(context)
auth, err := context.Authentication(request)
if err != nil || auth == nil || auth.(db.User).ID != 1 {
smolder.ErrorResponseHandler(request, response, err, smolder.NewErrorResponse(
http.StatusUnauthorized,
"Admin permission required for this operation",
"PaymentResource GET"))
return
}
for _, id := range ids {
iid, _ := strconv.ParseInt(id, 10, 0)
payment, err := context.(*db.APIContext).LoadPaymentByID(iid)
if err != nil {
r.NotFound(request, response)
return
}
resp.AddPayment(payment)
}
resp.Send(response)
}
// Get sends out items matching the query parameters
func (r *PaymentResource) Get(context smolder.APIContext, request *restful.Request, response *restful.Response, params map[string][]string) {
ctx := context.(*db.APIContext)
resp := PaymentResponse{}
resp.Init(context)
auth, err := context.Authentication(request)
if err != nil || auth == nil || auth.(db.User).ID != 1 {
smolder.ErrorResponseHandler(request, response, err, smolder.NewErrorResponse(
http.StatusUnauthorized,
"Admin permission required for this operation",
"PaymentResource GET"))
return
}
var payments []db.Payment
if len(params["budget"]) > 0 {
var budget db.Budget
budget, err = ctx.LoadBudgetByUUID(params["budget"][0])
if err != nil {
r.NotFound(request, response)
return
}
payments, err = budget.LoadPayments(ctx)
if err != nil {
smolder.ErrorResponseHandler(request, response, err, smolder.NewErrorResponse(
http.StatusInternalServerError,
"Can't load payments",
"PaymentsResource GET"))
return
}
} else if len(params["donor"]) > 0 {
payments, err = ctx.LoadPaymentsForDonor(params["donor"][0])
if err != nil {
smolder.ErrorResponseHandler(request, response, err, smolder.NewErrorResponse(
http.StatusInternalServerError,
"Can't load payments",
"PaymentsResource GET"))
return
}
} else {
direction := db.TRANSACTION_ALL
if len(params["direction"]) > 0 {
if strings.ToLower(params["direction"][0]) == "incoming" {
direction = db.TRANSACTION_INCOMING
}
if strings.ToLower(params["direction"][0]) == "outgoing" {
direction = db.TRANSACTION_OUTGOING
}
}
payments, err = ctx.LoadPendingPayments(direction)
if err != nil {
smolder.ErrorResponseHandler(request, response, err, smolder.NewErrorResponse(
http.StatusInternalServerError,
"Can't load payments",
"PaymentsResource GET"))
return
}
}
n := 0
if len(params["limit"]) > 0 {
limit, _ := strconv.ParseInt(params["limit"][0], 10, 0)
n = len(payments) - int(limit)
if n < 0 {
n = 0
}
}
for _, payment := range payments[n:] {
resp.AddPayment(payment)
}
resp.Send(response)
}
......@@ -17,10 +17,11 @@ import (
// PaymentPostStruct holds all values of an incoming POST request
type PaymentPostStruct struct {
Payment struct {
Source string `json:"source"`
SourceID string `json:"source_id"`
Amount int64 `json:"amount"`
TransactionCode string `json:"transaction_code"`
Source string `json:"source"`
SourceID string `json:"source_id"`
Amount int64 `json:"amount"`
Code string `json:"code"`
Pending bool `json:"pending"`
} `json:"payment"`
}
......@@ -64,7 +65,7 @@ func (r *PaymentResource) Post(context smolder.APIContext, data interface{}, req
switch ups.Payment.Source {
case "bank_transfer":
payment.Amount = ups.Payment.Amount
payment.Code = ups.Payment.TransactionCode
payment.Code = ups.Payment.Code
payment.Source = ups.Payment.Source
case "paypal":
......@@ -93,15 +94,15 @@ func (r *PaymentResource) Post(context smolder.APIContext, data interface{}, req
return
}
fmt.Printf("Payment: %+v\n", payments.Payments[0])
payment.UserID = payments.Payments[0].UserID
// payment.RemoteAccount = payments.Payments[0].UserID
payment.Amount = payments.Payments[0].Amount
payment.Currency = payments.Payments[0].Currency
payment.Code = payments.Payments[0].Code
payment.Description = payments.Payments[0].Description
payment.Purpose = payments.Payments[0].Description
payment.Source = payments.Payments[0].Source
payment.SourceID = payments.Payments[0].SourceID
payment.SourcePayerID = payments.Payments[0].SourcePayerID
payment.SourceTransactionID = payments.Payments[0].SourceTransactionID
payment.RemoteBankID = payments.Payments[0].SourceID
payment.RemoteAccount = payments.Payments[0].SourcePayerID
payment.RemoteTransactionID = payments.Payments[0].SourceTransactionID
payment.CreatedAt = payments.Payments[0].CreatedAt
case "stripe":
......@@ -130,15 +131,15 @@ func (r *PaymentResource) Post(context smolder.APIContext, data interface{}, req
return
}
fmt.Printf("Payment: %+v\n", payments.Payments[0])
payment.UserID = payments.Payments[0].UserID
// payment.RemoteAccount = payments.Payments[0].UserID
payment.Amount = payments.Payments[0].Amount
payment.Currency = payments.Payments[0].Currency
payment.Code = payments.Payments[0].Code
payment.Description = payments.Payments[0].Description
payment.Purpose = payments.Payments[0].Description
payment.Source = payments.Payments[0].Source
payment.SourceID = payments.Payments[0].SourceID
payment.SourcePayerID = payments.Payments[0].SourcePayerID
payment.SourceTransactionID = payments.Payments[0].SourceTransactionID
payment.RemoteBankID = payments.Payments[0].SourceID
payment.RemoteAccount = payments.Payments[0].SourcePayerID
payment.RemoteTransactionID = payments.Payments[0].SourceTransactionID
payment.CreatedAt = payments.Payments[0].CreatedAt
default:
......@@ -151,6 +152,6 @@ func (r *PaymentResource) Post(context smolder.APIContext, data interface{}, req
resp := PaymentResponse{}
resp.Init(context)
resp.AddPayment(&payment)
resp.AddPayment(payment)
resp.Send(response)
}
package payments
import (
"net/http"
"strconv"
"gitlab.techcultivation.org/sangha/sangha/db"
"github.com/emicklei/go-restful"
"github.com/muesli/smolder"
)
// PaymentPutStruct holds all values of an incoming PUT request
type PaymentPutStruct struct {
PaymentPostStruct
}
// PutAuthRequired returns true because all requests need authentication
func (r *PaymentResource) PutAuthRequired() bool {
return false
}
// PutDoc returns the description of this API endpoint
func (r *PaymentResource) PutDoc() string {
return "update an existing payment"
}
// PutParams returns the parameters supported by this API endpoint
func (r *PaymentResource) PutParams() []*restful.Parameter {
return nil
}
// Put processes an incoming PUT (update) request
func (r *PaymentResource) Put(context smolder.APIContext, data interface{}, request *restful.Request, response *restful.Response) {
ctx := context.(*db.APIContext)
resp := PaymentResponse{}
resp.Init(context)
iid, _ := strconv.ParseInt(request.PathParameter("payment-id"), 10, 0)
payment, err := ctx.LoadPaymentByID(iid)
if err != nil {
r.NotFound(request, response)
return
}
/* auth, err := context.Authentication(request)
if err != nil || (auth.(db.User).ID != 1 && auth.(db.User).ID != project.UserID) {
smolder.ErrorResponseHandler(request, response, smolder.NewErrorResponse(
http.StatusUnauthorized,
false,
"Admin permission required for this operation",
"ProjectResource PUT"))
return
} */
pps := data.(*PaymentPostStruct)
payment.Code = pps.Payment.Code
payment.Pending = pps.Payment.Pending
err = payment.Update(ctx)
if err != nil {
smolder.ErrorResponseHandler(request, response, err, smolder.NewErrorResponse(
http.StatusInternalServerError,
"Can't update payment",
"PaymentResource PUT"))
return
}
resp.AddPayment(payment)
resp.Send(response)
}
package payments
import (
"strconv"
"time"
"gitlab.techcultivation.org/sangha/sangha/db"
......@@ -18,16 +19,18 @@ type PaymentResponse struct {
type paymentInfoResponse struct {
ID int64 `json:"id"`
UserID int64 `json:"user_id"`
BudgetID string `json:"budget_id"`
CreatedAt time.Time `json:"created_at"`
Amount int64 `json:"amount"`
Currency string `json:"currency"`
Code string `json:"code"`
Description string `json:"description"`
Code *string `json:"code"`
Purpose string `json:"purpose"`
RemoteAccount string `json:"remote_account"`
RemoteBankID string `json:"remote_bank_id"`
RemoteTransactionID string `json:"remote_transaction_id"`
RemoteName string `json:"remote_name"`
Source string `json:"source"`
SourceID string `json:"source_id"`
SourcePayerID string `json:"source_payer_id"`
SourceTransactionID string `json:"source_transaction_id"`
CreatedAt time.Time `json:"created_at"`
Pending bool `json:"pending"`
}
// Init a new response
......@@ -39,8 +42,8 @@ func (r *PaymentResponse) Init(context smolder.APIContext) {
}
// AddPayment adds a payment to the response
func (r *PaymentResponse) AddPayment(payment *db.Payment) {
r.payments = append(r.payments, *payment)
func (r *PaymentResponse) AddPayment(payment db.Payment) {
r.payments = append(r.payments, payment)
r.Payments = append(r.Payments, preparePaymentResponse(r.Context, payment))
}
......@@ -56,18 +59,37 @@ func (r *PaymentResponse) EmptyResponse() interface{} {
return nil
}
func preparePaymentResponse(context smolder.APIContext, payment *db.Payment) paymentInfoResponse {
func preparePaymentResponse(context smolder.APIContext, payment db.Payment) paymentInfoResponse {
resp := paymentInfoResponse{
ID: payment.ID,
UserID: payment.UserID,
CreatedAt: payment.CreatedAt,
Amount: payment.Amount,
Currency: payment.Currency,
Description: payment.Description,
Purpose: payment.Purpose,
RemoteAccount: payment.RemoteAccount,
RemoteBankID: payment.RemoteBankID,
RemoteTransactionID: payment.RemoteTransactionID,
RemoteName: payment.RemoteName,
Source: payment.Source,
SourceID: payment.SourceID,
SourcePayerID: payment.SourcePayerID,
SourceTransactionID: payment.SourceTransactionID,
CreatedAt: payment.CreatedAt,
Pending: payment.Pending,
}
if payment.Code != "" {
resp.Code = &payment.Code
}
c, err := context.(*db.APIContext).LoadCodeByCode(payment.Code)
if err == nil {
bid, err := strconv.ParseInt(c.BudgetIDs[0], 10, 64)
if err != nil {
panic(err)
}
b, err := context.(*db.APIContext).LoadBudgetByID(bid)
if err != nil {
panic(err)
}
resp.BudgetID = b.UUID
}
return resp
......
......@@ -50,7 +50,7 @@ func (r *ProjectResource) Put(context smolder.APIContext, data interface{}, requ
return
} */
pps := data.(*ProjectPutStruct)
pps := data.(*ProjectPostStruct)
project.Name = pps.Project.Name
project.Summary = pps.Project.Summary
project.About = pps.Project.About
......
......@@ -10,7 +10,7 @@ import (
type ProjectResponse struct {
smolder.Response
Projects []projectInfoResponse `json:"projects,omitempty"`
Projects []ProjectInfoResponse `json:"projects,omitempty"`
projects []db.Project
}
......@@ -19,20 +19,21 @@ type contributorResponse struct {
Avatar string `json:"avatar"`
}
type projectInfoResponse struct {
ID string `json:"id"`
Slug string `json:"slug"`
Name string `json:"name"`
Summary string `json:"summary"`
About string `json:"about"`
Website string `json:"website"`
License string `json:"license"`
Repository string `json:"repository"`
Logo string `json:"logo"`
RootBudget string `json:"budget_root"`
Balance int64 `json:"balance"`
Contributors []contributorResponse `json:"contributors,omitempty"`
Activated bool `json:"activated"`
type ProjectInfoResponse struct {
ID string `json:"id"`
Slug string `json:"slug"`
Name string `json:"name"`
Summary string `json:"summary"`
About string `json:"about"`
Website string `json:"website"`
License string `json:"license"`
Repository string `json:"repository"`
Logo string `json:"logo"`
RootBudget string `json:"budget_root"`
Balance int64 `json:"balance"`
ProcessingCut int64 `json:"processing_cut"`
Contributors []contributorResponse `json:"contributors,omitempty"`
Activated bool `json:"activated"`
}
// Init a new response
......@@ -40,13 +41,13 @@ func (r *ProjectResponse) Init(context smolder.APIContext) {
r.Parent = r
r.Context = context
r.Projects = []projectInfoResponse{}
r.Projects = []ProjectInfoResponse{}
}
// AddProject adds a project to the response
func (r *ProjectResponse) AddProject(project *db.Project) {
r.projects = append(r.projects, *project)
r.Projects = append(r.Projects, prepareProjectResponse(r.Context, project))
r.Projects = append(r.Projects, PrepareProjectResponse(r.Context, project))
}
// EmptyResponse returns an empty API response for this endpoint if there's no data to respond with
......@@ -55,30 +56,31 @@ func (r *ProjectResponse) EmptyResponse() interface{} {
var out struct {
Projects interface{} `json:"projects"`
}
out.Projects = []projectInfoResponse{}
out.Projects = []ProjectInfoResponse{}
return out
}
return nil
}
func prepareProjectResponse(context smolder.APIContext, project *db.Project) projectInfoResponse {
func PrepareProjectResponse(context smolder.APIContext, project *db.Project) ProjectInfoResponse {
ctx := context.(*db.APIContext)
resp := projectInfoResponse{
ID: project.UUID,
Slug: project.Slug,
Name: project.Name,
Summary: project.Summary,
About: project.About,
Website: project.Website,
License: project.License,
Repository: project.Repository,
Logo: ctx.BuildImageURL(project.Logo, project.Name),
Activated: project.Activated,
resp := ProjectInfoResponse{
ID: project.UUID,
Slug: project.Slug,
Name: project.Name,
Summary: project.Summary,
About: project.About,
Website: project.Website,
License: project.License,
Repository: project.Repository,
Logo: ctx.BuildImageURL(project.Logo, project.Name),
ProcessingCut: project.ProcessingCut,
Activated: project.Activated,
}
budget, _ := ctx.LoadRootBudgetForProject(project)
resp.RootBudget = budget.UUID
resp.Balance, _ = budget.Balance(ctx)
resp.Balance, _ = project.Balance(ctx)
contributors, _ := project.Contributors(ctx)
for _, contributor := range contributors {
......
package searches
import (
"github.com/emicklei/go-restful"
"github.com/muesli/smolder"
)
// SearchesResource is the resource responsible for /searches
type SearchesResource struct {
smolder.Resource
}
var (
_ smolder.GetIDSupported = &SearchesResource{}
)
// Register this resource with the container to setup all the routes
func (r *SearchesResource) Register(container *restful.Container, config smolder.APIConfig, context smolder.APIContextFactory) {
r.Name = "SearchesResource"
r.TypeName = "searches"
r.Endpoint = "searches"
r.Doc = "Manage searches"
r.Config = config
r.Context = context
r.Init(container, r)
}
// Returns returns the model that will be returned
func (r *SearchesResource) Returns() interface{} {
return SearchResponse{}
}
package searches
import (
"gitlab.techcultivation.org/sangha/sangha/db"
"github.com/emicklei/go-restful"
"github.com/muesli/smolder"
)
// GetAuthRequired returns true because all requests need authentication
func (r *SearchesResource) GetAuthRequired() bool {
return false
}
// GetByIDsAuthRequired returns true because all requests need authentication
func (r *SearchesResource) GetByIDsAuthRequired() bool {
return false
}
// GetDoc returns the description of this API endpoint
func (r *SearchesResource) GetDoc() string {
return "retrieve searches"
}
// GetParams returns the parameters supported by this API endpoint
func (r *SearchesResource) GetParams() []*restful.Parameter {
params := []*restful.Parameter{}
return params
}
// GetByIDs sends out all items matching a set of IDs
func (r *SearchesResource) GetByIDs(context smolder.APIContext, request *restful.Request, response *restful.Response, ids []string) {
resp := SearchResponse{}
resp.Init(context)
if len(ids) > 0 {
search, err := context.(*db.APIContext).Search(ids[0])
if err != nil {
r.NotFound(request, response)
return
}
resp.AddSearch(&search)
}
resp.Send(response)
}
// Get sends out items matching the query parameters
func (r *SearchesResource) Get(context smolder.APIContext, request *restful.Request, response *restful.Response, params map[string][]string) {
resp := SearchResponse{}
resp.Init(context)
terms := params["term"]
if len(terms) > 0 {
search, err := context.(*db.APIContext).Search(terms[0])
if err != nil {
r.NotFound(request, response)
return
}
resp.AddSearch(&search)
} else {
r.NotFound(request, response)
return
}
resp.Send(response)
}
package searches
import (
"gitlab.techcultivation.org/sangha/sangha/db"
"github.com/muesli/smolder"
)
// SearchResponse is the common response to 'searches' requests
type SearchResponse struct {
smolder.Response
Searches []searchInfoResponse `json:"searches,omitempty"`
searches []db.Search
}
type searchInfoResponse struct {
ID string `json:"id"`
Projects []string `json:"projects"`
Budgets []string `json:"budgets"`
Payments []int64 `json:"payments"`
}
// Init a new response
func (r *SearchResponse) Init(context smolder.APIContext) {
r.Parent = r
r.Context = context
r.Searches = []searchInfoResponse{}
}
// AddSearches adds a search to the response
func (r *SearchResponse) AddSearch(search *db.Search) {
r.searches = append(r.searches, *search)
r.Searches = append(r.Searches, prepareSearchResponse(r.Context, search))
}
// EmptyResponse returns an empty API response for this endpoint if there's no data to respond with
func (r *SearchResponse) EmptyResponse() interface{} {
if len(r.searches) == 0 {
var out struct {
Searches interface{} `json:"searches"`
}
out.Searches = []searchInfoResponse{}
return out
}
return nil
}
func prepareSearchResponse(context smolder.APIContext, search *db.Search) searchInfoResponse {
resp := searchInfoResponse{
ID: search.ID,
}
for _, p := range search.Projects {
resp.Projects = append(resp.Projects, p.UUID)
}
for _, b := range search.Budgets {
resp.Budgets = append(resp.Budgets, b.UUID)
}
for _, p := range search.Payments {
resp.Payments = append(resp.Payments, p.ID)
}
return resp
}
package statistics
import (
"github.com/emicklei/go-restful"
"github.com/muesli/smolder"
)
// StatisticsResource is the resource responsible for /statistics
type StatisticsResource struct {
smolder.Resource
}
var (
_ smolder.GetSupported = &StatisticsResource{}
)
// Register this resource with the container to setup all the routes
func (r *StatisticsResource) Register(container *restful.Container, config smolder.APIConfig, context smolder.APIContextFactory) {
r.Name = "StatisticsResource"
r.TypeName = "statistics"
r.Endpoint = "statistics"
r.Doc = "Manage statistics"
r.Config = config
r.Context = context
r.Init(container, r)
}
// Returns returns the model that will be returned
func (r *StatisticsResource) Returns() interface{} {
return StatisticsResponse{}
}
package statistics
import (
"gitlab.techcultivation.org/sangha/sangha/db"
"github.com/emicklei/go-restful"
"github.com/muesli/smolder"
)
// GetAuthRequired returns true because all requests need authentication
func (r *StatisticsResource) GetAuthRequired() bool {
return false
}
// GetByIDsAuthRequired returns true because all requests need authentication
func (r *StatisticsResource) GetByIDsAuthRequired() bool {
return false
}
// GetDoc returns the description of this API endpoint
func (r *StatisticsResource) GetDoc() string {
return "retrieve statistics"
}
// GetParams returns the parameters supported by this API endpoint
func (r *StatisticsResource) GetParams() []*restful.Parameter {
params := []*restful.Parameter{}
params = append(params, restful.QueryParameter("project", "an ID of a project").DataType("string"))
params = append(params, restful.QueryParameter("budget", "an ID of a budget").DataType("string"))
return params
}
// Get sends out items matching the query parameters
func (r *StatisticsResource) Get(context smolder.APIContext, request *restful.Request, response *restful.Response, params map[string][]string) {
resp := StatisticsResponse{}
resp.Init(context)
projectID := params["project"]
if len(projectID) > 0 {
b, err := context.(*db.APIContext).LoadProjectByUUID(projectID[0])
if err != nil {
r.NotFound(request, response)
return
}
statistics, err := context.(*db.APIContext).LoadStatistics(b.ID)
if err != nil {
r.NotFound(request, response)
return
}
resp.AddStatistics(&statistics)
} else {
r.NotFound(request, response)
return
}
resp.Send(response)
}
package statistics
import (
"gitlab.techcultivation.org/sangha/sangha/db"
"github.com/muesli/smolder"
)
// StatisticsResponse is the common response to 'statistics' requests
type StatisticsResponse struct {
smolder.Response
Statistics []statisticsInfoResponse `json:"statistics,omitempty"`
statistics []db.Statistics
}
type statisticsInfoResponse struct {
ID string `json:"id"`
ProjectID int64 `json:"project_id"`
BudgetID int64 `json:"budget_id"`
MonthlyChange int64 `json:"monthly_change"`
PastMonths []int64 `json:"past_months"`
}
// Init a new response
func (r *StatisticsResponse) Init(context smolder.APIContext) {
r.Parent = r
r.Context = context
r.Statistics = []statisticsInfoResponse{}
}
// AddStatistics adds a statistics to the response
func (r *StatisticsResponse) AddStatistics(statistics *db.Statistics) {
r.statistics = append(r.statistics, *statistics)
r.Statistics = append(r.Statistics, prepareStatisticsResponse(r.Context, statistics))
}
// EmptyResponse returns an empty API response for this endpoint if there's no data to respond with
func (r *StatisticsResponse) EmptyResponse() interface{} {
if len(r.statistics) == 0 {
var out struct {
Statistics interface{} `json:"statistics"`
}
out.Statistics = []statisticsInfoResponse{}
return out
}
return nil
}
func prepareStatisticsResponse(context smolder.APIContext, statistics *db.Statistics) statisticsInfoResponse {
resp := statisticsInfoResponse{
ID: statistics.ID,
ProjectID: statistics.ProjectID,
MonthlyChange: statistics.MonthlyChange,
PastMonths: statistics.PastMonths,
}
return resp
}
package transactions
import (
"errors"
"github.com/emicklei/go-restful"
"github.com/muesli/smolder"
)
......@@ -15,6 +13,7 @@ type TransactionResource struct {
var (
_ smolder.GetSupported = &TransactionResource{}
_ smolder.PostSupported = &TransactionResource{}
_ smolder.PutSupported = &TransactionResource{}
)
// Register this resource with the container to setup all the routes
......@@ -44,8 +43,8 @@ func (r *TransactionResource) Returns() interface{} {
func (r *TransactionResource) Validate(context smolder.APIContext, data interface{}, request *restful.Request) error {
ups := data.(*TransactionPostStruct)
if ups.Transaction.Amount <= 0 {
return errors.New("Invalid transaction amount")
if ups.Transaction.Amount == 0 {
// return errors.New("Invalid transaction amount")
}
return nil
......
package transactions
import (
"net/http"
"strconv"
"strings"
"time"
"gitlab.techcultivation.org/sangha/sangha/db"
......@@ -28,10 +30,15 @@ func (r *TransactionResource) GetDoc() string {
// GetParams returns the parameters supported by this API endpoint
func (r *TransactionResource) GetParams() []*restful.Parameter {
params := []*restful.Parameter{}
params = append(params, restful.QueryParameter("limit", "returns at most n transactions").DataType("int"))
params = append(params, restful.QueryParameter("pending", "returns only pending transactions").DataType("bool"))
params = append(params, restful.QueryParameter("direction", "returns only 'incoming' or 'outgoing' transactions").DataType("string"))
params = append(params, restful.QueryParameter("project", "returns transactions for a specific project only").DataType("string"))
params = append(params, restful.QueryParameter("budget", "returns transactions for a specific budget only").DataType("string"))
params = append(params, restful.