Skip to content
Snippets Groups Projects
transaction.go 5.79 KiB
Newer Older
  • Learn to ignore specific revisions
  • meskio's avatar
    meskio committed
    package db
    
    import (
    	"errors"
    	"fmt"
    	"log"
    
    meskio's avatar
    meskio committed
    	"strconv"
    	"strings"
    
    meskio's avatar
    meskio committed
    	"time"
    
    	"gorm.io/gorm"
    	"gorm.io/gorm/clause"
    )
    
    
    meskio's avatar
    meskio committed
    const (
    	dateLayout = "2006-01-02"
    )
    
    
    meskio's avatar
    meskio committed
    type Transaction struct {
    	gorm.Model
    	MemberNum int       `json:"-" gorm:"column:member"`
    	Member    *Member   `json:"member,omitempty" gorm:"foreignKey:MemberNum;references:Num"`
    	Date      time.Time `json:"date"`
    	Total     int       `json:"total"`
    	Type      string    `json:"type"`
    
    meskio's avatar
    meskio committed
    	ProxyNum  int       `json:"-" gorm:"column:proxy"`
    	Proxy     *Member   `json:"proxy,omitempty" gorm:"foreignKey:ProxyNum;references:Num"`
    
    meskio's avatar
    meskio committed
    
    	Purchase      []Purchase      `json:"purchase,omitempty"`
    	Topup         *Topup          `json:"topup,omitempty"`
    
    meskio's avatar
    meskio committed
    	OrderPurchase []OrderPurchase `json:"order_purchase,omitempty" gorm:"foreignKey:TransactionID;constraint:OnDelete:CASCADE"`
    	Order         *Order          `json:"order,omitempty" gorm:"constraint:OnDelete:CASCADE"`
    
    meskio's avatar
    meskio committed
    	OrderID       *uint           `json:"-"`
    	Refund        *Order          `json:"refund,omitempty" gorm:"foreignKey:TransactionID"`
    }
    
    type Topup struct {
    	gorm.Model    `json:"-"`
    	TransactionID uint   `json:"-" gorm:"column:transaction"`
    	Comment       string `json:"comment"`
    }
    
    type Purchase struct {
    
    meskio's avatar
    meskio committed
    	gorm.Model
    
    meskio's avatar
    meskio committed
    	TransactionID uint    `json:"-" gorm:"column:transaction"`
    	ProductCode   int     `json:"code" gorm:"column:product"`
    	Product       Product `json:"product" gorm:"foreignKey:ProductCode;references:Code"`
    	Price         int     `json:"price"`
    	Amount        int     `json:"amount"`
    }
    
    
    meskio's avatar
    meskio committed
    func (d *DB) ListTransactions(num int, query map[string][]string) (transactions []Transaction, err error) {
    
    meskio's avatar
    meskio committed
    	tx := d.transactionQuery()
    
    meskio's avatar
    meskio committed
    	if num != 0 {
    		tx = tx.Where("member = ?", num)
    	}
    
    meskio's avatar
    meskio committed
    	for k, v := range query {
    		switch k {
    		case "start-date":
    			var date time.Time
    			date, err = time.Parse(dateLayout, v[0])
    			if err != nil {
    				return
    			}
    			tx = tx.Where("date >= ?", date)
    		case "end-date":
    			var date time.Time
    			date, err = time.Parse(dateLayout, v[0])
    
    			date = date.Add(24 * time.Hour)
    
    meskio's avatar
    meskio committed
    			if err != nil {
    				return
    			}
    			tx = tx.Where("date <= ?", date)
    		case "member":
    
    meskio's avatar
    meskio committed
    			if num != 0 {
    				continue
    			}
    
    			tx = tx.Where("member in ?", v)
    
    meskio's avatar
    meskio committed
    		case "proxy":
    
    			tx = tx.Where("proxy in ?", v)
    
    meskio's avatar
    meskio committed
    		case "type":
    			tx = tx.Where("type in ?", v)
    
    meskio's avatar
    meskio committed
    		case "product":
    			var ids []interface{}
    			where := make([]string, len(v))
    			for i := range v {
    				var id int
    				id, err = strconv.Atoi(v[i])
    				if err != nil {
    					return
    				}
    				ids = append(ids, id, id)
    				where[i] = "purchases.product = ? OR order_products.product_code = ?"
    			}
    			tx = tx.Joins("left join purchases on purchases.`transaction` = transactions.id").
    				Joins("left join order_purchases on order_purchases.transaction_id = transactions.id").
    				Joins("left join order_products on order_products.id = order_purchases.order_product_id").
    				Where(strings.Join(where, " OR "), ids...)
    
    meskio's avatar
    meskio committed
    		default:
    			log.Printf("Unexpected transaction query: %s %v", k, v)
    		}
    	}
    
    meskio's avatar
    meskio committed
    	err = tx.Group("transactions.id").
    
    meskio's avatar
    meskio committed
    		Order("date desc").
    		Find(&transactions).Error
    	return
    }
    
    func (d *DB) GetTransaction(id int) (transaction Transaction, err error) {
    	err = d.transactionQuery().
    		First(&transaction, id).Error
    	if errors.Is(err, gorm.ErrRecordNotFound) {
    		err = ErrorNotFound
    	}
    	return
    }
    
    func (d *DB) AddTopup(adminNum int, memberNum int, amount int, comment string) (transaction Transaction, err error) {
    	transaction = Transaction{
    		MemberNum: memberNum,
    
    meskio's avatar
    meskio committed
    		ProxyNum:  adminNum,
    
    meskio's avatar
    meskio committed
    		Date:      time.Now(),
    		Topup: &Topup{
    
    meskio's avatar
    meskio committed
    			Comment: comment,
    
    meskio's avatar
    meskio committed
    		},
    		Type:  "topup",
    		Total: amount,
    	}
    	err = createTransaction(d.db, &transaction)
    	return
    }
    
    
    meskio's avatar
    meskio committed
    func (d *DB) AddPurchase(adminNum int, memberNum int, purchase []Purchase) (transaction Transaction, err error) {
    
    meskio's avatar
    meskio committed
    	total := 0
    	for i, p := range purchase {
    		var product Product
    		err = d.db.Where("code = ?", p.ProductCode).First(&product).Error
    		if err != nil {
    			log.Printf("Can't get product %d: %v", p.ProductCode, err)
    			err = ErrorNotFound
    			return
    		}
    
    		total += product.Price * p.Amount
    		purchase[i].Price = product.Price
    	}
    	if total == 0 {
    		log.Printf("Empty purchase (%d)", memberNum)
    		err = ErrorInvalidRequest
    		return
    	}
    
    	transaction = Transaction{
    		MemberNum: memberNum,
    
    meskio's avatar
    meskio committed
    		ProxyNum:  adminNum,
    
    meskio's avatar
    meskio committed
    		Date:      time.Now(),
    		Purchase:  purchase,
    		Type:      "purchase",
    		Total:     -total,
    	}
    	err = d.db.Transaction(func(tx *gorm.DB) error {
    		err := createTransaction(tx, &transaction)
    		if err != nil {
    			return err
    		}
    
    		for _, p := range purchase {
    			err = tx.Model(&Product{}).
    				Where("code = ?", p.ProductCode).
    				Update("stock", gorm.Expr("stock - ?", p.Amount)).Error
    			if err != nil {
    				return fmt.Errorf("Can't update product stock %d-%d: %v", p.ProductCode, p.Amount, err)
    			}
    		}
    		return nil
    	})
    	return
    }
    
    func (d *DB) transactionQuery() *gorm.DB {
    	return d.db.Preload("Purchase.Product").
    		Preload("Order.Products").
    
    		Preload("OrderPurchase.OrderProduct.Product").
    
    meskio's avatar
    meskio committed
    		Preload(clause.Associations)
    }
    
    func createTransaction(db *gorm.DB, transaction *Transaction) error {
    	return db.Transaction(func(tx *gorm.DB) error {
    
    meskio's avatar
    meskio committed
    		err := updateMemberBalance(tx, transaction.MemberNum, transaction.Total)
    
    meskio's avatar
    meskio committed
    		if err != nil {
    			return err
    		}
    		return tx.Create(&transaction).Error
    	})
    }
    
    meskio's avatar
    meskio committed
    
    func updateMemberBalance(tx *gorm.DB, memberNum int, amount int) error {
    	var member Member
    	err := tx.Where("num = ?", memberNum).Find(&member).Error
    	if err != nil {
    		log.Printf("Can't find member for transaction %d: %v", memberNum, err)
    		return ErrorNotFound
    	}
    	if member.Balance < -amount {
    		log.Printf("Member %d don't have enough money (%d-%d)", member.Num, member.Balance, amount)
    		return ErrorInvalidRequest
    	}
    	err = tx.Model(&Member{}).
    		Where("num = ?", memberNum).
    		Update("balance", gorm.Expr("balance + ?", amount)).Error
    	if err != nil {
    		log.Printf("Can't update update member balance %d-%d: %v", member.Num, amount, err)
    	}
    	return err
    }