From 28b7c2203ae72cb42d4a97440812b62c7118c583 Mon Sep 17 00:00:00 2001
From: meskio <meskio@sindominio.net>
Date: Tue, 29 Sep 2020 13:31:52 +0200
Subject: [PATCH] Add topup transactions

---
 api/api.go           |  3 ++-
 api/auth.go          | 22 +++++++++++++++
 api/purchase.go      | 25 ++---------------
 api/purchase_test.go |  2 +-
 api/topup.go         | 64 ++++++++++++++++++++++++++++++++++++++++++++
 api/topup_test.go    | 50 ++++++++++++++++++++++++++++++++++
 api/transaction.go   | 22 +++++++++++++++
 src/Head.js          | 11 +++++---
 8 files changed, 170 insertions(+), 29 deletions(-)
 create mode 100644 api/topup.go
 create mode 100644 api/topup_test.go

diff --git a/api/api.go b/api/api.go
index 79f2ffe..b5f3287 100644
--- a/api/api.go
+++ b/api/api.go
@@ -19,7 +19,7 @@ func initDB(dbPath string) (*gorm.DB, error) {
 		return nil, err
 	}
 
-	db.AutoMigrate(&Member{}, &Product{}, &Purchase{}, &Transaction{})
+	db.AutoMigrate(&Member{}, &Product{}, &Purchase{}, &Topup{}, &Transaction{})
 	return db, err
 }
 
@@ -56,5 +56,6 @@ func Init(dbPath string, signKey string, r *mux.Router) error {
 	r.HandleFunc("/transaction/mine", a.authNum(a.getTransactionsByMember)).Methods("GET")
 
 	r.HandleFunc("/purchase", a.authNum(a.AddPurchase)).Methods("POST")
+	r.HandleFunc("/topup", a.authAdminNum(a.AddTopup)).Methods("POST")
 	return nil
 }
diff --git a/api/auth.go b/api/auth.go
index 66930ee..a31bea5 100644
--- a/api/auth.go
+++ b/api/auth.go
@@ -141,6 +141,28 @@ func (a *api) authAdmin(fn func(http.ResponseWriter, *http.Request)) func(http.R
 	}
 }
 
