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 }