github.com/condensat/bank-core@v0.1.0/database/query/accountoperation.go (about)

     1  // Copyright 2020 Condensat Tech. All rights reserved.
     2  // Use of this source code is governed by a MIT
     3  // license that can be found in the LICENSE file.
     4  
     5  package query
     6  
     7  import (
     8  	"errors"
     9  	"time"
    10  
    11  	"github.com/condensat/bank-core/database"
    12  	"github.com/condensat/bank-core/database/model"
    13  
    14  	"github.com/jinzhu/gorm"
    15  )
    16  
    17  const (
    18  	HistoryMaxOperationCount = 1000
    19  )
    20  
    21  var (
    22  	ErrInvalidAccountOperation = errors.New("Invalid Account Operation")
    23  )
    24  
    25  type AccountOperationPrevNext struct {
    26  	model.AccountOperation
    27  	Previous model.AccountOperationID
    28  	Next     model.AccountOperationID
    29  }
    30  
    31  func AppendAccountOperation(db database.Context, operation model.AccountOperation) (model.AccountOperation, error) {
    32  	result, err := AppendAccountOperationSlice(db, operation)
    33  	if err != nil {
    34  		return model.AccountOperation{}, err
    35  	}
    36  	if len(result) != 1 {
    37  		return model.AccountOperation{}, ErrInvalidAccountOperation
    38  	}
    39  	return result[0], nil
    40  }
    41  
    42  func TxAppendAccountOperation(db database.Context, operation model.AccountOperation) (model.AccountOperation, error) {
    43  	operation.Timestamp = operation.Timestamp.UTC().Truncate(time.Second)
    44  
    45  	return txApppendAccountOperation(db, operation)
    46  }
    47  
    48  func AppendAccountOperationSlice(db database.Context, operations ...model.AccountOperation) ([]model.AccountOperation, error) {
    49  	if db == nil {
    50  		return nil, database.ErrInvalidDatabase
    51  	}
    52  
    53  	var result []model.AccountOperation
    54  	err := db.Transaction(func(db database.Context) error {
    55  		var txErr error
    56  		result, txErr = TxAppendAccountOperationSlice(db, operations...)
    57  		return txErr
    58  	})
    59  
    60  	return result, err
    61  }
    62  
    63  func TxAppendAccountOperationSlice(db database.Context, operations ...model.AccountOperation) ([]model.AccountOperation, error) {
    64  	if db == nil {
    65  		return nil, database.ErrInvalidDatabase
    66  	}
    67  
    68  	// pre-check all operations
    69  	for _, operation := range operations {
    70  		// check for valid accountID
    71  		accountID := operation.AccountID
    72  		if accountID == 0 {
    73  			return nil, ErrInvalidAccountID
    74  		}
    75  
    76  		// UTC timestamp
    77  		operation.Timestamp = operation.Timestamp.UTC().Truncate(time.Second)
    78  
    79  		// pre-check operation with ids
    80  		if !operation.PreCheck() {
    81  			return nil, ErrInvalidAccountOperation
    82  		}
    83  	}
    84  
    85  	// already within a db transaction
    86  	var result []model.AccountOperation
    87  	err := func(db database.Context) error {
    88  
    89  		// append all operations in same transaction
    90  		// returning error will cause rollback
    91  		for _, operation := range operations {
    92  			op, err := txApppendAccountOperation(db, operation)
    93  			if err != nil {
    94  				return err
    95  			}
    96  			result = append(result, op)
    97  		}
    98  
    99  		return nil
   100  	}(db)
   101  
   102  	// return result with error
   103  	return result, err
   104  }
   105  
   106  func GetPreviousAccountOperation(db database.Context, accountID model.AccountID, operationID model.AccountOperationID) (model.AccountOperation, error) {
   107  	if db == nil {
   108  		return model.AccountOperation{}, database.ErrInvalidDatabase
   109  	}
   110  	gdb := db.DB().(*gorm.DB)
   111  
   112  	if accountID == 0 {
   113  		return model.AccountOperation{}, ErrInvalidAccountID
   114  	}
   115  	if operationID == 0 {
   116  		return model.AccountOperation{}, ErrInvalidAccountOperation
   117  	}
   118  
   119  	var result model.AccountOperation
   120  	err := gdb.
   121  		Where(model.AccountOperation{
   122  			AccountID: accountID,
   123  		}).
   124  		Where("id < ?", operationID).
   125  		Order("id DESC", true).
   126  		Take(&result).Error
   127  
   128  	if err != nil {
   129  		return model.AccountOperation{}, err
   130  	}
   131  
   132  	return result, err
   133  }
   134  
   135  func GetNextAccountOperation(db database.Context, accountID model.AccountID, operationID model.AccountOperationID) (model.AccountOperation, error) {
   136  	if db == nil {
   137  		return model.AccountOperation{}, database.ErrInvalidDatabase
   138  	}
   139  	gdb := db.DB().(*gorm.DB)
   140  
   141  	if accountID == 0 {
   142  		return model.AccountOperation{}, ErrInvalidAccountID
   143  	}
   144  	if operationID == 0 {
   145  		return model.AccountOperation{}, ErrInvalidAccountOperation
   146  	}
   147  
   148  	var result model.AccountOperation
   149  	err := gdb.
   150  		Where(model.AccountOperation{
   151  			AccountID: accountID,
   152  		}).
   153  		Where("id > ?", operationID).
   154  		Order("id ASC", true).
   155  		First(&result).Error
   156  
   157  	if err != nil {
   158  		return model.AccountOperation{}, err
   159  	}
   160  
   161  	return result, err
   162  }
   163  
   164  func GetLastAccountOperation(db database.Context, accountID model.AccountID) (model.AccountOperation, error) {
   165  	if db == nil {
   166  		return model.AccountOperation{}, database.ErrInvalidDatabase
   167  	}
   168  	gdb := db.DB().(*gorm.DB)
   169  
   170  	if accountID == 0 {
   171  		return model.AccountOperation{}, ErrInvalidAccountID
   172  	}
   173  
   174  	var result model.AccountOperation
   175  	err := gdb.
   176  		Where(model.AccountOperation{
   177  			AccountID: accountID,
   178  		}).
   179  		Last(&result).Error
   180  
   181  	if err != nil {
   182  		return model.AccountOperation{}, err
   183  	}
   184  
   185  	return result, err
   186  }
   187  
   188  func GeAccountHistory(db database.Context, accountID model.AccountID) ([]model.AccountOperation, error) {
   189  	if db == nil {
   190  		return nil, database.ErrInvalidDatabase
   191  	}
   192  	gdb := db.DB().(*gorm.DB)
   193  
   194  	if accountID == 0 {
   195  		return nil, ErrInvalidAccountID
   196  	}
   197  
   198  	var list []*model.AccountOperation
   199  	err := gdb.
   200  		Where(model.AccountOperation{
   201  			AccountID: accountID,
   202  		}).
   203  		Order("id ASC").
   204  		Limit(HistoryMaxOperationCount).
   205  		Find(&list).Error
   206  
   207  	if err != nil && err != gorm.ErrRecordNotFound {
   208  		return nil, err
   209  	}
   210  
   211  	return convertAccountOperationList(list), nil
   212  }
   213  
   214  func GeAccountHistoryWithPrevNext(db database.Context, accountID model.AccountID) ([]AccountOperationPrevNext, error) {
   215  	if db == nil {
   216  		return nil, database.ErrInvalidDatabase
   217  	}
   218  	gdb := db.DB().(*gorm.DB)
   219  
   220  	if accountID == 0 {
   221  		return nil, ErrInvalidAccountID
   222  	}
   223  
   224  	var rows []*AccountOperationPrevNext
   225  	const query = `
   226  		SELECT *,
   227  			(SELECT id FROM account_operation AS sub
   228  					WHERE ops.account_id = sub.account_id AND sub.id < ops.id
   229  					ORDER BY sub.id DESC
   230  					LIMIT 1
   231  			) AS previous,
   232  			(SELECT id FROM account_operation AS sub
   233  					WHERE ops.account_id = sub.account_id AND sub.id > ops.id
   234  					ORDER BY sub.id ASC
   235  					LIMIT 1
   236  			) AS next
   237  		FROM account_operation as ops WHERE account_id = ? ORDER BY id asc;`
   238  	err := gdb.
   239  		Raw(query, accountID).
   240  		Find(&rows).Error
   241  	if err != nil && err != gorm.ErrRecordNotFound {
   242  		return nil, err
   243  	}
   244  
   245  	return convertAccountOperationPrevNextList(rows), nil
   246  }
   247  
   248  func GeAccountHistoryRange(db database.Context, accountID model.AccountID, from, to time.Time) ([]model.AccountOperation, error) {
   249  	if db == nil {
   250  		return nil, database.ErrInvalidDatabase
   251  	}
   252  	gdb := db.DB().(*gorm.DB)
   253  
   254  	if accountID == 0 {
   255  		return nil, ErrInvalidAccountID
   256  	}
   257  
   258  	from = from.UTC().Truncate(time.Second)
   259  	to = to.UTC().Truncate(time.Second)
   260  
   261  	if from.After(to) {
   262  		from, to = to, from
   263  	}
   264  
   265  	var list []*model.AccountOperation
   266  	err := gdb.
   267  		Where(model.AccountOperation{
   268  			AccountID: accountID,
   269  		}).
   270  		Where("timestamp BETWEEN ? AND ?", from, to).
   271  		Order("id ASC").
   272  		Limit(HistoryMaxOperationCount).
   273  		Find(&list).Error
   274  
   275  	if err != nil && err != gorm.ErrRecordNotFound {
   276  		return nil, err
   277  	}
   278  
   279  	return convertAccountOperationList(list), nil
   280  }
   281  
   282  func FindAccountOperationByReference(db database.Context, synchroneousType model.SynchroneousType, operationType model.OperationType, referenceID model.RefID) (model.AccountOperation, error) {
   283  	if db == nil {
   284  		return model.AccountOperation{}, database.ErrInvalidDatabase
   285  	}
   286  	gdb := db.DB().(*gorm.DB)
   287  
   288  	if len(synchroneousType) == 0 {
   289  		return model.AccountOperation{}, model.ErrSynchroneousTypeInvalid
   290  	}
   291  	if len(operationType) == 0 {
   292  		return model.AccountOperation{}, model.ErrOperationTypeInvalid
   293  	}
   294  	if referenceID == 0 {
   295  		return model.AccountOperation{}, ErrInvalidReferenceID
   296  	}
   297  
   298  	var result model.AccountOperation
   299  	err := gdb.
   300  		Where(model.AccountOperation{
   301  			SynchroneousType: synchroneousType,
   302  			OperationType:    operationType,
   303  			ReferenceID:      referenceID,
   304  		}).
   305  		Last(&result).Error
   306  
   307  	if err != nil {
   308  		return model.AccountOperation{}, err
   309  	}
   310  
   311  	return result, err
   312  }
   313  
   314  func convertAccountOperationList(list []*model.AccountOperation) []model.AccountOperation {
   315  	var result []model.AccountOperation
   316  	for _, curr := range list {
   317  		if curr != nil {
   318  			result = append(result, *curr)
   319  		}
   320  	}
   321  
   322  	return result[:]
   323  }
   324  
   325  func convertAccountOperationPrevNextList(list []*AccountOperationPrevNext) []AccountOperationPrevNext {
   326  	var result []AccountOperationPrevNext
   327  	for _, curr := range list {
   328  		if curr != nil {
   329  			result = append(result, *curr)
   330  		}
   331  	}
   332  
   333  	return result[:]
   334  }
   335  
   336  // ErrInvalidAccountOperation perform oerpation within a db transaction
   337  func txApppendAccountOperation(db database.Context, operation model.AccountOperation) (model.AccountOperation, error) {
   338  	if db == nil {
   339  		return model.AccountOperation{}, database.ErrInvalidDatabase
   340  	}
   341  	gdb := db.DB().(*gorm.DB)
   342  
   343  	if operation.OperationType != model.OperationTypeInit {
   344  
   345  		info, err := fetchAccountInfo(db, operation.AccountID)
   346  		if err != nil {
   347  			return model.AccountOperation{}, err
   348  		}
   349  		prepareNextOperation(&info, &operation)
   350  	}
   351  
   352  	// pre-check operation with newupdated values
   353  	if !operation.PreCheck() {
   354  		return model.AccountOperation{}, ErrInvalidAccountOperation
   355  	}
   356  
   357  	// store operation
   358  	err := gdb.Create(&operation).Error
   359  	if err != nil {
   360  		return model.AccountOperation{}, err
   361  	}
   362  	// check if operation is valid
   363  	if !operation.IsValid() {
   364  		return model.AccountOperation{}, ErrInvalidAccountOperation
   365  	}
   366  
   367  	return operation, nil
   368  }
   369  
   370  func fetchAccountInfo(db database.Context, accountID model.AccountID) (AccountInfo, error) {
   371  	// check for valid accountID
   372  	if accountID == 0 {
   373  		return AccountInfo{}, ErrInvalidAccountID
   374  	}
   375  
   376  	// get Account (for currency)
   377  	account, err := GetAccountByID(db, accountID)
   378  	if err != nil {
   379  		return AccountInfo{}, ErrAccountNotFound
   380  	}
   381  
   382  	// check currency status
   383  	curr, err := GetCurrencyByName(db, account.CurrencyName)
   384  	if err != nil {
   385  		return AccountInfo{}, ErrCurrencyNotFound
   386  	}
   387  	if !curr.IsAvailable() {
   388  		return AccountInfo{}, ErrCurrencyNotAvailable
   389  	}
   390  
   391  	// check account status
   392  	accountState, err := GetAccountStatusByAccountID(db, accountID)
   393  	if err != nil {
   394  		return AccountInfo{}, ErrAccountStateNotFound
   395  	}
   396  	if !accountState.State.Valid() {
   397  		return AccountInfo{}, ErrInvalidAccountState
   398  	}
   399  	if accountState.State != model.AccountStatusNormal {
   400  		return AccountInfo{}, ErrAccountIsDisabled
   401  	}
   402  
   403  	// fetch last operation
   404  	lastOperation, err := GetLastAccountOperation(db, accountID)
   405  	if err != nil && err != gorm.ErrRecordNotFound {
   406  		return AccountInfo{}, err
   407  	}
   408  
   409  	return AccountInfo{
   410  		Account:  account,
   411  		Currency: curr,
   412  		State:    accountState,
   413  		Last:     lastOperation,
   414  	}, nil
   415  }
   416  
   417  type AccountInfo struct {
   418  	Account  model.Account
   419  	Currency model.Currency
   420  	State    model.AccountState
   421  	Last     model.AccountOperation
   422  }
   423  
   424  func prepareNextOperation(info *AccountInfo, operation *model.AccountOperation) {
   425  	// compute Balance with last operation and new Amount
   426  	*operation.Balance = *operation.Amount
   427  	if info.Last.Balance != nil {
   428  		*operation.Balance += *info.Last.Balance
   429  	}
   430  
   431  	// compute TotalLocked with last operation and new LockAmount
   432  	*operation.TotalLocked = *operation.LockAmount
   433  	if info.Last.TotalLocked != nil {
   434  		*operation.TotalLocked += *info.Last.TotalLocked
   435  	}
   436  
   437  	// To fixed precision
   438  	*operation.Amount = model.ToFixedFloat(*operation.Amount)
   439  	*operation.Balance = model.ToFixedFloat(*operation.Balance)
   440  
   441  	*operation.LockAmount = model.ToFixedFloat(*operation.LockAmount)
   442  	*operation.TotalLocked = model.ToFixedFloat(*operation.TotalLocked)
   443  }