+func (a *api) authAdminNum(fn func(int, http.ResponseWriter, *http.Request)) func(http.ResponseWriter, *http.Request) {
+	return func(w http.ResponseWriter, req *http.Request) {
+		token := req.Header.Get("x-authentication")
+		ok, claims := a.validateToken(token)
+		if !ok {
+			w.WriteHeader(http.StatusUnauthorized)
+			return
+		}
+		role, ok := claims["role"].(string)
+		if !ok || role != "admin" {
+			w.WriteHeader(http.StatusUnauthorized)
+			return
+		}
+		num, ok := claims["num"].(float64)
+		if !ok {
+			w.WriteHeader(http.StatusUnauthorized)
+			return
+		}
+		fn(int(num), w, req)
+	}
+}
+
 func (a *api) authNumRole(fn func(int, string, http.ResponseWriter, *http.Request)) func(http.ResponseWriter, *http.Request) {
 	return func(w http.ResponseWriter, req *http.Request) {
 		token := req.Header.Get("x-authentication")
diff --git a/api/purchase.go b/api/purchase.go
index 86b3b25..2d0e9c2 100644
--- a/api/purchase.go
+++ b/api/purchase.go
@@ -45,7 +45,7 @@ func (a *api) AddPurchase(num int, w http.ResponseWriter, req *http.Request) {
 		return
 	}
 
-	httpStatus := a.substractMemberBalance(num, total)
+	httpStatus := a.updateMemberBalance(num, -total)
 	if httpStatus != http.StatusOK {
 		w.WriteHeader(httpStatus)
 		return
@@ -56,7 +56,7 @@ func (a *api) AddPurchase(num int, w http.ResponseWriter, req *http.Request) {
 		Date:      time.Now(),
 		Purchase:  purchase,
 		Type:      "purchase",
-		Total:     total,
+		Total:     -total,
 	}
 	err = a.db.Create(&transaction).Error
 	if err != nil {
@@ -83,24 +83,3 @@ func (a *api) AddPurchase(num int, w http.ResponseWriter, req *http.Request) {
 	}
 
 }
-
-func (a *api) substractMemberBalance(num int, total int) int {
-	var member Member
-	err := a.db.Where("num = ?", num).Find(&member).Error
-	if err != nil {
-		log.Printf("Can't find member %d: %v", num, err)
-		return http.StatusNotAcceptable
-	}
-	if member.Balance < total {
-		log.Printf("Member %d don't have enough money (%d-%d)", num, member.Balance, total)
-		return http.StatusBadRequest
-	}
-	err = a.db.Model(&Member{}).
-		Where("num = ?", num).
-		Update("balance", gorm.Expr("balance - ?", total)).Error
-	if err != nil {
-		log.Printf("Can't update update member balance %d-%d: %v", num, total, err)
-		return http.StatusNotAcceptable
-	}
-	return http.StatusOK
-}
diff --git a/api/purchase_test.go b/api/purchase_test.go
index e176e19..3e979cb 100644
--- a/api/purchase_test.go
+++ b/api/purchase_test.go
@@ -30,7 +30,7 @@ func TestPurchaseAddListMine(t *testing.T) {
 	if len(transactions) != 1 {
 		t.Fatal("Wrong number of transactions", len(transactions), transactions)
 	}
-	if transactions[0].Total != testProduct.Price*products[0].Ammount {
+	if transactions[0].Total != -testProduct.Price*products[0].Ammount {
 		t.Error("Wrong total:", transactions[0].Total)
 	}
 	if len(transactions[0].Purchase) != 1 {
diff --git a/api/topup.go b/api/topup.go
new file mode 100644
index 0000000..cc6397e
--- /dev/null
+++ b/api/topup.go
@@ -0,0 +1,64 @@
+package api
+
+import (
+	"encoding/json"
+	"log"
+	"net/http"
+	"time"
+
+	"gorm.io/gorm"
+)
+
+type Topup struct {
+	gorm.Model    `json:"-"`
+	TransactionID int    `json:"-" gorm:"column:transaction"`
+	MemberNum     int    `json:"member" gorm:"column:member"`
+	Member        Member `json:"-" gorm:"foreignKey:MemberNum;references:Num"`
+	Comment       string `json:"comment"`
+}
+
+func (a *api) AddTopup(adminNum int, w http.ResponseWriter, req *http.Request) {
+	var topup struct {
+		Member  int    `json:"member"`
+		Comment string `json:"comment"`
+		Ammount int    `json:"ammount"`
+	}
+	err := json.NewDecoder(req.Body).Decode(&topup)
+	if err != nil {
+		log.Printf("Can't parse topup: %v", err)
+		w.WriteHeader(http.StatusInternalServerError)
+		return
+	}
+
+	httpStatus := a.updateMemberBalance(topup.Member, topup.Ammount)
+	if httpStatus != http.StatusOK {
+		w.WriteHeader(httpStatus)
+		return
+	}
+
+	transaction := Transaction{
+		MemberNum: topup.Member,
+		Date:      time.Now(),
+		Topup: Topup{
+			MemberNum: adminNum,
+			Comment:   topup.Comment,
+		},
+		Type:  "toupup",
+		Total: topup.Ammount,
+	}
+	err = a.db.Create(&transaction).Error
+	if err != nil {
+		log.Printf("Can't create topup: %v\n%v", err, transaction)
+		w.WriteHeader(http.StatusInternalServerError)
+		return
+	}
+
+	w.Header().Set("Content-Type", "application/json")
+	w.WriteHeader(http.StatusCreated)
+	err = json.NewEncoder(w).Encode(transaction)
+	if err != nil {
+		log.Printf("Can't encode added topup: %v", err)
+		w.WriteHeader(http.StatusInternalServerError)
+	}
+
+}
diff --git a/api/topup_test.go b/api/topup_test.go
new file mode 100644
index 0000000..e58883f
--- /dev/null
+++ b/api/topup_test.go
@@ -0,0 +1,50 @@
+package api
+
+import (
+	"net/http"
+	"testing"
+)
+
+func TestTopupAddListMine(t *testing.T) {
+	tapi := newTestAPI(t)
+	defer tapi.close()
+	tapi.addTestMember()
+	tapi.addTestProducts()
+
+	topup := map[string]interface{}{
+		"member":  testMember.Num,
+		"comment": "my topup",
+		"ammount": 20,
+	}
+	resp := tapi.do("POST", "/topup", topup, nil)
+	if resp.StatusCode != http.StatusCreated {
+		t.Fatal("Can't create topup:", resp.Status)
+	}
+	var transactions []Transaction
+	resp = tapi.do("GET", "/transaction/mine", nil, &transactions)
+	if resp.StatusCode != http.StatusOK {
+		t.Fatal("Can't get transactions:", resp.Status)
+	}
+
+	if len(transactions) != 1 {
+		t.Fatal("Wrong number of transactions", len(transactions), transactions)
+	}
+	if transactions[0].Total != 20 {
+		t.Error("Wrong total:", transactions[0].Total)
+	}
+	if transactions[0].Topup.MemberNum != testMember.Num {
+		t.Error("Wrong topup member:", transactions[0].Topup.MemberNum)
+	}
+	if transactions[0].Topup.Comment != "my topup" {
+		t.Error("Wrong topup comment:", transactions[0].Topup.Comment)
+	}
+
+	var member Member
+	resp = tapi.do("GET", "/member/10", nil, &member)
+	if resp.StatusCode != http.StatusOK {
+		t.Error("Can't find the member:", resp.Status)
+	}
+	if member.Balance != testMember.Balance+20 {
+		t.Error("Wrong product balance:", member.Balance)
+	}
+}
diff --git a/api/transaction.go b/api/transaction.go
index 74b3356..c33a2b0 100644
--- a/api/transaction.go
+++ b/api/transaction.go
@@ -21,6 +21,7 @@ type Transaction struct {
 	Type      string    `json:"type"`
 
 	Purchase []Purchase `json:"purchase"`
+	Topup    Topup      `json:"topup"`
 }
 
 func (a *api) ListTransactions(w http.ResponseWriter, req *http.Request) {
@@ -99,3 +100,24 @@ func (a *api) getTransactionsByMember(num int, w http.ResponseWriter, req *http.
 		w.WriteHeader(http.StatusInternalServerError)
 	}
 }
+
+func (a *api) updateMemberBalance(num int, ammount int) int {
+	var member Member
+	err := a.db.Where("num = ?", num).Find(&member).Error
+	if err != nil {
+		log.Printf("Can't find member %d: %v", num, err)
+		return http.StatusNotAcceptable
+	}
+	if member.Balance < -ammount {
+		log.Printf("Member %d don't have enough money (%d-%d)", num, member.Balance, ammount)
+		return http.StatusBadRequest
+	}
+	err = a.db.Model(&Member{}).
+		Where("num = ?", num).
+		Update("balance", gorm.Expr("balance + ?", ammount)).Error
+	if err != nil {
+		log.Printf("Can't update update member balance %d-%d: %v", num, ammount, err)
+		return http.StatusNotAcceptable
+	}
+	return http.StatusOK
+}
diff --git a/src/Head.js b/src/Head.js
index 69aa158..f300d89 100644
--- a/src/Head.js
+++ b/src/Head.js
@@ -1,6 +1,6 @@
 import React, { useContext } from 'react';
 import mano from './mano.svg';
-import { Navbar, Nav, Button, Form } from 'react-bootstrap';
+import { Navbar, Nav, NavDropdown, Button, Form } from 'react-bootstrap';
 import AuthContext from './AuthContext';
 
 function Head(props) {
@@ -8,9 +8,12 @@ function Head(props) {
 
     let adminNav;
     if (auth.role === "admin") {
-        adminNav = <Nav.Link href="/members">Socias</Nav.Link>;
+        adminNav = (
+            <NavDropdown title="Admin" id="admin">
+                <Nav.Link href="/members">Socias</Nav.Link>
+            </NavDropdown>
+        );
     }
-
     return (
         <Navbar bg="light">
             <Navbar.Brand href="/">
@@ -21,8 +24,8 @@ function Head(props) {
                 <Nav className="mr-auto">
                     <Nav.Link href="/">Dashboard</Nav.Link>
                     <Nav.Link href="/products">Productos</Nav.Link>
-                    {adminNav}
                 </Nav>
+                {adminNav}
                 <Form inline>
                   <Button
                     variant="outline-success"
-- 
GitLab