From 94cac3d5638559a78bb1112bd0fa29210bfff8f9 Mon Sep 17 00:00:00 2001
From: meskio <meskio@sindominio.net>
Date: Wed, 30 Dec 2020 19:08:48 +0100
Subject: [PATCH] Update order

---
 api/api.go        |   1 +
 api/db/order.go   | 231 +++++++++++++++++++++++++++++++++++++++-------
 api/order.go      |  39 ++++++++
 api/order_test.go | 224 ++++++++++++++++++++++++++++++++++++++++++++
 4 files changed, 463 insertions(+), 32 deletions(-)

diff --git a/api/api.go b/api/api.go
index bfab481..8a29074 100644
--- a/api/api.go
+++ b/api/api.go
@@ -58,6 +58,7 @@ func Init(dbPath string, signKey string, mail *Mail, r *mux.Router) error {
 	r.HandleFunc("/order", a.auth(a.ListOrders)).Methods("GET")
 	r.HandleFunc("/order", a.authOrderNum(a.AddOrder)).Methods("POST")
 	r.HandleFunc("/order/{id:[0-9]+}", a.authNum(a.GetOrder)).Methods("GET")
+	r.HandleFunc("/order/{id:[0-9]+}", a.authNumRole(a.UpdateOrder)).Methods("PUT")
 	r.HandleFunc("/order/{id:[0-9]+}", a.authNumRole(a.DeleteOrder)).Methods("DELETE")
 	r.HandleFunc("/order/active", a.auth(a.ListActiveOrders)).Methods("GET")
 	r.HandleFunc("/order/picks", a.authOrderNum(a.ListOrderPicks)).Methods("GET")
diff --git a/api/db/order.go b/api/db/order.go
index e4cfc5c..ece51ef 100644
--- a/api/db/order.go
+++ b/api/db/order.go
@@ -68,10 +68,14 @@ func (d *DB) GetOrder(memberNum int, id int) (order Order, transaction Transacti
 	if err != nil {
 		if errors.Is(err, gorm.ErrRecordNotFound) {
 			err = ErrorNotFound
-			return
 		}
 		return
 	}
+
+	if memberNum == 0 {
+		return
+	}
+
 	err = d.db.Where("member = ? AND type = 'order' AND order_id = ?", memberNum, id).
 		Preload("OrderPurchase.OrderProduct").
 		Find(&transaction).Error
@@ -82,16 +86,131 @@ func (d *DB) AddOrder(order *Order) error {
 	return d.db.Create(&order).Error
 }
 
-func (d *DB) DeleteOrder(memberNum int, id int) error {
-	var order Order
-	err := d.db.Preload(clause.Associations).
-		Preload("Transactions.OrderPurchase").
-		Preload("Transactions.Member").
-		First(&order, id).Error
+func (d *DB) UpdateOrder(memberNum int, id int, order *Order) error {
+	dbOrder, _, err := d.GetOrder(0, id)
 	if err != nil {
-		if errors.Is(err, gorm.ErrRecordNotFound) {
-			return ErrorNotFound
+		return err
+	}
+
+	if memberNum != 0 && dbOrder.MemberNum != memberNum {
+		return ErrorInvalidRequest
+	}
+	if dbOrder.Deadline.Add(5 * 24 * time.Hour).Before(time.Now()) {
+		return ErrorInvalidRequest
+	}
+	for _, p := range order.Products {
+		err = d.orderProductExist(&p)
+		if err != nil {
+			return err
+		}
+	}
+
+	dbOrder.Name = order.Name
+	dbOrder.Description = order.Description
+	dbOrder.Deadline = order.Deadline
+
+	return d.db.Transaction(func(tx *gorm.DB) error {
+		for _, product := range order.Products {
+			var err error
+			dbProduct := findOrderProduct(product.ProductCode, dbOrder.Products)
+			if dbProduct != nil {
+				dbProduct.Price = product.Price
+				err = tx.Save(&dbProduct).Error
+			} else {
+				err = tx.Create(&product).Error
+			}
+			if err != nil {
+				return err
+			}
+		}
+		for _, product := range dbOrder.Products {
+			if findOrderProduct(product.ProductCode, order.Products) == nil {
+				err = tx.Delete(&product).Error
+				if err != nil {
+					return err
+				}
+			}
+		}
+
+		totalSum := 0
+		for i, t := range dbOrder.Transactions {
+			var transaction Transaction
+			err := tx.Preload("OrderPurchase.OrderProduct").First(&transaction, id).Error
+			if err != nil {
+				return err
+			}
+			total, _ := calculateOrderPurchaseTotal(transaction.OrderPurchase, order.Products)
+			err = updateOrderPurchase(tx, t.MemberNum, &dbOrder.Transactions[i], total, t.OrderPurchase)
+			if err != nil {
+				return err
+			}
+			totalSum += total
+		}
+
+		if dbOrder.TransactionID != nil {
+			updateOrderTransaction(tx, int(*dbOrder.TransactionID), totalSum, &dbOrder)
+		}
+		err := tx.Save(&dbOrder).Error
+		if err != nil {
+			return err
+		}
+		return nil
+	})
+}
+
+func updateOrderTransaction(tx *gorm.DB, id int, total int, order *Order) error {
+	var transaction Transaction
+	err := tx.First(&transaction, id).Error
+	if err != nil {
+		return err
+	}
+
+	if order.Deadline.After(time.Now()) {
+		err := updateMemberBalance(tx, order.MemberNum, -transaction.Total)
+		if err != nil {
+			return err
+		}
+		err = tx.Delete(&transaction).Error
+		if err != nil {
+			return err
+		}
+		order.Active = true
+		order.TransactionID = nil
+	} else {
+		totalDiff := total - transaction.Total
+		err := updateMemberBalance(tx, order.MemberNum, totalDiff)
+		if err != nil {
+			return err
+		}
+		transaction.Total = total
+		err = tx.Save(&transaction).Error
+		if err != nil {
+			return err
+		}
+	}
+	return nil
+}
+
+func findOrderProduct(code int, products []OrderProduct) *OrderProduct {
+	for _, p := range products {
+		if p.ProductCode == code {
+			return &p
 		}
+	}
+	return nil
+}
+
+func (d *DB) orderProductExist(product *OrderProduct) error {
+	err := d.db.Where("code = ?", product.ProductCode).Find(&Product{}).Error
+	if errors.Is(err, gorm.ErrRecordNotFound) {
+		err = ErrorNotFound
+	}
+	return err
+}
+
+func (d *DB) DeleteOrder(memberNum int, id int) error {
+	order, _, err := d.GetOrder(0, id)
+	if err != nil {
 		return err
 	}
 	if memberNum != 0 && order.MemberNum != memberNum {
@@ -135,26 +254,14 @@ func (d *DB) AddOrderPurchase(memberNum int, orderID int, purchase []OrderPurcha
 		return
 	}
 
-	total := 0
-	for _, p := range purchase {
-		found := false
-		for _, product := range order.Products {
-			if product.ID == p.OrderProductID {
-				total += product.Price * p.Amount
-				found = true
-				break
-			}
-		}
-
-		if !found {
-			log.Printf("Order purchase product %d not in order: %v", p.OrderProductID, purchase)
-			err = ErrorInvalidRequest
-			return
-		}
+	total, err := calculateOrderPurchaseTotal(purchase, order.Products)
+	if err != nil {
+		return
 	}
 
 	if transaction.ID != 0 {
-		err = d.updateOrderPurchase(memberNum, &transaction, total, purchase)
+		transaction.Date = time.Now()
+		err = updateOrderPurchase(d.db, memberNum, &transaction, total, purchase)
 		return
 	}
 
@@ -170,32 +277,92 @@ func (d *DB) AddOrderPurchase(memberNum int, orderID int, purchase []OrderPurcha
 	return
 }
 
-func (d *DB) updateOrderPurchase(memberNum int, transaction *Transaction, total int, purchase []OrderPurchase) error {
+func calculateOrderPurchaseTotal(purchase []OrderPurchase, products []OrderProduct) (total int, err error) {
+	total = 0
+	for _, p := range purchase {
+		found := false
+		for _, product := range products {
+			log.Printf("%v", p.OrderProduct)
+			if (p.OrderProduct != nil && product.ProductCode == p.OrderProduct.ProductCode) ||
+				product.ID == p.OrderProductID {
+				total += product.Price * p.Amount
+				found = true
+				break
+			}
+		}
+
+		if !found {
+			log.Printf("Order purchase product %d not in order: %v", p.OrderProductID, purchase)
+			err = ErrorInvalidRequest
+		}
+	}
+	return total, err
+}
+
+func updateOrderPurchase(tx *gorm.DB, memberNum int, transaction *Transaction, total int, purchase []OrderPurchase) error {
 	totalDiff := -(transaction.Total + total)
 	transaction.Total = -total
-	transaction.Date = time.Now()
 
-	for i, p := range transaction.OrderPurchase {
-		for _, new_purchase := range purchase {
+	var updatePurchases []OrderPurchase
+	var newPurchases []OrderPurchase
+	for _, new_purchase := range purchase {
+		found := false
+		for i, p := range transaction.OrderPurchase {
 			if new_purchase.OrderProductID == p.OrderProductID {
 				transaction.OrderPurchase[i].Amount = new_purchase.Amount
+				updatePurchases = append(updatePurchases, transaction.OrderPurchase[i])
+				found = true
 				break
 			}
 		}
+
+		if !found {
+			newPurchases = append(newPurchases, OrderPurchase{
+				Amount:         new_purchase.Amount,
+				TransactionID:  transaction.ID,
+				OrderProductID: new_purchase.OrderProductID,
+			})
+		}
 	}
 
-	return d.db.Transaction(func(tx *gorm.DB) error {
+	var delPurchases []OrderPurchase
+	for _, p := range transaction.OrderPurchase {
+		found := false
+		for _, p2 := range purchase {
+			if p.OrderProductID == p2.OrderProductID {
+				found = true
+				break
+			}
+		}
+		if !found {
+			delPurchases = append(delPurchases, p)
+		}
+	}
+
+	return tx.Transaction(func(tx *gorm.DB) error {
 		err := updateMemberBalance(tx, memberNum, totalDiff)
 		if err != nil {
 			return err
 		}
 
-		for _, p := range transaction.OrderPurchase {
+		for _, p := range delPurchases {
+			err = tx.Delete(&p).Error
+			if err != nil {
+				return err
+			}
+		}
+		for _, p := range updatePurchases {
 			err = tx.Save(&p).Error
 			if err != nil {
 				return err
 			}
 		}
+		for _, p := range newPurchases {
+			err = tx.Create(&p).Error
+			if err != nil {
+				return err
+			}
+		}
 		return tx.Save(&transaction).Error
 	})
 }
diff --git a/api/order.go b/api/order.go
index dddd9c1..259aa47 100644
--- a/api/order.go
+++ b/api/order.go
@@ -154,6 +154,45 @@ func (a *api) AddOrder(num int, w http.ResponseWriter, req *http.Request) {
 	}
 }
 
+func (a *api) UpdateOrder(num int, role string, w http.ResponseWriter, req *http.Request) {
+	var order db.Order
+	err := json.NewDecoder(req.Body).Decode(&order)
+	if err != nil {
+		log.Printf("Can't parse order: %v", err)
+		w.WriteHeader(http.StatusInternalServerError)
+		return
+	}
+
+	vars := mux.Vars(req)
+	id, _ := strconv.Atoi(vars["id"])
+	if role == "admin" {
+		num = 0
+	}
+	err = a.db.UpdateOrder(num, id, &order)
+	if err != nil {
+		if errors.Is(err, db.ErrorNotFound) {
+			w.WriteHeader(http.StatusNotFound)
+			return
+		}
+		if errors.Is(err, db.ErrorInvalidRequest) {
+			w.WriteHeader(http.StatusUnauthorized)
+			return
+		}
+		log.Printf("Can't update order %s: %v", vars["id"], err)
+		w.WriteHeader(http.StatusInternalServerError)
+		return
+	}
+
+	w.Header().Set("Content-Type", "application/json")
+	w.WriteHeader(http.StatusAccepted)
+	err = json.NewEncoder(w).Encode(order)
+	if err != nil {
+		log.Printf("Can't encode order: %v", err)
+		w.WriteHeader(http.StatusInternalServerError)
+		return
+	}
+}
+
 func (a *api) AddOrderPurchase(num int, w http.ResponseWriter, req *http.Request) {
 	vars := mux.Vars(req)
 	id, _ := strconv.Atoi(vars["id"])
diff --git a/api/order_test.go b/api/order_test.go
index 5833fbb..1130f13 100644
--- a/api/order_test.go
+++ b/api/order_test.go
@@ -423,6 +423,230 @@ func TestUpdateOrderPurchase(t *testing.T) {
 	}
 }
 
+func TestOrderUpdate(t *testing.T) {
+	tapi := newTestAPI(t)
+	defer tapi.close()
+	tapi.addTestMember()
+	tapi.addTestOrder()
+
+	var orders []db.Order
+	resp := tapi.do("GET", "/order/active", nil, &orders)
+	if resp.StatusCode != http.StatusOK {
+		t.Fatal("Can't get orders:", resp.Status)
+	}
+
+	purchase := []db.OrderPurchase{
+		{
+			OrderProductID: orders[0].Products[0].ID,
+			Amount:         3,
+		},
+	}
+	resp = tapi.do("POST", fmt.Sprintf("/order/%d/purchase", orders[0].ID), purchase, nil)
+	if resp.StatusCode != http.StatusCreated {
+		t.Fatal("Can't create order:", resp.Status)
+	}
+
+	order := testOrder
+	order.Products[0].Price = 1000
+	resp = tapi.doOrder("PUT", fmt.Sprintf("/order/%d", orders[0].ID), order, nil)
+	if resp.StatusCode != http.StatusAccepted {
+		tapi.t.Fatal("Can't update order:", resp.Status)
+	}
+
+	var transactions []db.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(orders), orders)
+	}
+	total := 3 * order.Products[0].Price
+	if transactions[0].Total != -total {
+		t.Error("Wrong total", transactions[0].Total)
+	}
+
+	var member db.Member
+	resp = tapi.do("GET", "/member/me", nil, &member)
+	if resp.StatusCode != http.StatusOK {
+		t.Error("Can't find the member:", resp.Status)
+	}
+	if member.Balance != testMember.Balance-total {
+		t.Error("Wrong product balance:", member.Balance)
+	}
+}
+
+func TestOrderUpdateProduct(t *testing.T) {
+	tapi := newTestAPI(t)
+	defer tapi.close()
+	tapi.addTestMember()
+	tapi.addTestOrder()
+
+	var orders []db.Order
+	resp := tapi.do("GET", "/order/active", nil, &orders)
+	if resp.StatusCode != http.StatusOK {
+		t.Fatal("Can't get orders:", resp.Status)
+	}
+
+	purchase := []db.OrderPurchase{
+		{
+			OrderProductID: orders[0].Products[0].ID,
+			Amount:         3,
+		},
+	}
+	resp = tapi.do("POST", fmt.Sprintf("/order/%d/purchase", orders[0].ID), purchase, nil)
+	if resp.StatusCode != http.StatusCreated {
+		t.Fatal("Can't create order:", resp.Status)
+	}
+
+	testProduct2 := db.Product{
+		Code:  123,
+		Name:  "Huevos",
+		Price: 120,
+		Stock: 15,
+	}
+	resp = tapi.doAdmin("POST", "/product", testProduct2, nil)
+	if resp.StatusCode != http.StatusCreated {
+		tapi.t.Fatal("Can't create product:", resp.Status)
+	}
+
+	order := testOrder
+	order.Products[0].Price = testProduct2.Price
+	order.Products[0].ProductCode = testProduct2.Code
+	resp = tapi.doOrder("PUT", fmt.Sprintf("/order/%d", orders[0].ID), order, nil)
+	if resp.StatusCode != http.StatusAccepted {
+		tapi.t.Fatal("Can't update order:", resp.Status)
+	}
+
+	var transactions []db.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(orders), orders)
+	}
+	if transactions[0].Total != 0 {
+		t.Error("Wrong total", transactions[0].Total)
+	}
+}
+
+func TestOrderUpdateReactivate(t *testing.T) {
+	tapi := newTestAPI(t)
+	defer tapi.close()
+	tapi.addTestMember()
+	tapi.addTestProducts()
+
+	order := testOrder
+	now := time.Now()
+	order.Deadline = time.Date(now.Year(), now.Month(), now.Day()-1, 0, 0, 0, 0, time.Local)
+	resp := tapi.doOrder("POST", "/order", order, nil)
+	if resp.StatusCode != http.StatusCreated {
+		t.Fatal("Can't create order:", resp.Status)
+	}
+
+	var orders []db.Order
+	resp = tapi.do("GET", "/order/active", nil, &orders)
+	if resp.StatusCode != http.StatusOK {
+		t.Fatal("Can't get orders:", resp.Status)
+	}
+
+	purchase := []db.OrderPurchase{
+		{
+			OrderProductID: orders[0].Products[0].ID,
+			Amount:         3,
+		},
+	}
+	resp = tapi.do("POST", fmt.Sprintf("/order/%d/purchase", orders[0].ID), purchase, nil)
+	if resp.StatusCode != http.StatusCreated {
+		t.Fatal("Can't create order:", resp.Status)
+	}
+
+	orders = tapi.deactivateOrders()
+	if len(orders) != 1 {
+		t.Error("Deactivated none orders:", orders)
+	}
+
+	order.Deadline = testOrder.Deadline
+	resp = tapi.doOrder("PUT", fmt.Sprintf("/order/%d", orders[0].ID), order, nil)
+	if resp.StatusCode != http.StatusAccepted {
+		tapi.t.Fatal("Can't update order:", resp.Status)
+	}
+
+	resp = tapi.do("GET", "/order/active", nil, &orders)
+	if resp.StatusCode != http.StatusOK {
+		t.Fatal("Can't get orders:", resp.Status)
+	}
+	if len(orders) != 1 {
+		t.Error("The order is not being reactivated", orders)
+	}
+
+	var transactions []db.Transaction
+	resp = tapi.doOrder("GET", "/transaction/mine", nil, &transactions)
+	if resp.StatusCode != http.StatusOK {
+		t.Fatal("Can't get transactions:", resp.Status)
+	}
+	if len(transactions) != 0 {
+		t.Fatal("Wrong number of transactions", transactions)
+	}
+}
+
+func TestOrderUpdateDeactivated(t *testing.T) {
+	tapi := newTestAPI(t)
+	defer tapi.close()
+	tapi.addTestMember()
+	tapi.addTestProducts()
+
+	order := testOrder
+	now := time.Now()
+	order.Deadline = time.Date(now.Year(), now.Month(), now.Day()-1, 0, 0, 0, 0, time.Local)
+	resp := tapi.doOrder("POST", "/order", order, nil)
+	if resp.StatusCode != http.StatusCreated {
+		t.Fatal("Can't create order:", resp.Status)
+	}
+
+	var orders []db.Order
+	resp = tapi.do("GET", "/order/active", nil, &orders)
+	if resp.StatusCode != http.StatusOK {
+		t.Fatal("Can't get orders:", resp.Status)
+	}
+
+	purchase := []db.OrderPurchase{
+		{
+			OrderProductID: orders[0].Products[0].ID,
+			Amount:         3,
+		},
+	}
+	resp = tapi.do("POST", fmt.Sprintf("/order/%d/purchase", orders[0].ID), purchase, nil)
+	if resp.StatusCode != http.StatusCreated {
+		t.Fatal("Can't create order:", resp.Status)
+	}
+
+	orders = tapi.deactivateOrders()
+	if len(orders) != 1 {
+		t.Error("Deactivated none orders:", orders)
+	}
+
+	order.Products[0].Price = 1000
+	resp = tapi.doOrder("PUT", fmt.Sprintf("/order/%d", orders[0].ID), order, nil)
+	if resp.StatusCode != http.StatusAccepted {
+		tapi.t.Fatal("Can't update order:", resp.Status)
+	}
+
+	total := 3 * order.Products[0].Price
+	var transactions []db.Transaction
+	resp = tapi.doOrder("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", transactions)
+	}
+	if transactions[0].Total != total {
+		t.Fatal("Wrong updated total", transactions[0].Total, total)
+	}
+}
+
 func TestOrderPicks(t *testing.T) {
 	tapi := newTestAPI(t)
 	defer tapi.close()
-- 
GitLab