...
 
Commits (4)
......@@ -99,9 +99,9 @@ func (budget *Budget) Save(context *APIContext) error {
}
// Balance returns this budget's total balance
func (budget *Budget) Balance(context *APIContext) (float64, error) {
var val float64
err := context.QueryRow("SELECT SUM(value) FROM transactions WHERE budget_id = $1", budget.ID).
func (budget *Budget) Balance(context *APIContext) (int64, error) {
var val int64
err := context.QueryRow("SELECT SUM(amount) FROM transactions WHERE budget_id = $1", budget.ID).
Scan(&val)
return val, err
}
......
......@@ -132,6 +132,11 @@ 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,
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
......
package db
import "time"
import (
"fmt"
"time"
)
// Transaction represents the db schema of a transaction
type Transaction struct {
ID int64
BudgetID int64
FromBudgetID *int64
ToBudgetID *int64
Amount float64
CreatedAt time.Time
ID int64
BudgetID int64
FromBudgetID *int64
ToBudgetID *int64
Amount int64
CreatedAt time.Time
RemotePurpose string
RemoteAccount string
RemoteBankID string
RemoteName string
}
var ()
const (
TRANSACTION_ALL = iota
TRANSACTION_INCOMING
TRANSACTION_OUTGOING
)
// LoadTransactionByID loads a transaction by ID from the database
func (context *APIContext) LoadTransactionByID(id int64) (Transaction, error) {
......@@ -21,14 +32,91 @@ 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 FROM transactions WHERE id = $1", id).
Scan(&transaction.ID, &transaction.BudgetID, &transaction.FromBudgetID, &transaction.ToBudgetID, &transaction.Amount, &transaction.CreatedAt)
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)
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)
if err != nil {
return transactions, 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
}
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
}
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)
}
return transactions, err
}
// LoadPendingTransaction loads all pending transactions
func (context *APIContext) LoadPendingTransactions(direction int) ([]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))
if err != nil {
return transactions, 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
}
transactions = append(transactions, transaction)
}
return transactions, err
}
// 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) VALUES ($1, $2, $3, $4, $5) RETURNING id",
transaction.BudgetID, transaction.FromBudgetID, transaction.ToBudgetID, transaction.Amount, time.Now()).Scan(&transaction.ID)
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)
return err
}
......@@ -66,6 +66,7 @@ func (r *PaymentResource) Post(context smolder.APIContext, data interface{}, req
payment.Amount = ups.Payment.Amount
payment.Code = ups.Payment.TransactionCode
payment.Source = ups.Payment.Source
case "paypal":
resp, err := http.Get(ctx.Config.Connections.PayPal + "/" + ups.Payment.SourceID)
if err != nil {
......@@ -104,6 +105,46 @@ func (r *PaymentResource) Post(context smolder.APIContext, data interface{}, req
payment.SourcePayerID = payments.Payments[0].SourcePayerID
payment.SourceTransactionID = payments.Payments[0].SourceTransactionID
payment.CreatedAt = payments.Payments[0].CreatedAt
case "stripe":
resp, err := http.Get(ctx.Config.Connections.Stripe + "/" + ups.Payment.SourceID)
if err != nil {
panic(err)
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
fmt.Println(string(body))
if resp.StatusCode != http.StatusOK {
smolder.ErrorResponseHandler(request, response, smolder.NewErrorResponse(
http.StatusBadRequest,
false,
"Unknown payment ID",
"PaymentResource POST"))
return
}
err = json.Unmarshal(body, &payments)
if err != nil {
smolder.ErrorResponseHandler(request, response, smolder.NewErrorResponse(
http.StatusInternalServerError,
true,
"Error decoding payment response",
"PaymentResource POST"))
return
}
fmt.Printf("Payment: %+v\n", payments.Payments[0])
payment.UserID = 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.Source = payments.Payments[0].Source
payment.SourceID = payments.Payments[0].SourceID
payment.SourcePayerID = payments.Payments[0].SourcePayerID
payment.SourceTransactionID = payments.Payments[0].SourceTransactionID
payment.CreatedAt = payments.Payments[0].CreatedAt
default:
smolder.ErrorResponseHandler(request, response, smolder.NewErrorResponse(
http.StatusBadRequest,
......
......@@ -30,6 +30,7 @@ type projectInfoResponse struct {
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"`
}
......@@ -77,6 +78,7 @@ func prepareProjectResponse(context smolder.APIContext, project *db.Project) pro
budget, _ := ctx.LoadRootBudgetForProject(project)
resp.RootBudget = budget.UUID
resp.Balance, _ = budget.Balance(ctx)
contributors, _ := project.Contributors(ctx)
for _, contributor := range contributors {
......
......@@ -13,6 +13,7 @@ type TransactionResource struct {
}
var (
_ smolder.GetSupported = &TransactionResource{}
_ smolder.PostSupported = &TransactionResource{}
)
......
package transactions
import (
"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 *TransactionResource) GetAuthRequired() bool {
return false
}
// GetByIDsAuthRequired returns true because all requests need authentication
func (r *TransactionResource) GetByIDsAuthRequired() bool {
return false
}
// GetDoc returns the description of this API endpoint
func (r *TransactionResource) GetDoc() string {
return "retrieve transactions"
}
// GetParams returns the parameters supported by this API endpoint
func (r *TransactionResource) GetParams() []*restful.Parameter {
params := []*restful.Parameter{}
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("donor", "returns transactions for a specific donor only").DataType("string"))
return params
}
// GetByIDs sends out all items matching a set of IDs
func (r *TransactionResource) GetByIDs(context smolder.APIContext, request *restful.Request, response *restful.Response, ids []string) {
resp := TransactionResponse{}
resp.Init(context)
for _, id := range ids {
iid, _ := strconv.ParseInt(id, 10, 0)
transaction, err := context.(*db.APIContext).LoadTransactionByID(iid)
if err != nil {
r.NotFound(request, response)
return
}
resp.AddTransaction(&transaction)
}
resp.Send(response)
}
// Get sends out items matching the query parameters
func (r *TransactionResource) Get(context smolder.APIContext, request *restful.Request, response *restful.Response, params map[string][]string) {
resp := TransactionResponse{}
resp.Init(context)
var err error
var project db.Project
var transactions []db.Transaction
if len(params["project"]) > 0 {
project, err = context.(*db.APIContext).LoadProjectByUUID(params["project"][0])
if err != nil {
r.NotFound(request, response)
return
}
transactions, err = project.LoadTransactions(context.(*db.APIContext))
if err != nil {
r.NotFound(request, response)
return
}
} else if len(params["donor"]) > 0 {
transactions, err = context.(*db.APIContext).LoadTransactionsForDonor(params["donor"][0])
if err != nil {
r.NotFound(request, response)
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
}
}
transactions, err = context.(*db.APIContext).LoadPendingTransactions(direction)
if err != nil {
r.NotFound(request, response)
return
}
}
for _, transaction := range transactions {
resp.AddTransaction(&transaction)
}
resp.Send(response)
}
......@@ -12,10 +12,10 @@ import (
// TransactionPostStruct holds all values of an incoming POST request
type TransactionPostStruct struct {
Transaction struct {
Source string `json:"type"`
SourceID string `json:"source_id"`
Amount float64 `json:"amount"`
TransactionCode string `json:"transaction_code"`
Source string `json:"type"`
SourceID string `json:"source_id"`
Amount int64 `json:"amount"`
TransactionCode string `json:"transaction_code"`
} `json:"transaction"`
}
......
package transactions
import (
"time"
"gitlab.techcultivation.org/sangha/sangha/db"
"github.com/muesli/smolder"
......@@ -15,9 +17,14 @@ type TransactionResponse struct {
}
type transactionInfoResponse struct {
ID int64 `json:"id"`
BudgetID int64 `json:"budget_id"`
Amount float64 `json:"amount"`
ID int64 `json:"id"`
BudgetID int64 `json:"budget_id"`
Amount int64 `json:"amount"`
CreatedAt time.Time `json:"created_at"`
RemotePurpose string `json:"remote_purpose"`
RemoteAccount string `json:"remote_account"`
RemoteBankID string `json:"remote_bank_id"`
RemoteName string `json:"remote_name"`
}
// Init a new response
......@@ -48,9 +55,14 @@ func (r *TransactionResponse) EmptyResponse() interface{} {
func prepareTransactionResponse(context smolder.APIContext, transaction *db.Transaction) transactionInfoResponse {
resp := transactionInfoResponse{
ID: transaction.ID,
BudgetID: transaction.BudgetID,
Amount: transaction.Amount,
ID: transaction.ID,
BudgetID: transaction.BudgetID,
Amount: transaction.Amount,
CreatedAt: transaction.CreatedAt,
RemotePurpose: transaction.RemotePurpose,
RemoteAccount: transaction.RemoteAccount,
RemoteBankID: transaction.RemoteBankID,
RemoteName: transaction.RemoteName,
}
return resp
......