Skip to content

Commit

Permalink
#5 transactional h2 slick repo submitted
Browse files Browse the repository at this point in the history
  • Loading branch information
bearmug committed Aug 13, 2017
1 parent 6ccdf6d commit 7172f3e
Show file tree
Hide file tree
Showing 6 changed files with 123 additions and 90 deletions.
20 changes: 11 additions & 9 deletions src/main/scala/org/bearmug/transfer/model/Account.scala
Original file line number Diff line number Diff line change
Expand Up @@ -7,30 +7,32 @@ package org.bearmug.transfer.model
* @param id account identifier
* @param balance current account balance
*/
sealed case class Account protected(id: Option[Int], owner: String, balance: Long) {
sealed case class Account (owner: String, balance: Long, id: Option[Int] = None) {

def transferIn(amount: Long): Account = {
private[model] def transferIn(amount: Long): Account = {
require(amount >= 0, s"amount to transfer should be >= to zero, its current value: $amount")
Account(id, owner, balance + amount)
Account(owner, balance + amount, id)
}

def transferOut(amount: Long): Account = {
private[model] def transferOut(amount: Long): Account = {
require(amount >= 0, s"amount to transfer should be >= to zero, its current value: $amount")
require(balance >= amount, s"insufficient funds, current balance $balance, amount to transfer out $amount")
Account(id, owner, balance - amount)
Account(owner, balance - amount, id)
}
}

object EmptyAccount extends Account(None, "", -1)
object EmptyAccount extends Account("", -1)

/**
* Companion object to create pre-validated new account only
* Companion object to create pre-validated new account only. Renamed from companion object to prevent clashes with
* apply/unapply logic, used from slick repo.
*/
object Account {
object AccountFactory {

def createNew(owner: String, initialBalance: Long): Account = {
require(owner.trim.nonEmpty, "owner name can not be empty")
require(initialBalance >= 0, s"initial balance $initialBalance can not be less than zero")
Account(None, owner, initialBalance)
Account(owner, initialBalance)
}

def transfer(from: Account, to: Account,amount: Long): (Account, Account) = {
Expand Down
20 changes: 15 additions & 5 deletions src/main/scala/org/bearmug/transfer/repo/AccountRepo.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package org.bearmug.transfer.repo

import org.bearmug.transfer.model.Account

import scala.concurrent.Future

/**
* Account repository abstraction, representing persistence layer.
*/
Expand All @@ -11,35 +13,43 @@ trait AccountRepo {
* Available accounts listing. Operation with no side-effects for storage.
* @return persisted accounts list
*/
def list(): List[Account]
def list(): Future[List[Account]]

/**
* Create account with unique identity. No uniqueness content validation assumed.
* @param account account content to create
* @return None if account with assigned identity already exists or Some with actually created accounts with
* real accounts id
*/
def create(account: Account): Option[Account]
def create(account: Account): Future[Option[Account]]

/**
* Lookup account by account id
* @param id account identity to lookup by
* @return None if there are no such account or account under maintenance (mutual update) or Some with actual
* account content
*/
def find(id: Int): Option[Account]
def find(id: Int): Future[Option[Account]]

/**
* Updating account by specific identity
* @param account account to update, including identity to find it
* @return None if account under maintenance (mutual update) or Some with updated account content
*/
def update(account: Account): Option[Account]
def update(account: Account): Future[Option[Account]]

/**
* Purges specific account with specific identity from the repo.
* @param id account identity to cleanup to
* @return None if account under update or account updated recently or Some with purged account content
*/
def delete(id: Int): Option[Account]
def delete(id: Int): Future[Option[Account]]

/**
* Accounts money transfer stuff
* @param idFrom id to transfer money from
* @param idTo id to transfer money to
* @param amount amount to transfer
*/
def transfer(idFrom: Int, idTo: Int, amount: Long): Future[Option[(Account, Account)]]
}
54 changes: 0 additions & 54 deletions src/main/scala/org/bearmug/transfer/repo/AccountRepoMap.scala

This file was deleted.

85 changes: 77 additions & 8 deletions src/main/scala/org/bearmug/transfer/repo/AccountRepoSlick.scala
Original file line number Diff line number Diff line change
@@ -1,16 +1,85 @@
package org.bearmug.transfer.repo
import org.bearmug.transfer.model.Account
import slick.lifted.TableQuery

class AccountRepoSlick extends AccountRepo {
import org.bearmug.transfer.model.{Account, AccountFactory}
import slick.jdbc.JdbcProfile

override def list(): List[Account] = ???
import scala.concurrent.Future
import scala.concurrent.ExecutionContext.Implicits.global

override def create(account: Account): Option[Account] = ???
class AccountRepoSlick extends AccountRepo with AccountTable with H2Config {

override def find(id: Int): Option[Account] = ???
import driver.api._

override def update(account: Account): Option[Account] = ???
override def list(): Future[List[Account]] = db.run(accounts.result).map(_.toList)

override def delete(id: Int): Option[Account] = ???
override def create(account: Account): Future[Option[Account]] = db.run(accountsId += account).flatMap(find)

override def find(id: Int): Future[Option[Account]] = db.run(accounts.filter(_.id === id).result.headOption)

override def update(account: Account): Future[Option[Account]] =
db.run(accounts.filter(_.id === account.id).update(account)).flatMap(_ => find(account.id.head))

override def delete(id: Int): Future[Option[Account]] = for {
acct <- find(id)
removed <- db.run(accounts.filter(_.id === id).delete)
} yield (acct, removed) match {
case (_, 0) => None
case (res@Some(_), _) => res
case _ => None
}

def transferTransactionally(fromAcc: Option[Account], toAcc: Option[Account], amount: Long): Option[(Account, Account)] =
(fromAcc, toAcc) match {
case (Some(acc1), Some(acc2)) =>
val updatedAccounts = AccountFactory.transfer(acc1, acc2, amount)
for {
upd <- db.run {
DBIO.seq {
accounts.filter(_.id === updatedAccounts._1.id).update(updatedAccounts._1)
accounts.filter(_.id === updatedAccounts._2.id).update(updatedAccounts._2)
}.transactionally
}
fromPersisted <- find(acc1.id.head)
toPersisted <- find(acc2.id.head)
} yield (fromPersisted, toPersisted) match {
case (Some(resFrom), Some(resTo)) => Some(resFrom, resTo)
case _ => None
}
None
case _ => None
}

override def transfer(idFrom: Int, idTo: Int, amount: Long): Future[Option[(Account, Account)]] = for {
f <- find(idFrom)
t <- find(idTo)
} yield transferTransactionally(f, t, amount)
}

trait AccountTable {
this: DbConfig =>

import driver.api._

class Accounts(tag: Tag) extends Table[Account](tag, "ACCOUNTS") {
// columns definition
val id = column[Int]("ACCOUNT_ID", O.PrimaryKey, O.AutoInc)
val owner = column[String]("ACCOUNT_OWNER", O.Length(256))
val balance = column[Long]("ACCOUNT_BALANCE")

// selection definition
def * = (owner, balance, id.?) <> (Account.tupled, Account.unapply)
}

val accounts = TableQuery[Accounts](new Accounts(_))
val accountsId = accounts returning TableQuery[Accounts](new Accounts(_)).map(_.id)
}

trait DbConfig {
val driver: JdbcProfile
val db: driver.api.Database
}

trait H2Config extends DbConfig {
val driver = slick.jdbc.H2Profile
val db = driver.api.Database.forURL("jdbc:h2:mem:test1;DB_CLOSE_DELAY=-1", driver = "org.h2.Driver")
}
24 changes: 12 additions & 12 deletions src/test/scala/org/bearmug/transfer/model/AccountSuite.scala
Original file line number Diff line number Diff line change
Expand Up @@ -8,64 +8,64 @@ import org.scalatest.junit.JUnitRunner
class AccountSuite extends FunSuite {

test("account initiated with zero identity and proper owner/balance") {
val account = Account.createNew("owner",10)
val account = AccountFactory.createNew("owner",10)
assert(account.id.isEmpty)
assert(account.balance == 10)
assert(account.owner == "owner")
}

test("new account owner name can not be empty") {
val e = intercept[IllegalArgumentException] {
Account.createNew("", 10)
AccountFactory.createNew("", 10)
}
assert(e.getMessage == "requirement failed: owner name can not be empty")
}

test("new account owner name can not be empty after trim") {
val e = intercept[IllegalArgumentException] {
Account.createNew(" ", 10)
AccountFactory.createNew(" ", 10)
}
assert(e.getMessage == "requirement failed: owner name can not be empty")
}

test("new account initialAmount can not be < 0") {
val e = intercept[IllegalArgumentException] {
Account.createNew("owner", -10)
AccountFactory.createNew("owner", -10)
}
assert(e.getMessage == "requirement failed: initial balance -10 can not be less than zero")
}

test("overdraft is not allowed for account outgoing transfer") {
val e = intercept[IllegalArgumentException] {
Account.createNew("owner", 10).transferOut(100)
AccountFactory.createNew("owner", 10).transferOut(100)
}
assert(e.getMessage == "requirement failed: insufficient funds, current balance 10, amount to transfer out 100")
}

test("account transfer out is OK") {
val account = Account.createNew("owner", 10).transferOut(10)
val account = AccountFactory.createNew("owner", 10).transferOut(10)
assert(account.balance == 0)
assert(account.owner == "owner")
}

test("account transfer in is OK") {
val account = Account.createNew("owner", 10).transferIn(10)
val account = AccountFactory.createNew("owner", 10).transferIn(10)
assert(account.balance == 20)
assert(account.owner == "owner")
}

test("funds transfer inside account is unavailable") {
val e = intercept[IllegalArgumentException] {
val account = Account.createNew("owner", 10)
Account.transfer(account, account, 1)
val account = AccountFactory.createNew("owner", 10)
AccountFactory.transfer(account, account, 1)
}
assert(e.getMessage == "requirement failed: funds transfer inside account is unavailable")
}

test("funds transfer between two accounts is ok") {
val accountFrom = Account.createNew("ownerFrom", 10)
val accountTo = Account.createNew("ownerTo", 10)
Account.transfer(accountFrom, accountTo, 1) match {
val accountFrom = AccountFactory.createNew("ownerFrom", 10)
val accountTo = AccountFactory.createNew("ownerTo", 10)
AccountFactory.transfer(accountFrom, accountTo, 1) match {
case (from, to) =>
assert(from.owner == "ownerFrom")
assert(from.balance == 9)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
package org.bearmug.transfer.repo

import org.scalatest.FunSuite
import org.scalatest.{BeforeAndAfter, FunSuite}

class AccountRepoMapSuite extends FunSuite {
class AccountRepoSlickSuite extends FunSuite with BeforeAndAfter {

var repo: AccountRepo = _

before {
repo = new AccountRepoSlick()
}

test("new account listed after creation") {}
test("account lookup works fine for added account") {}
Expand Down

0 comments on commit 7172f3e

Please sign in to comment.