From f2fcb2b397ec47fe147d32f69f449bef8281b613 Mon Sep 17 00:00:00 2001
From: meskio <meskio@sindominio.net>
Date: Fri, 9 Oct 2020 15:07:35 +0200
Subject: [PATCH] Redesign orders in the db

---
 api/order.go       | 73 ++++++++++++++++++++++++++++------------------
 api/order_test.go  | 36 +++++++++++++----------
 api/transaction.go | 28 +++++++++++-------
 3 files changed, 82 insertions(+), 55 deletions(-)

diff --git a/api/order.go b/api/order.go
index 1c05d46..11e68eb 100644
--- a/api/order.go
+++ b/api/order.go
@@ -16,14 +16,14 @@ type Order struct {
 	gorm.Model
 	Name        string    `json:"name"`
 	Description string    `json:"description"`
-	MemberNum   int       `json:"-" gorm:"column:member"`
+	MemberNum   int       `json:"member_num" gorm:"column:member"`
 	Member      *Member   `json:"member,omitempty" gorm:"foreignKey:MemberNum;references:Num"`
 	Deadline    time.Time `json:"deadline"`
 	Active      bool      `json:"active" gorm:"index"`
 
-	Products      []Product       `json:"products" gorm:"many2many:order_products;References:Code;JoinReferences:ProductCode"`
-	Purchases     []OrderPurchase `json:"purchases"`
-	TransactionID *uint           `json:"-" gorm:"column:transaction"`
+	Products      []Product     `json:"products" gorm:"many2many:order_products;References:Code;JoinReferences:ProductCode"`
+	Transactions  []Transaction `json:"transactions" gorm:"foreignKey:OrderID"`
+	TransactionID *uint         `json:"-" gorm:"column:transaction"`
 }
 
 type OrderPurchase struct {
@@ -31,8 +31,6 @@ type OrderPurchase struct {
 	TransactionID uint     `json:"-"`
 	ProductCode   int      `json:"product_code"`
 	Product       *Product `json:"product" gorm:"foreignKey:ProductCode;references:Code"`
-	OrderID       uint     `json:"order_id"`
-	Order         *Order   `json:"-"`
 	Price         int      `json:"price"`
 	Amount        int      `json:"amount"`
 }
@@ -42,6 +40,11 @@ type OrderGetResponse struct {
 	Transaction *Transaction `json:"transaction"`
 }
 
+type OrderPurchaseRequest struct {
+	Purchase []OrderPurchase `json:"purchase"`
+	OrderID  uint            `json:"order"`
+}
+
 func (a *api) refundOrders() {
 	const refundSleeptime = 10 * time.Minute
 	for {
@@ -55,7 +58,7 @@ func (a *api) deactivateOrders() {
 	now := time.Now()
 	t := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, time.Local)
 	err := a.db.Where("active = ? AND deadline < ?", true, t).
-		Preload("Purchases").
+		Preload("Transactions.OrderPurchase").
 		Find(&orders).Error
 	if err != nil {
 		log.Println("Error refunding orders:", err)
@@ -64,8 +67,10 @@ func (a *api) deactivateOrders() {
 
 	for _, order := range orders {
 		total := 0
-		for _, purchase := range order.Purchases {
-			total += purchase.Price * purchase.Amount
+		for _, transaction := range order.Transactions {
+			for _, purchase := range transaction.OrderPurchase {
+				total += purchase.Price * purchase.Amount
+			}
 		}
 
 		transaction := Transaction{
@@ -105,7 +110,8 @@ func (a *api) ListActiveOrders(w http.ResponseWriter, req *http.Request) {
 
 func (a *api) listOrders(active bool, w http.ResponseWriter, req *http.Request) {
 	var orders []Order
-	query := a.db.Preload(clause.Associations)
+	query := a.db.Preload(clause.Associations).
+		Preload("Transactions.OrderPurchase")
 	if active {
 		query = query.Where("active = ?", true)
 	}
@@ -129,6 +135,8 @@ func (a *api) GetOrder(num int, w http.ResponseWriter, req *http.Request) {
 	vars := mux.Vars(req)
 	var order Order
 	err := a.db.Preload(clause.Associations).
+		Preload("Transactions.OrderPurchase").
+		Preload("Transactions.Member").
 		First(&order, vars["id"]).Error
 	if err != nil {
 		if errors.Is(err, gorm.ErrRecordNotFound) {
@@ -143,10 +151,8 @@ func (a *api) GetOrder(num int, w http.ResponseWriter, req *http.Request) {
 	body.Order = order
 
 	var transaction Transaction
-	err = a.db.Where("member = ? AND type = 'order' AND id IN (?)", num,
-		a.db.Table("order_purchases").
-			Where("order_id = ?", order.ID).
-			Select("transaction_id")).
+	err = a.db.Where("member = ? AND type = 'order' AND order_id = ?", num, vars["id"]).
+		Preload("OrderPurchase.Product").
 		Find(&transaction).Error
 	if err != nil {
 		log.Printf("Can't get order transaction %s: %v", vars["id"], err)
@@ -196,14 +202,14 @@ func (a *api) AddOrder(num int, w http.ResponseWriter, req *http.Request) {
 }
 
 func (a *api) AddOrderPurchase(num int, w http.ResponseWriter, req *http.Request) {
-	var purchase []OrderPurchase
-	err := json.NewDecoder(req.Body).Decode(&purchase)
+	var request OrderPurchaseRequest
+	err := json.NewDecoder(req.Body).Decode(&request)
 	if err != nil {
-		log.Printf("Can't parse order: %v", err)
+		log.Printf("Can't parse order purchase: %v", err)
 		w.WriteHeader(http.StatusInternalServerError)
 		return
 	}
-	if len(purchase) == 0 {
+	if len(request.Purchase) == 0 {
 		log.Printf("Empty order purchase")
 		w.WriteHeader(http.StatusBadRequest)
 		return
@@ -211,43 +217,52 @@ func (a *api) AddOrderPurchase(num int, w http.ResponseWriter, req *http.Request
 
 	var order Order
 	err = a.db.Preload("Products").
-		First(&order, purchase[0].OrderID).Error
+		Preload("Transactions").
+		First(&order, request.OrderID).Error
 	if err != nil {
-		log.Printf("Can't get order %d: %v", purchase[0].OrderID, err)
+		log.Printf("Can't get order %d: %v", request.OrderID, err)
 		w.WriteHeader(http.StatusInternalServerError)
 		return
 	}
 	if !order.Active {
-		log.Printf("Order is not active %d: %v", order.ID, purchase)
+		log.Printf("Order is not active %d: %v", order.ID, request)
 		w.WriteHeader(http.StatusBadRequest)
 		return
 	}
+	for _, t := range order.Transactions {
+		if t.MemberNum == num {
+			log.Printf("Purchase by %d for %d when there is already one by this member: %v", num, order.ID, request)
+			w.WriteHeader(http.StatusBadRequest)
+			return
+		}
+	}
 
 	total := 0
-	for i, p := range purchase {
+	for i, p := range request.Purchase {
 		found := false
 		for _, product := range order.Products {
 			if product.Code == p.ProductCode {
 				total += product.Price * p.Amount
-				purchase[i].Price = product.Price
+				request.Purchase[i].Price = product.Price
 				found = true
 				break
 			}
 		}
 
 		if !found {
-			log.Printf("Order purchase product %d not in order: %v", p.ProductCode, purchase)
+			log.Printf("Order purchase product %d not in order: %v", p.ProductCode, request)
 			w.WriteHeader(http.StatusBadRequest)
 			return
 		}
 	}
 
 	transaction := Transaction{
-		MemberNum: num,
-		Total:     -total,
-		Type:      "order",
-		Date:      time.Now(),
-		Order:     purchase,
+		MemberNum:     num,
+		Total:         -total,
+		Type:          "order",
+		Date:          time.Now(),
+		OrderPurchase: request.Purchase,
+		OrderID:       &order.ID,
 	}
 	httpStatus, err := createTransaction(a.db, &transaction)
 	if err != nil {
diff --git a/api/order_test.go b/api/order_test.go
index 0343838..a73261c 100644
--- a/api/order_test.go
+++ b/api/order_test.go
@@ -75,11 +75,13 @@ func TestOrderPurchase(t *testing.T) {
 		t.Fatal("Can't get orders:", resp.Status)
 	}
 
-	purchase := []OrderPurchase{
-		{
-			ProductCode: testProduct.Code,
-			Amount:      3,
-			OrderID:     orders[0].ID,
+	purchase := OrderPurchaseRequest{
+		OrderID: orders[0].ID,
+		Purchase: []OrderPurchase{
+			{
+				ProductCode: testProduct.Code,
+				Amount:      3,
+			},
 		},
 	}
 	resp = tapi.do("POST", "/order/purchase", purchase, nil)
@@ -170,11 +172,13 @@ func TestOrderDeactivation(t *testing.T) {
 		t.Fatal("Didn't find my new order")
 	}
 
-	purchase := []OrderPurchase{
-		{
-			ProductCode: testProduct.Code,
-			Amount:      3,
-			OrderID:     orders[0].ID,
+	purchase := OrderPurchaseRequest{
+		OrderID: orders[0].ID,
+		Purchase: []OrderPurchase{
+			{
+				ProductCode: testProduct.Code,
+				Amount:      3,
+			},
 		},
 	}
 	resp = tapi.doAdmin("POST", "/order/purchase", purchase, nil)
@@ -253,11 +257,13 @@ func TestGetOrder(t *testing.T) {
 		t.Error("Wrong name:", body.Order.Name)
 	}
 
-	purchase := []OrderPurchase{
-		{
-			ProductCode: testProduct.Code,
-			Amount:      3,
-			OrderID:     orders[0].ID,
+	purchase := OrderPurchaseRequest{
+		OrderID: orders[0].ID,
+		Purchase: []OrderPurchase{
+			{
+				ProductCode: testProduct.Code,
+				Amount:      3,
+			},
 		},
 	}
 	resp = tapi.do("POST", "/order/purchase", purchase, nil)
diff --git a/api/transaction.go b/api/transaction.go
index f1bd209..6addd9d 100644
--- a/api/transaction.go
+++ b/api/transaction.go
@@ -22,16 +22,17 @@ type Transaction struct {
 	Total     int       `json:"total"`
 	Type      string    `json:"type"`
 
-	Purchase []Purchase      `json:"purchase"`
-	Topup    *Topup          `json:"topup"`
-	Order    []OrderPurchase `json:"order"`
-	Refund   *Order          `json:"refund"`
+	Purchase      []Purchase      `json:"purchase,omitempty"`
+	Topup         *Topup          `json:"topup,omitempty"`
+	OrderPurchase []OrderPurchase `json:"order_purchase,omitempty" gorm:"foreignKey:TransactionID"`
+	Order         *Order          `json:"order,omitempty"`
+	OrderID       *uint           `json:"-"`
+	Refund        *Order          `json:"refund,omitempty" gorm:"foreignKey:TransactionID"`
 }
 
 func (a *api) ListTransactions(w http.ResponseWriter, req *http.Request) {
 	var transactions []Transaction
-	err := a.db.Preload("Purchase.Product").
-		Preload(clause.Associations).
+	err := a.transactionQuery().
 		Order("date desc").
 		Find(&transactions).Error
 	if err != nil {
@@ -51,8 +52,7 @@ func (a *api) ListTransactions(w http.ResponseWriter, req *http.Request) {
 func (a *api) GetTransaction(num int, role string, w http.ResponseWriter, req *http.Request) {
 	vars := mux.Vars(req)
 	var transaction Transaction
-	err := a.db.Preload("Purchase.Product").
-		Preload(clause.Associations).
+	err := a.transactionQuery().
 		First(&transaction, vars["id"]).Error
 	if err != nil {
 		if errors.Is(err, gorm.ErrRecordNotFound) {
@@ -87,9 +87,8 @@ func (a *api) GetMemberTransactions(w http.ResponseWriter, req *http.Request) {
 
 func (a *api) getTransactionsByMember(num int, w http.ResponseWriter, req *http.Request) {
 	var transactions []Transaction
-	err := a.db.Where("member = ?", num).
-		Preload("Purchase.Product").
-		Preload(clause.Associations).
+	err := a.transactionQuery().
+		Where("member = ?", num).
 		Order("date desc").
 		Find(&transactions).Error
 	if err != nil {
@@ -136,3 +135,10 @@ func createTransaction(db *gorm.DB, transaction *Transaction) (httpStatus int, e
 	})
 	return
 }
+
+func (a *api) transactionQuery() *gorm.DB {
+	return a.db.Preload("Purchase.Product").
+		Preload("Order.Products").
+		Preload("OrderPurchase.Product").
+		Preload(clause.Associations)
+}
-- 
GitLab