diff --git a/src/main/scala/bootstrap/liftweb/Boot.scala b/src/main/scala/bootstrap/liftweb/Boot.scala index 70507e524..74e0fc8ce 100755 --- a/src/main/scala/bootstrap/liftweb/Boot.scala +++ b/src/main/scala/bootstrap/liftweb/Boot.scala @@ -32,6 +32,13 @@ Berlin 13359, Germany package bootstrap.liftweb import code.api.sandbox.SandboxApiCalls +import code.metadata.comments.MappedComment +import code.metadata.counterparties.{MappedCounterpartyWhereTag, MappedCounterpartyMetadata} +import code.metadata.narrative.MappedNarrative +import code.metadata.tags.MappedTag +import code.metadata.transactionimages.MappedTransactionImage +import code.metadata.wheretags.MappedWhereTag +import code.metrics.MappedMetric import code.bankbranches.{MappedBankBranch, MappedDataLicense} import code.customerinfo.{MappedCustomerMessage, MappedCustomerInfo} import code.tesobe.{ImporterAPI, CashAccountAPI} @@ -48,8 +55,8 @@ import net.liftweb.util.Helpers import java.io.FileInputStream import java.io.File import javax.mail.internet.MimeMessage -import code.model.{Nonce, Consumer, Token, dataAccess} -import dataAccess._ +import code.model._ +import code.model.dataAccess._ import code.api._ import code.snippet.{OAuthAuthorisation, OAuthWorkedThanks} @@ -326,6 +333,9 @@ class Boot extends Loggable{ object ToSchemify { val models = List(OBPUser, Admin, Nonce, Token, Consumer, ViewPrivileges, ViewImpl, APIUser, MappedAccountHolder, - MappedCustomerInfo, MappedCustomerMessage, + MappedComment, MappedNarrative, MappedTag, + MappedTransactionImage, MappedWhereTag, MappedCounterpartyMetadata, + MappedCounterpartyWhereTag, MappedBank, MappedBankAccount, MappedTransaction, + MappedMetric, MappedCustomerInfo, MappedCustomerMessage, MappedBankBranch, MappedDataLicense) } diff --git a/src/main/scala/code/bankconnectors/Connector.scala b/src/main/scala/code/bankconnectors/Connector.scala index ef8ba399f..dc8872474 100644 --- a/src/main/scala/code/bankconnectors/Connector.scala +++ b/src/main/scala/code/bankconnectors/Connector.scala @@ -18,7 +18,7 @@ object Connector extends SimpleInjector { val connector = new Inject(buildOne _) {} - def buildOne: Connector = LocalConnector + def buildOne: Connector = LocalMappedConnector } diff --git a/src/main/scala/code/bankconnectors/LocalMappedConnector.scala b/src/main/scala/code/bankconnectors/LocalMappedConnector.scala new file mode 100644 index 000000000..92b268930 --- /dev/null +++ b/src/main/scala/code/bankconnectors/LocalMappedConnector.scala @@ -0,0 +1,190 @@ +package code.bankconnectors + +import code.metadata.counterparties.Counterparties +import code.model._ +import code.model.dataAccess.{UpdatesRequestSender, MappedBankAccount, MappedAccountHolder, MappedBank} +import code.util.Helper +import com.tesobe.model.UpdateBankAccount +import net.liftweb.common.{Loggable, Full, Box} +import net.liftweb.mapper._ +import net.liftweb.util.Helpers._ +import net.liftweb.util.Props + +import scala.concurrent.ops._ + +object LocalMappedConnector extends Connector { + + type AccountType = MappedBankAccount + + //gets a particular bank handled by this connector + override def getBank(bankId: BankId): Box[Bank] = + getMappedBank(bankId) + + private def getMappedBank(bankId: BankId): Box[MappedBank] = + MappedBank.find(By(MappedBank.permalink, bankId.value)) + + //gets banks handled by this connector + override def getBanks: List[Bank] = + MappedBank.findAll + + override def getTransaction(bankId: BankId, accountID: AccountId, transactionId: TransactionId): Box[Transaction] = { + + updateAccountTransactions(bankId, accountID) + + MappedTransaction.find( + By(MappedTransaction.bank, bankId.value), + By(MappedTransaction.account, accountID.value), + By(MappedTransaction.transactionId, transactionId.value)).flatMap(_.toTransaction) + } + + override def getTransactions(bankId: BankId, accountID: AccountId, queryParams: OBPQueryParam*): Box[List[Transaction]] = { + val limit = queryParams.collect { case OBPLimit(value) => MaxRows[MappedTransaction](value) }.headOption + val offset = queryParams.collect { case OBPOffset(value) => StartAt[MappedTransaction](value) }.headOption + val fromDate = queryParams.collect { case OBPFromDate(date) => By_>=(MappedTransaction.tFinishDate, date) }.headOption + val toDate = queryParams.collect { case OBPToDate(date) => By_<=(MappedTransaction.tFinishDate, date) }.headOption + val ordering = queryParams.collect { + //we don't care about the intended sort field and only sort on finish date for now + case OBPOrdering(_, direction) => + direction match { + case OBPAscending => OrderBy(MappedTransaction.tFinishDate, Ascending) + case OBPDescending => OrderBy(MappedTransaction.tFinishDate, Descending) + } + } + + val optionalParams : Seq[QueryParam[MappedTransaction]] = Seq(limit.toSeq, offset.toSeq, fromDate.toSeq, toDate.toSeq, ordering.toSeq).flatten + val mapperParams = Seq(By(MappedTransaction.bank, bankId.value), By(MappedTransaction.account, accountID.value)) ++ optionalParams + + val mappedTransactions = MappedTransaction.findAll(mapperParams: _*) + + updateAccountTransactions(bankId, accountID) + + for (account <- getBankAccount(bankId, accountID)) + yield mappedTransactions.flatMap(_.toTransaction(account)) + } + + /** + * + * refreshes transactions via hbci if the transaction info is sourced from hbci + * + * Checks if the last update of the account was made more than one hour ago. + * if it is the case we put a message in the message queue to ask for + * transactions updates + * + * It will be used each time we fetch transactions from the DB. But the test + * is performed in a different thread. + */ + private def updateAccountTransactions(bankId : BankId, accountId : AccountId) = { + + for { + bank <- getMappedBank(bankId) + account <- getBankAccountType(bankId, accountId) + } { + spawn{ + val useMessageQueue = Props.getBool("messageQueue.updateBankAccountsTransaction", false) + val outDatedTransactions = now after time(account.lastUpdate.get.getTime + hours(1)) + if(outDatedTransactions && useMessageQueue) { + UpdatesRequestSender.sendMsg(UpdateBankAccount(account.accountNumber.get, bank.national_identifier.get)) + } + } + } + } + + override def getBankAccountType(bankId: BankId, accountId: AccountId): Box[MappedBankAccount] = { + MappedBankAccount.find( + By(MappedBankAccount.bank, bankId.value), + By(MappedBankAccount.theAccountId, accountId.value)) + } + + //gets the users who are the legal owners/holders of the account + override def getAccountHolders(bankId: BankId, accountID: AccountId): Set[User] = + MappedAccountHolder.findAll( + By(MappedAccountHolder.accountBankPermalink, bankId.value), + By(MappedAccountHolder.accountPermalink, accountID.value)).map(accHolder => accHolder.user.obj).flatten.toSet + + + def getOtherBankAccount(thisAccountBankId : BankId, thisAccountId : AccountId, metadata : OtherBankAccountMetadata) : Box[OtherBankAccount] = { + //because we don't have a db backed model for OtherBankAccounts, we need to construct it from an + //OtherBankAccountMetadata and a transaction + for { //find a transaction with this counterparty + t <- MappedTransaction.find( + By(MappedTransaction.bank, thisAccountBankId.value), + By(MappedTransaction.account, thisAccountId.value), + By(MappedTransaction.counterpartyAccountHolder, metadata.getHolder), + By(MappedTransaction.counterpartyAccountNumber, metadata.getAccountNumber)) + } yield { + new OtherBankAccount( + //counterparty id is defined to be the id of its metadata as we don't actually have an id for the counterparty itself + id = metadata.metadataId, + label = metadata.getHolder, + nationalIdentifier = t.counterpartyNationalId.get, + swift_bic = None, + iban = t.getCounterpartyIban(), + number = metadata.getAccountNumber, + bankName = t.counterpartyBankName.get, + kind = t.counterpartyAccountKind.get, + originalPartyBankId = thisAccountBankId, + originalPartyAccountId = thisAccountId, + alreadyFoundMetadata = Some(metadata) + ) + } + } + + override def getOtherBankAccounts(bankId: BankId, accountID: AccountId): List[OtherBankAccount] = + Counterparties.counterparties.vend.getMetadatas(bankId, accountID).flatMap(getOtherBankAccount(bankId, accountID, _)) + + override def getOtherBankAccount(bankId: BankId, accountID: AccountId, otherAccountID: String): Box[OtherBankAccount] = + Counterparties.counterparties.vend.getMetadata(bankId, accountID, otherAccountID).flatMap(getOtherBankAccount(bankId, accountID, _)) + + override def getPhysicalCards(user: User): Set[PhysicalCard] = + Set.empty + + override def getPhysicalCardsForBank(bankId: BankId, user: User): Set[PhysicalCard] = + Set.empty + + + override def makePaymentImpl(fromAccount: MappedBankAccount, toAccount: MappedBankAccount, amt: BigDecimal): Box[TransactionId] = { + val fromTransAmt = -amt //from account balance should decrease + val toTransAmt = amt //to account balance should increase + + //we need to save a copy of this payment as a transaction in each of the accounts involved, with opposite amounts + val sentTransactionId = saveTransaction(fromAccount, toAccount, fromTransAmt) + saveTransaction(toAccount, fromAccount, toTransAmt) + + sentTransactionId + } + + /** + * Saves a transaction with amount @amt and counterparty @counterparty for account @account. Returns the id + * of the saved transaction. + */ + private def saveTransaction(account : MappedBankAccount, counterparty : BankAccount, amt : BigDecimal) : Box[TransactionId] = { + + val transactionTime = now + val currency = account.currency + + + //update the balance of the account for which a transaction is being created + val newAccountBalance : Long = account.accountBalance.get + Helper.convertToSmallestCurrencyUnits(amt, account.currency) + account.accountBalance(newAccountBalance).save() + + + val mappedTransaction = MappedTransaction.create + .bank(account.bankId.value) + .account(account.accountId.value) + .transactionType("sandbox-payment") + .amount(Helper.convertToSmallestCurrencyUnits(amt, currency)) + .newAccountBalance(newAccountBalance) + .currency(currency) + .tStartDate(transactionTime) + .tFinishDate(transactionTime) + .description("") + .counterpartyAccountHolder(counterparty.accountHolder) + .counterpartyAccountNumber(counterparty.number) + .counterpartyAccountKind(counterparty.accountType) + .counterpartyBankName(counterparty.bankName) + .counterpartyIban(counterparty.iban.getOrElse("")) + .counterpartyNationalId(counterparty.nationalIdentifier).saveMe + + Full(mappedTransaction.theTransactionId) + } +} diff --git a/src/main/scala/code/metadata/comments/Comments.scala b/src/main/scala/code/metadata/comments/Comments.scala index 2c6463c01..4d4cf2a7b 100644 --- a/src/main/scala/code/metadata/comments/Comments.scala +++ b/src/main/scala/code/metadata/comments/Comments.scala @@ -9,7 +9,7 @@ object Comments extends SimpleInjector { val comments = new Inject(buildOne _) {} - def buildOne: Comments = MongoTransactionComments + def buildOne: Comments = MappedComments } @@ -17,6 +17,7 @@ trait Comments { def getComments(bankId : BankId, accountId : AccountId, transactionId : TransactionId)(viewId : ViewId) : List[Comment] def addComment(bankId : BankId, accountId : AccountId, transactionId: TransactionId)(userId: UserId, viewId : ViewId, text : String, datePosted : Date) : Box[Comment] + //TODO: should commentId be unique among all comments, removing the need for the other parameters? def deleteComment(bankId : BankId, accountId : AccountId, transactionId: TransactionId)(commentId : String) : Box[Unit] } \ No newline at end of file diff --git a/src/main/scala/code/metadata/comments/MappedComment.scala b/src/main/scala/code/metadata/comments/MappedComment.scala new file mode 100644 index 000000000..5aa3c4e9d --- /dev/null +++ b/src/main/scala/code/metadata/comments/MappedComment.scala @@ -0,0 +1,80 @@ +package code.metadata.comments + +import java.util.{UUID, Date} + +import code.model._ +import code.model.dataAccess.APIUser +import code.util.MappedUUID +import net.liftweb.common.{Failure, Full, Box} +import net.liftweb.mapper._ +import net.liftweb.util.Helpers.tryo + +private object MappedComments extends Comments { + override def getComments(bankId: BankId, accountId: AccountId, transactionId: TransactionId)(viewId: ViewId): List[Comment] = { + MappedComment.findAll( + By(MappedComment.bank, bankId.value), + By(MappedComment.account, accountId.value), + By(MappedComment.transaction, transactionId.value), + By(MappedComment.view, viewId.value)) + } + + override def deleteComment(bankId: BankId, accountId: AccountId, transactionId: TransactionId)(commentId: String): Box[Unit] = { + val deleted = for { + comment <- MappedComment.find(By(MappedComment.bank, bankId.value), + By(MappedComment.account, accountId.value), + By(MappedComment.transaction, transactionId.value), + By(MappedComment.apiId, commentId)) + } yield comment.delete_! + + deleted match { + case Full(true) => Full(Unit) + case _ => Failure("Could not delete comment") + } + } + + override def addComment(bankId: BankId, accountId: AccountId, transactionId: TransactionId)(userId: UserId, viewId: ViewId, text: String, datePosted: Date): Box[Comment] = { + tryo { + MappedComment.create + .bank(bankId.value) + .account(accountId.value) + .transaction(transactionId.value) + .poster(userId.value) + .view(viewId.value) + .text_(text) + .date(datePosted).saveMe + } + } +} + +class MappedComment extends Comment with LongKeyedMapper[MappedComment] with IdPK with CreatedUpdated { + + def getSingleton = MappedComment + + object apiId extends MappedUUID(this) + + object text_ extends MappedText(this) { + override def defaultValue = "" + } + object poster extends MappedLongForeignKey(this, APIUser) + object replyTo extends MappedUUID(this) { + override def defaultValue = "" + } + + object view extends MappedString(this, 255) + object date extends MappedDateTime(this) + + object bank extends MappedString(this, 255) + object account extends MappedString(this, 255) + object transaction extends MappedString(this, 255) + + override def id_ : String = apiId.get + override def text: String = text_.get + override def postedBy: Box[User] = poster.obj + override def replyToID: String = replyTo.get + override def viewId: ViewId = ViewId(view.get) + override def datePosted: Date = date.get +} + +object MappedComment extends MappedComment with LongKeyedMetaMapper[MappedComment] { + override def dbIndexes = UniqueIndex(apiId) :: Index(view, bank, account, transaction) :: super.dbIndexes +} diff --git a/src/main/scala/code/metadata/counterparties/Counterparties.scala b/src/main/scala/code/metadata/counterparties/Counterparties.scala index 1b40ad681..edada886d 100644 --- a/src/main/scala/code/metadata/counterparties/Counterparties.scala +++ b/src/main/scala/code/metadata/counterparties/Counterparties.scala @@ -1,5 +1,6 @@ package code.metadata.counterparties +import net.liftweb.common.Box import net.liftweb.util.SimpleInjector import code.model.{AccountId, BankId, OtherBankAccountMetadata, OtherBankAccount} @@ -7,7 +8,7 @@ object Counterparties extends SimpleInjector { val counterparties = new Inject(buildOne _) {} - def buildOne: Counterparties = MongoCounterparties + def buildOne: Counterparties = MapperCounterparties } @@ -17,4 +18,6 @@ trait Counterparties { //get all counterparty metadatas for a single OBP account def getMetadatas(originalPartyBankId: BankId, originalPartyAccountId : AccountId) : List[OtherBankAccountMetadata] + + def getMetadata(originalPartyBankId: BankId, originalPartyAccountId : AccountId, counterpartyMetadataId : String) : Box[OtherBankAccountMetadata] } \ No newline at end of file diff --git a/src/main/scala/code/metadata/counterparties/MapperCounterparties.scala b/src/main/scala/code/metadata/counterparties/MapperCounterparties.scala new file mode 100644 index 000000000..1fb45924e --- /dev/null +++ b/src/main/scala/code/metadata/counterparties/MapperCounterparties.scala @@ -0,0 +1,218 @@ +package code.metadata.counterparties + +import java.util.Date + +import code.model._ +import code.model.dataAccess.APIUser +import code.util.MappedUUID +import net.liftweb.common.{Box, Full} +import net.liftweb.mapper._ +import net.liftweb.util.Helpers.tryo + +object MapperCounterparties extends Counterparties { + override def getOrCreateMetadata(originalPartyBankId: BankId, originalPartyAccountId: AccountId, otherParty: OtherBankAccount): OtherBankAccountMetadata = { + + /** + * Generates a new alias name that is guaranteed not to collide with any existing public alias names + * for the account in question + */ + def newPublicAliasName(): String = { + import scala.util.Random + val firstAliasAttempt = "ALIAS_" + Random.nextLong().toString.take(6) + + /** + * Returns true if @publicAlias is already the name of a public alias within @account + */ + def isDuplicate(publicAlias: String) : Boolean = { + MappedCounterpartyMetadata.find( + By(MappedCounterpartyMetadata.thisAccountBankId, originalPartyBankId.value), + By(MappedCounterpartyMetadata.thisAccountId, originalPartyAccountId.value), + By(MappedCounterpartyMetadata.publicAlias, publicAlias) + ).isDefined + } + + /** + * Appends things to @publicAlias until it a unique public alias name within @account + */ + def appendUntilUnique(publicAlias: String): String = { + val newAlias = publicAlias + Random.nextLong().toString.take(1) + if (isDuplicate(newAlias)) appendUntilUnique(newAlias) + else newAlias + } + + if (isDuplicate(firstAliasAttempt)) appendUntilUnique(firstAliasAttempt) + else firstAliasAttempt + } + + + //can't find by MappedCounterpartyMetadata.counterpartyId = otherParty.id because in this implementation + //if the metadata doesn't exist, the id field of ther OtherBankAccount is not known yet, and will be empty + def findMappedCounterpartyMetadata(originalPartyBankId: BankId, originalPartyAccountId: AccountId, + otherParty: OtherBankAccount) : Box[MappedCounterpartyMetadata] = { + MappedCounterpartyMetadata.find( + By(MappedCounterpartyMetadata.thisAccountBankId, originalPartyBankId.value), + By(MappedCounterpartyMetadata.thisAccountId, originalPartyAccountId.value), + By(MappedCounterpartyMetadata.holder, otherParty.label), + By(MappedCounterpartyMetadata.accountNumber, otherParty.number)) + } + + val existing = findMappedCounterpartyMetadata(originalPartyBankId, originalPartyAccountId, otherParty) + + existing match { + case Full(e) => e + case _ => MappedCounterpartyMetadata.create + .thisAccountBankId(originalPartyBankId.value) + .thisAccountId(originalPartyAccountId.value) + .holder(otherParty.label) + .publicAlias(newPublicAliasName()) + .accountNumber(otherParty.number).saveMe + } + } + + //get all counterparty metadatas for a single OBP account + override def getMetadatas(originalPartyBankId: BankId, originalPartyAccountId: AccountId): List[OtherBankAccountMetadata] = { + MappedCounterpartyMetadata.findAll( + By(MappedCounterpartyMetadata.thisAccountBankId, originalPartyBankId.value), + By(MappedCounterpartyMetadata.thisAccountId, originalPartyAccountId.value) + ) + } + + override def getMetadata(originalPartyBankId: BankId, originalPartyAccountId: AccountId, counterpartyMetadataId: String): Box[OtherBankAccountMetadata] = { + /** + * This particular implementation requires the metadata id to be the same as the otherParty (OtherBankAccount) id + */ + MappedCounterpartyMetadata.find( + By(MappedCounterpartyMetadata.thisAccountBankId, originalPartyBankId.value), + By(MappedCounterpartyMetadata.thisAccountId, originalPartyAccountId.value), + By(MappedCounterpartyMetadata.counterpartyId, counterpartyMetadataId) + ) + } +} + +class MappedCounterpartyMetadata extends OtherBankAccountMetadata with LongKeyedMapper[MappedCounterpartyMetadata] with IdPK with CreatedUpdated { + override def getSingleton = MappedCounterpartyMetadata + + object counterpartyId extends MappedUUID(this) + + //these define the obp account to which this counterparty belongs + object thisAccountBankId extends MappedString(this, 255) + object thisAccountId extends MappedString(this, 255) + + //these define the counterparty + object holder extends MappedString(this, 255) + object accountNumber extends MappedString(this, 100) + + //this is the counterparty's metadata + object publicAlias extends MappedString(this, 100) + object privateAlias extends MappedString(this, 100) + object moreInfo extends MappedString(this, 100) + object url extends MappedString(this, 100) + object imageUrl extends MappedString(this, 100) + object openCorporatesUrl extends MappedString(this, 100) + + object physicalLocation extends MappedLongForeignKey(this, MappedCounterpartyWhereTag) + object corporateLocation extends MappedLongForeignKey(this, MappedCounterpartyWhereTag) + + /** + * Evaluates f, and then attempts to save. If no exceptions are thrown and save executes successfully, + * true is returned. If an exception is thrown or if the save fails, false is returned. + * @param f the expression to evaluate (e.g. imageUrl("http://example.com/foo.png") + * @return If saving the model worked after having evaluated f + */ + private def trySave(f : => Any) : Boolean = + tryo{ + f + save() + }.getOrElse(false) + + private def setWhere(whereTag : Box[MappedCounterpartyWhereTag]) + (userId: UserId, datePosted : Date, longitude : Double, latitude : Double) : Box[MappedCounterpartyWhereTag] = { + val toUpdate = whereTag match { + case Full(c) => c + case _ => MappedCounterpartyWhereTag.create + } + + tryo{ + toUpdate + .user(userId.value) + .date(datePosted) + .geoLongitude(longitude) + .geoLatitude(latitude) + .saveMe + } + } + + def setCorporateLocation(userId: UserId, datePosted : Date, longitude : Double, latitude : Double) : Boolean = { + //save where tag + val savedWhere = setWhere(corporateLocation.obj)(userId, datePosted, longitude, latitude) + //set where tag for counterparty + savedWhere.map(location => trySave{corporateLocation(location)}).getOrElse(false) + } + + def setPhysicalLocation(userId: UserId, datePosted : Date, longitude : Double, latitude : Double) : Boolean = { + //save where tag + val savedWhere = setWhere(physicalLocation.obj)(userId, datePosted, longitude, latitude) + //set where tag for counterparty + savedWhere.map(location => trySave{physicalLocation(location)}).getOrElse(false) + } + + override def metadataId: String = counterpartyId.get + override def getAccountNumber: String = accountNumber.get + override def getHolder: String = holder.get + override def getPublicAlias: String = publicAlias.get + override def getCorporateLocation: Option[GeoTag] = + corporateLocation.obj + override def getOpenCorporatesURL: String = openCorporatesUrl.get + override def getMoreInfo: String = moreInfo.get + override def getPrivateAlias: String = privateAlias.get + override def getImageURL: String = imageUrl.get + override def getPhysicalLocation: Option[GeoTag] = + physicalLocation.obj + override def getUrl: String = url.get + + override val addPhysicalLocation: (UserId, Date, Double, Double) => Boolean = setPhysicalLocation _ + override val addCorporateLocation: (UserId, Date, Double, Double) => Boolean = setCorporateLocation _ + override val addPrivateAlias: (String) => Boolean = (x) => + trySave{privateAlias(x)} + override val addURL: (String) => Boolean = (x) => + trySave{url(x)} + override val addMoreInfo: (String) => Boolean = (x) => + trySave{moreInfo(x)} + override val addPublicAlias: (String) => Boolean = (x) => + trySave{publicAlias(x)} + override val addOpenCorporatesURL: (String) => Boolean = (x) => + trySave{openCorporatesUrl(x)} + override val addImageURL: (String) => Boolean = (x) => + trySave{imageUrl(x)} + override val deleteCorporateLocation = () => + corporateLocation.obj.map(_.delete_!).getOrElse(false) + override val deletePhysicalLocation = () => + physicalLocation.obj.map(_.delete_!).getOrElse(false) + +} + +object MappedCounterpartyMetadata extends MappedCounterpartyMetadata with LongKeyedMetaMapper[MappedCounterpartyMetadata] { + override def dbIndexes = + UniqueIndex(counterpartyId) :: + Index(thisAccountBankId, thisAccountId, holder, accountNumber) :: + super.dbIndexes +} + +class MappedCounterpartyWhereTag extends GeoTag with LongKeyedMapper[MappedCounterpartyWhereTag] with IdPK with CreatedUpdated { + + def getSingleton = MappedCounterpartyWhereTag + + object user extends MappedLongForeignKey(this, APIUser) + object date extends MappedDateTime(this) + + //TODO: require these to be valid latitude/longitudes + object geoLatitude extends MappedDouble(this) + object geoLongitude extends MappedDouble(this) + + override def postedBy: Box[User] = user.obj + override def datePosted: Date = date.get + override def latitude: Double = geoLatitude.get + override def longitude: Double = geoLongitude.get +} + +object MappedCounterpartyWhereTag extends MappedCounterpartyWhereTag with LongKeyedMetaMapper[MappedCounterpartyWhereTag] \ No newline at end of file diff --git a/src/main/scala/code/metadata/counterparties/MongoCounterparties.scala b/src/main/scala/code/metadata/counterparties/MongoCounterparties.scala index f2c8c0fe5..26a3c75ad 100644 --- a/src/main/scala/code/metadata/counterparties/MongoCounterparties.scala +++ b/src/main/scala/code/metadata/counterparties/MongoCounterparties.scala @@ -1,8 +1,11 @@ package code.metadata.counterparties import code.model.{AccountId, BankId, OtherBankAccountMetadata, OtherBankAccount} -import net.liftweb.common.Loggable +import net.liftweb.common.{Box, Loggable} import com.mongodb.QueryBuilder +import net.liftweb.util.Helpers.tryo +import net.liftweb.common.Full +import org.bson.types.ObjectId object MongoCounterparties extends Counterparties with Loggable { @@ -12,21 +15,25 @@ object MongoCounterparties extends Counterparties with Loggable { Metadata.findAll(query) } + def getMetadata(originalPartyBankId: BankId, originalPartyAccountId : AccountId, counterpartyMetadataId : String) : Box[OtherBankAccountMetadata] = { + /** + * This particular implementation requires the metadata id to be the same as the otherParty (OtherBankAccount) id + */ + for { + objId <- tryo { new ObjectId(counterpartyMetadataId) } + query = QueryBuilder.start("originalPartyBankId").is(originalPartyBankId.value).put("originalPartyAccountId").is(originalPartyAccountId.value). + put("_id").is(objId).get() + m <- Metadata.find(query) + } yield m + } + def getOrCreateMetadata(originalPartyBankId: BankId, originalPartyAccountId : AccountId, otherParty : OtherBankAccount) : OtherBankAccountMetadata = { - import net.liftweb.util.Helpers.tryo - import net.liftweb.common.Full - import org.bson.types.ObjectId /** * This particular implementation requires the metadata id to be the same as the otherParty (OtherBankAccount) id */ - val existing = for { - objId <- tryo { new ObjectId(otherParty.id) } - query = QueryBuilder.start("originalPartyBankId").is(originalPartyBankId.value).put("originalPartyAccountId").is(originalPartyAccountId.value). - put("_id").is(objId).get() - m <- Metadata.find(query) - } yield m + val existing = getMetadata(originalPartyBankId, originalPartyAccountId, otherParty.id) val metadata = existing match { case Full(m) => m diff --git a/src/main/scala/code/metadata/narrative/MappedNarratives.scala b/src/main/scala/code/metadata/narrative/MappedNarratives.scala new file mode 100644 index 000000000..a229b81d3 --- /dev/null +++ b/src/main/scala/code/metadata/narrative/MappedNarratives.scala @@ -0,0 +1,53 @@ +package code.metadata.narrative + +import code.model.{TransactionId, AccountId, BankId} +import net.liftweb.common.Full +import net.liftweb.mapper._ + +object MappedNarratives extends Narrative { + + private def getMappedNarrative(bankId: BankId, accountId: AccountId, transactionId: TransactionId) = { + MappedNarrative.find(By(MappedNarrative.bank, bankId.value), + By(MappedNarrative.account, accountId.value), + By(MappedNarrative.transaction, transactionId.value)) + } + + override def getNarrative(bankId: BankId, accountId: AccountId, transactionId: TransactionId)(): String = { + val found = getMappedNarrative(bankId: BankId, accountId: AccountId, transactionId: TransactionId) + + found.map(_.narrative.get).getOrElse("") + } + + override def setNarrative(bankId: BankId, accountId: AccountId, transactionId: TransactionId)(narrative: String): Unit = { + + val existing = getMappedNarrative(bankId: BankId, accountId: AccountId, transactionId: TransactionId) + + if(narrative.isEmpty) { + //if the new narrative is empty, we can just delete the existing one + existing.foreach(_.delete_!) + } else { + val mappedNarrative = existing match { + case Full(n) => n + case _ => MappedNarrative.create + .bank(bankId.value) + .account(accountId.value) + .transaction(transactionId.value) + } + mappedNarrative.narrative(narrative).save + } + } +} + +class MappedNarrative extends LongKeyedMapper[MappedNarrative] with IdPK with CreatedUpdated { + def getSingleton = MappedNarrative + + object bank extends MappedString(this, 255) + object account extends MappedString(this, 255) + object transaction extends MappedString(this, 255) + + object narrative extends MappedText(this) +} + +object MappedNarrative extends MappedNarrative with LongKeyedMetaMapper[MappedNarrative] { + override def dbIndexes = Index(bank, account, transaction) :: super.dbIndexes +} \ No newline at end of file diff --git a/src/main/scala/code/metadata/narrative/Narrative.scala b/src/main/scala/code/metadata/narrative/Narrative.scala index 5e223f171..825099c31 100644 --- a/src/main/scala/code/metadata/narrative/Narrative.scala +++ b/src/main/scala/code/metadata/narrative/Narrative.scala @@ -7,7 +7,7 @@ object Narrative extends SimpleInjector { val narrative = new Inject(buildOne _) {} - def buildOne: Narrative = MongoTransactionNarrative + def buildOne: Narrative = MappedNarratives } diff --git a/src/main/scala/code/metadata/tags/MappedTags.scala b/src/main/scala/code/metadata/tags/MappedTags.scala new file mode 100644 index 000000000..7c66469c3 --- /dev/null +++ b/src/main/scala/code/metadata/tags/MappedTags.scala @@ -0,0 +1,66 @@ +package code.metadata.tags + +import java.util.Date + +import code.model._ +import code.model.dataAccess.APIUser +import code.util.MappedUUID +import net.liftweb.common.Box +import net.liftweb.util.Helpers.tryo +import net.liftweb.mapper._ + +object MappedTags extends Tags { + override def getTags(bankId: BankId, accountId: AccountId, transactionId: TransactionId)(viewId: ViewId): List[TransactionTag] = { + MappedTag.findAll(MappedTag.findQuery(bankId, accountId, transactionId, viewId): _*) + } + + override def addTag(bankId: BankId, accountId: AccountId, transactionId: TransactionId) + (userId: UserId, viewId: ViewId, tagText: String, datePosted: Date): Box[TransactionTag] = { + tryo{ + MappedTag.create + .bank(bankId.value) + .account(accountId.value) + .transaction(transactionId.value) + .view(viewId.value) + .user(userId.value) + .tag(tagText) + .date(datePosted).saveMe + } + } + + override def deleteTag(bankId: BankId, accountId: AccountId, transactionId: TransactionId)(tagId: String): Box[Unit] = { + //tagId is always unique so we actually don't need to use bankId, accountId, or transactionId + MappedTag.find(By(MappedTag.tagId, tagId)).map(_.delete_!).map(x => ()) //TODO: this should return something more useful than Box[Unit] + } +} + +class MappedTag extends TransactionTag with LongKeyedMapper[MappedTag] with IdPK with CreatedUpdated { + def getSingleton = MappedTag + + object bank extends MappedString(this, 255) + object account extends MappedString(this, 255) + object transaction extends MappedString(this, 255) + object view extends MappedString(this, 255) + + object tagId extends MappedUUID(this) + + object user extends MappedLongForeignKey(this, APIUser) + object tag extends MappedString(this, 255) + object date extends MappedDateTime(this) + + override def id_ : String = tagId.get + override def postedBy: Box[User] = user.obj + override def value: String = tag.get + override def viewId: ViewId = ViewId(view.get) + override def datePosted: Date = date.get +} + +object MappedTag extends MappedTag with LongKeyedMetaMapper[MappedTag] { + override def dbIndexes = Index(bank, account, transaction, view) :: UniqueIndex(tagId) :: super.dbIndexes + + def findQuery(bankId: BankId, accountId: AccountId, transactionId: TransactionId, viewId: ViewId) = + By(MappedTag.bank, bankId.value) :: + By(MappedTag.account, accountId.value) :: + By(MappedTag.transaction, transactionId.value) :: + By(MappedTag.view, viewId.value) :: Nil +} \ No newline at end of file diff --git a/src/main/scala/code/metadata/tags/Tags.scala b/src/main/scala/code/metadata/tags/Tags.scala index 3bc99a511..5cc6ecf86 100644 --- a/src/main/scala/code/metadata/tags/Tags.scala +++ b/src/main/scala/code/metadata/tags/Tags.scala @@ -9,7 +9,7 @@ object Tags extends SimpleInjector { val tags = new Inject(buildOne _) {} - def buildOne: Tags = MongoTransactionTags + def buildOne: Tags = MappedTags } @@ -17,6 +17,7 @@ trait Tags { def getTags(bankId : BankId, accountId : AccountId, transactionId: TransactionId)(viewId : ViewId) : List[TransactionTag] def addTag(bankId : BankId, accountId : AccountId, transactionId: TransactionId)(userId: UserId, viewId : ViewId, tagText : String, datePosted : Date) : Box[TransactionTag] + //TODO: viewId? should tagId always be unique -> in that case bankId, accountId, and transactionId would not be required def deleteTag(bankId : BankId, accountId : AccountId, transactionId: TransactionId)(tagId : String) : Box[Unit] } \ No newline at end of file diff --git a/src/main/scala/code/metadata/transactionimages/MapperTransactionImages.scala b/src/main/scala/code/metadata/transactionimages/MapperTransactionImages.scala new file mode 100644 index 000000000..e1ca13791 --- /dev/null +++ b/src/main/scala/code/metadata/transactionimages/MapperTransactionImages.scala @@ -0,0 +1,73 @@ +package code.metadata.transactionimages + +import java.net.URL +import java.util.Date +import code.model._ +import code.model.dataAccess.APIUser +import code.util.MappedUUID +import net.liftweb.common.Box +import net.liftweb.mapper._ +import net.liftweb.util.Helpers.tryo + +object MapperTransactionImages extends TransactionImages { + override def getImagesForTransaction(bankId: BankId, accountId: AccountId, transactionId: TransactionId)(viewId: ViewId): List[TransactionImage] = { + MappedTransactionImage.findAll( + By(MappedTransactionImage.bank, bankId.value), + By(MappedTransactionImage.account, accountId.value), + By(MappedTransactionImage.transaction, transactionId.value), + By(MappedTransactionImage.view, viewId.value) + ) + } + + override def deleteTransactionImage(bankId: BankId, accountId: AccountId, transactionId: TransactionId)(imageId: String): Box[Unit] = { + //imageId is unique, so we don't need bankId, accountId, and transactionId + //TODO: this should return something more useful than Box[Unit] + MappedTransactionImage.find(By(MappedTransactionImage.imageId, imageId)).map(_.delete_!).map(x => ()) + + } + + override def addTransactionImage(bankId: BankId, accountId: AccountId, transactionId: TransactionId) + (userId: UserId, viewId: ViewId, description: String, datePosted: Date, imageURL: URL): Box[TransactionImage] = { + tryo { + MappedTransactionImage.create + .bank(bankId.value) + .account(accountId.value) + .transaction(transactionId.value) + .view(viewId.value) + .user(userId.value) + .imageDescription(description) + .url(imageURL.toString) + .date(datePosted).saveMe + } + } +} + +class MappedTransactionImage extends TransactionImage with LongKeyedMapper[MappedTransactionImage] with IdPK with CreatedUpdated { + def getSingleton = MappedTransactionImage + + object bank extends MappedString(this, 255) + object account extends MappedString(this, 255) + object transaction extends MappedString(this, 255) + object view extends MappedString(this, 255) + + object imageId extends MappedUUID(this) + object user extends MappedLongForeignKey(this, APIUser) + object date extends MappedDateTime(this) + + object url extends MappedText(this) + object imageDescription extends MappedText(this) + + override def id_ : String = imageId.get + override def postedBy: Box[User] = user.obj + override def description: String = imageDescription.get + override def imageUrl: URL = tryo {new URL(url.get)} getOrElse MappedTransactionImage.notFoundUrl + override def viewId: ViewId = ViewId(view.get) + override def datePosted: Date = date.get +} + +object MappedTransactionImage extends MappedTransactionImage with LongKeyedMetaMapper[MappedTransactionImage] { + override def dbIndexes = Index(bank, account, transaction, view) :: UniqueIndex(imageId) :: super.dbIndexes + + + val notFoundUrl = new URL("http://example.com/notfound.png") //TODO: Make this image exist? +} \ No newline at end of file diff --git a/src/main/scala/code/metadata/transactionimages/TransactionImages.scala b/src/main/scala/code/metadata/transactionimages/TransactionImages.scala index ce1c83bc4..80203c258 100644 --- a/src/main/scala/code/metadata/transactionimages/TransactionImages.scala +++ b/src/main/scala/code/metadata/transactionimages/TransactionImages.scala @@ -10,7 +10,7 @@ object TransactionImages extends SimpleInjector { val transactionImages = new Inject(buildOne _) {} - def buildOne: TransactionImages = MongoTransactionImages + def buildOne: TransactionImages = MapperTransactionImages } diff --git a/src/main/scala/code/metadata/wheretags/MapperWhereTags.scala b/src/main/scala/code/metadata/wheretags/MapperWhereTags.scala new file mode 100644 index 000000000..1def1a39a --- /dev/null +++ b/src/main/scala/code/metadata/wheretags/MapperWhereTags.scala @@ -0,0 +1,79 @@ +package code.metadata.wheretags + +import java.util.Date + +import code.model._ +import code.model.dataAccess.APIUser +import net.liftweb.util.Helpers.tryo +import net.liftweb.common.Box +import net.liftweb.mapper._ + +object MapperWhereTags extends WhereTags { + + private def findMappedWhereTag(bankId: BankId, accountId: AccountId, transactionId: TransactionId, viewId : ViewId) = { + MappedWhereTag.find( + By(MappedWhereTag.bank, bankId.value), + By(MappedWhereTag.account, accountId.value), + By(MappedWhereTag.transaction, transactionId.value), + By(MappedWhereTag.view, viewId.value)) + } + + override def addWhereTag(bankId: BankId, accountId: AccountId, transactionId: TransactionId) + (userId: UserId, viewId: ViewId, datePosted: Date, longitude: Double, latitude: Double): Boolean = { + + val found = findMappedWhereTag(bankId, accountId, transactionId, viewId) + + val toUpdate = found.getOrElse { + MappedWhereTag.create + .bank(bankId.value) + .account(accountId.value) + .transaction(transactionId.value) + .view(viewId.value) + } + + toUpdate + .user(userId.value) + .date(datePosted) + .geoLatitude(latitude) + .geoLongitude(longitude) + + + tryo{toUpdate.saveMe}.isDefined + } + + override def deleteWhereTag(bankId: BankId, accountId: AccountId, transactionId: TransactionId)(viewId: ViewId): Boolean = { + val found = findMappedWhereTag(bankId, accountId, transactionId, viewId) + + found.map(_.delete_!).getOrElse(false) + } + + override def getWhereTagForTransaction(bankId: BankId, accountId: AccountId, transactionId: TransactionId)(viewId: ViewId): Option[GeoTag] = { + findMappedWhereTag(bankId: BankId, accountId: AccountId, transactionId: TransactionId, viewId: ViewId) + } +} + +class MappedWhereTag extends GeoTag with LongKeyedMapper[MappedWhereTag] with IdPK with CreatedUpdated { + + def getSingleton = MappedWhereTag + + object bank extends MappedString(this, 255) + object account extends MappedString(this, 255) + object transaction extends MappedString(this, 255) + object view extends MappedString(this, 255) + + object user extends MappedLongForeignKey(this, APIUser) + object date extends MappedDateTime(this) + + //TODO: require these to be valid latitude/longitudes + object geoLatitude extends MappedDouble(this) + object geoLongitude extends MappedDouble(this) + + override def datePosted: Date = date.get + override def postedBy: Box[User] = user.obj + override def latitude: Double = geoLatitude.get + override def longitude: Double = geoLongitude.get +} + +object MappedWhereTag extends MappedWhereTag with LongKeyedMetaMapper[MappedWhereTag] { + override def dbIndexes = Index(bank, account, transaction, view) :: super.dbIndexes +} diff --git a/src/main/scala/code/metadata/wheretags/WhereTags.scala b/src/main/scala/code/metadata/wheretags/WhereTags.scala index b1dd04b78..7bc6dc173 100644 --- a/src/main/scala/code/metadata/wheretags/WhereTags.scala +++ b/src/main/scala/code/metadata/wheretags/WhereTags.scala @@ -8,7 +8,7 @@ object WhereTags extends SimpleInjector { val whereTags = new Inject(buildOne _) {} - def buildOne: WhereTags = MongoTransactionWhereTags + def buildOne: WhereTags = MapperWhereTags } diff --git a/src/main/scala/code/metrics/APIMetrics.scala b/src/main/scala/code/metrics/APIMetrics.scala index a3e42e307..5ed4e712d 100644 --- a/src/main/scala/code/metrics/APIMetrics.scala +++ b/src/main/scala/code/metrics/APIMetrics.scala @@ -7,7 +7,7 @@ object APIMetrics extends SimpleInjector { val apiMetrics = new Inject(buildOne _) {} - def buildOne: APIMetrics = MongoAPIMetric + def buildOne: APIMetrics = MappedMetrics /** * Returns a Date which is at the start of the day of the date diff --git a/src/main/scala/code/metrics/MappedMetrics.scala b/src/main/scala/code/metrics/MappedMetrics.scala new file mode 100644 index 000000000..75eb89ada --- /dev/null +++ b/src/main/scala/code/metrics/MappedMetrics.scala @@ -0,0 +1,39 @@ +package code.metrics + +import java.util.Date + +import net.liftweb.mapper._ + +object MappedMetrics extends APIMetrics { + + override def saveMetric(url: String, date: Date): Unit = { + MappedMetric.create.url(url).date(date).save + } + + override def getAllGroupedByDay(): Map[Date, List[APIMetric]] = { + //TODO: do this all at the db level using an actual group by query + MappedMetric.findAll.groupBy(APIMetrics.getMetricDay) + } + + override def getAllGroupedByUrl(): Map[String, List[APIMetric]] = { + //TODO: do this all at the db level using an actual group by query + MappedMetric.findAll.groupBy(_.getUrl()) + } + +} + +class MappedMetric extends APIMetric with LongKeyedMapper[MappedMetric] with IdPK { + override def getSingleton = MappedMetric + + object url extends MappedText(this) + object date extends MappedDateTime(this) + + + override def getUrl(): String = url.get + override def getDate(): Date = date.get + +} + +object MappedMetric extends MappedMetric with LongKeyedMetaMapper[MappedMetric] { + override def dbIndexes = Index(url) :: Index(date) :: super.dbIndexes +} diff --git a/src/main/scala/code/model/BankingData.scala b/src/main/scala/code/model/BankingData.scala index 22741fe93..f84de80c2 100644 --- a/src/main/scala/code/model/BankingData.scala +++ b/src/main/scala/code/model/BankingData.scala @@ -164,7 +164,9 @@ class AccountOwner( case class BankAccountUID(bankId : BankId, accountId : AccountId) -trait BankAccount extends Loggable { +trait BankAccount { + + @transient protected val log = Logger(this.getClass) @deprecated def uuid : String @@ -192,7 +194,6 @@ trait BankAccount extends Loggable { final def owners: Set[User] = { val accountHolders = Connector.connector.vend.getAccountHolders(bankId, accountId) - if(accountHolders.isEmpty) { //account holders are not all set up in the db yet, so we might not get any back. //In this case, we just use the previous behaviour, which did not return very much information at all @@ -215,7 +216,7 @@ trait BankAccount extends Loggable { user match { case Full(u) => u.permittedViews(this) case _ =>{ - logger.info("no user was found in the permittedViews") + log.info("no user was found in the permittedViews") publicViews } } @@ -353,7 +354,7 @@ trait BankAccount extends Loggable { val view = Views.views.vend.createView(this, v) if(view.isDefined) { - logger.info("user: " + userDoingTheCreate.idGivenByProvider + " at provider " + userDoingTheCreate.provider + " created view: " + view.get + + log.info("user: " + userDoingTheCreate.idGivenByProvider + " at provider " + userDoingTheCreate.provider + " created view: " + view.get + " for account " + accountId + "at bank " + bankId) } @@ -368,7 +369,7 @@ trait BankAccount extends Loggable { val view = Views.views.vend.updateView(this, viewId, v) if(view.isDefined) { - logger.info("user: " + userDoingTheUpdate.idGivenByProvider + " at provider " + userDoingTheUpdate.provider + " updated view: " + view.get + + log.info("user: " + userDoingTheUpdate.idGivenByProvider + " at provider " + userDoingTheUpdate.provider + " updated view: " + view.get + " for account " + accountId + "at bank " + bankId) } @@ -383,7 +384,7 @@ trait BankAccount extends Loggable { val deleted = Views.views.vend.removeView(viewId, this) if(deleted.isDefined) { - logger.info("user: " + userDoingTheRemove.idGivenByProvider + " at provider " + userDoingTheRemove.provider + " deleted view: " + viewId + + log.info("user: " + userDoingTheRemove.idGivenByProvider + " at provider " + userDoingTheRemove.provider + " deleted view: " + viewId + " for account " + accountId + "at bank " + bankId) } @@ -442,7 +443,6 @@ trait BankAccount extends Loggable { else viewNotAllowed(view) - @deprecated(Helper.deprecatedJsonGenerationMessage) final def overviewJson(user: Box[User]): JObject = { val views = permittedViews(user) diff --git a/src/main/scala/code/model/MappedTransaction.scala b/src/main/scala/code/model/MappedTransaction.scala new file mode 100644 index 000000000..e04350617 --- /dev/null +++ b/src/main/scala/code/model/MappedTransaction.scala @@ -0,0 +1,128 @@ +package code.model + +import java.util.UUID + +import code.bankconnectors.Connector +import code.metadata.counterparties.Counterparties +import code.util.{Helper, MappedUUID} +import net.liftweb.common.{Logger, Box} +import net.liftweb.mapper._ + +class MappedTransaction extends LongKeyedMapper[MappedTransaction] with IdPK with CreatedUpdated with TransactionUUID { + + private val logger = Logger(classOf[MappedTransaction]) + + def getSingleton = MappedTransaction + + object bank extends MappedString(this, 255) + object account extends MappedString(this, 255) + object transactionId extends MappedString(this, 255) { + override def defaultValue = UUID.randomUUID().toString + } + //TODO: review the need for this + object transactionUUID extends MappedUUID(this) + object transactionType extends MappedString(this, 20) + + //amount/new balance use the smallest unit of currency! e.g. cents, yen, pence, øre, etc. + object amount extends MappedLong(this) + object newAccountBalance extends MappedLong(this) + + object currency extends MappedString(this, 10) + + object tStartDate extends MappedDateTime(this) + object tFinishDate extends MappedDateTime(this) + + object description extends MappedText(this) + + object counterpartyAccountNumber extends MappedString(this, 60) + object counterpartyAccountHolder extends MappedString(this, 100) + //still unclear exactly how what this is defined to mean + object counterpartyNationalId extends MappedString(this, 40) + //this should eventually be calculated using counterpartyNationalId + object counterpartyBankName extends MappedString(this, 100) + //this should eventually either generate counterpartyAccountNumber or be generated + object counterpartyIban extends MappedString(this, 100) + object counterpartyAccountKind extends MappedString(this, 40) + + + override def theTransactionId = TransactionId(transactionId.get) + override def theAccountId = AccountId(account.get) + override def theBankId = BankId(bank.get) + + def getCounterpartyIban() = { + val i = counterpartyIban.get + if(i.isEmpty) None else Some(i) + } + + def toTransaction(account: BankAccount): Option[Transaction] = { + val tBankId = theBankId + val tAccId = theAccountId + + if (tBankId != account.bankId || tAccId != account.accountId) { + logger.warn("Attempted to convert MappedTransaction to Transaction using unrelated existing BankAccount object") + None + } else { + val label = { + val d = description.get + if (d.isEmpty) None else Some(d) + } + + val transactionCurrency = currency.get + val amt = Helper.smallestCurrencyUnitToBigDecimal(amount.get, transactionCurrency) + val newBalance = Helper.smallestCurrencyUnitToBigDecimal(newAccountBalance.get, transactionCurrency) + + def createOtherBankAccount(alreadyFoundMetadata : Option[OtherBankAccountMetadata]) = { + new OtherBankAccount( + id = alreadyFoundMetadata.map(_.metadataId).getOrElse(""), + label = counterpartyAccountHolder.get, + nationalIdentifier = counterpartyNationalId.get, + swift_bic = None, //TODO: need to add this to the json/model + iban = getCounterpartyIban(), + number = counterpartyAccountNumber.get, + bankName = counterpartyBankName.get, + kind = counterpartyAccountKind.get, + originalPartyBankId = theBankId, + originalPartyAccountId = theAccountId, + alreadyFoundMetadata = alreadyFoundMetadata + ) + } + + //it's a bit confusing what's going on here, as normally metadata should be automatically generated if + //it doesn't exist when an OtherBankAccount object is created. The issue here is that for legacy reasons + //otherAccount ids are metadata ids, so the metadata needs to exist before we created the OtherBankAccount + //so that we know what id to give it. + + //creates a dummy OtherBankAccount without an OtherBankAccountMetadata, which results in one being generated (in OtherBankAccount init) + val dummyOtherBankAccount = createOtherBankAccount(None) + + //and create the proper OtherBankAccount with the correct "id" attribute set to the metadataId of the OtherBankAccountMetadata object + //note: as we are passing in the OtherBankAccountMetadata we don't incur another db call to get it in OtherBankAccount init + val otherAccount = createOtherBankAccount(Some(dummyOtherBankAccount.metadata)) + + Some(new Transaction( + transactionUUID.get, + theTransactionId, + account, + otherAccount, + transactionType.get, + amt, + transactionCurrency, + label, + tStartDate.get, + tFinishDate.get, + newBalance)) + } + } + + def toTransaction : Option[Transaction] = { + for { + acc <- Connector.connector.vend.getBankAccount(theBankId, theAccountId) + transaction <- toTransaction(acc) + } yield transaction + } + +} + +object MappedTransaction extends MappedTransaction with LongKeyedMetaMapper[MappedTransaction] { + override def dbIndexes = UniqueIndex(transactionId, bank, account) :: Index(bank, account) :: super.dbIndexes +} diff --git a/src/main/scala/code/model/dataAccess/MappedBank.scala b/src/main/scala/code/model/dataAccess/MappedBank.scala new file mode 100644 index 000000000..0674d39e2 --- /dev/null +++ b/src/main/scala/code/model/dataAccess/MappedBank.scala @@ -0,0 +1,29 @@ +package code.model.dataAccess + +import code.model.{BankId, Bank} +import net.liftweb.mapper._ + +class MappedBank extends Bank with LongKeyedMapper[MappedBank] with IdPK with CreatedUpdated { + def getSingleton = MappedBank + + object permalink extends MappedString(this, 255) + object fullBankName extends MappedString(this, 255) + object shortBankName extends MappedString(this, 100) + object logoURL extends MappedString(this, 255) + object websiteURL extends MappedString(this, 255) + object national_identifier extends MappedString(this, 255) + + override def bankId: BankId = BankId(permalink.get) + override def fullName: String = fullBankName.get + override def shortName: String = shortBankName.get + override def logoUrl: String = logoURL.get + override def websiteUrl: String = websiteURL.get + override def nationalIdentifier: String = national_identifier.get +} + +object MappedBank extends MappedBank with LongKeyedMetaMapper[MappedBank] { + override def dbIndexes = Index(permalink) :: super.dbIndexes + + def findByBankId(bankId : BankId) = + MappedBank.find(By(MappedBank.permalink, bankId.value)) +} \ No newline at end of file diff --git a/src/main/scala/code/model/dataAccess/MappedBankAccount.scala b/src/main/scala/code/model/dataAccess/MappedBankAccount.scala new file mode 100644 index 000000000..a9c87f9de --- /dev/null +++ b/src/main/scala/code/model/dataAccess/MappedBankAccount.scala @@ -0,0 +1,54 @@ +package code.model.dataAccess + +import java.math.MathContext + +import code.model._ +import code.util.Helper +import net.liftweb.mapper._ + +class MappedBankAccount extends BankAccount with LongKeyedMapper[MappedBankAccount] with IdPK with CreatedUpdated { + + override def getSingleton = MappedBankAccount + + object bank extends MappedString(this, 255) + object theAccountId extends MappedString(this, 255) + object accountIban extends MappedString(this, 50) + object accountCurrency extends MappedString(this, 10) + object accountSwiftBic extends MappedString(this, 50) + object accountNumber extends MappedString(this, 50) + + @deprecated + object holder extends MappedString(this, 100) + + //this is the smallest unit of currency! e.g. cents, yen, pence, øre, etc. + object accountBalance extends MappedLong(this) + + object accountName extends MappedString(this, 255) + object kind extends MappedString(this, 40) + object accountLabel extends MappedString(this, 255) + + //the last time this account was updated via hbci + object lastUpdate extends MappedDateTime(this) + + override def accountId: AccountId = AccountId(theAccountId.get) + override def iban: Option[String] = { + val i = accountIban.get + if(i.isEmpty) None else Some(i) + } + override def bankId: BankId = BankId(bank.get) + override def currency: String = accountCurrency.get + override def swift_bic: Option[String] = { + val sb = accountSwiftBic.get + if(sb.isEmpty) None else Some(sb) + } + override def number: String = accountNumber.get + override def balance: BigDecimal = Helper.smallestCurrencyUnitToBigDecimal(accountBalance.get, currency) + override def name: String = accountName.get + override def accountType: String = kind.get + override def label: String = accountLabel.get + override def accountHolder: String = holder.get +} + +object MappedBankAccount extends MappedBankAccount with LongKeyedMetaMapper[MappedBankAccount] { + override def dbIndexes = UniqueIndex(bank, theAccountId) :: super.dbIndexes +} diff --git a/src/main/scala/code/sandbox/LocalMappedConnectorDataImport.scala b/src/main/scala/code/sandbox/LocalMappedConnectorDataImport.scala new file mode 100644 index 000000000..a55515665 --- /dev/null +++ b/src/main/scala/code/sandbox/LocalMappedConnectorDataImport.scala @@ -0,0 +1,100 @@ +package code.sandbox + +import code.metadata.counterparties.{MappedCounterpartyMetadata} +import code.model.dataAccess.{MappedBankAccount, MappedBank} +import code.model.{MappedTransaction, AccountId, BankId} +import code.util.Helper.convertToSmallestCurrencyUnits +import net.liftweb.common.{Full, Failure, Box} +import net.liftweb.mapper.Mapper +import net.liftweb.util.Helpers._ + +case class MappedSaveable[T <: Mapper[_]](value : T) extends Saveable[T] { + def save() = value.save() +} + +object LocalMappedConnectorDataImport extends OBPDataImport with CreateViewImpls with CreateOBPUsers { + + type BankType = MappedBank + type AccountType = MappedBankAccount + type MetadataType = MappedCounterpartyMetadata + type TransactionType = MappedTransaction + + protected def createSaveableBanks(data : List[SandboxBankImport]) : Box[List[Saveable[BankType]]] = { + val mappedBanks = data.map(bank => { + MappedBank.create + .permalink(bank.id) + .fullBankName(bank.full_name) + .shortBankName(bank.short_name) + .logoURL(bank.logo) + .websiteURL(bank.website) + }) + + val validationErrors = mappedBanks.flatMap(_.validate) + + if(validationErrors.nonEmpty) { + Failure(s"Errors: ${validationErrors.map(_.msg)}") + } else { + Full(mappedBanks.map(MappedSaveable(_))) + } + } + + protected def createSaveableAccount(acc : SandboxAccountImport, banks : List[BankType]) : Box[Saveable[AccountType]] = { + + val mappedAccount = for { + balance <- tryo{BigDecimal(acc.balance.amount)} ?~ s"Invalid balance: ${acc.balance.amount}" + currency = acc.balance.currency + } yield { + MappedBankAccount.create + .theAccountId(acc.id) + .bank(acc.bank) + .accountLabel(acc.label) + .accountNumber(acc.number) + .kind(acc.`type`) + .accountIban(acc.IBAN) + .accountCurrency(currency) + .accountBalance(convertToSmallestCurrencyUnits(balance, currency)) + } + + val validationErrors = mappedAccount.map(_.validate).getOrElse(Nil) + + if(validationErrors.nonEmpty) { + Failure(s"Errors: ${validationErrors.map(_.msg)}") + } else { + mappedAccount.map(MappedSaveable(_)) + } + } + + + override protected def createSaveableTransaction(t : SandboxTransactionImport, createdBanks : List[BankType], createdAccounts : List[AccountType]): + Box[Saveable[TransactionType]] = { + + for { + createdAcc <- Box(createdAccounts.find(acc => acc.accountId == AccountId(t.this_account.id) && acc.bankId == BankId(t.this_account.bank))) ?~ { + logger.warn("Data import failed because a created account was not found for a transaction when it should have been") + "Server Error" + } + currency = createdAcc.currency + newBalanceValueAsBigDecimal <- tryo{BigDecimal(t.details.new_balance)} ?~ s"Invalid new balance: ${t.details.new_balance}" + tValueAsBigDecimal <- tryo{BigDecimal(t.details.value)} ?~ s"Invalid transaction value: ${t.details.value}" + postedDate <- tryo{dateFormat.parse(t.details.posted)} ?~ s"Invalid date format: ${t.details.posted}. Expected pattern $datePattern" + completedDate <-tryo{dateFormat.parse(t.details.completed)} ?~ s"Invalid date format: ${t.details.completed}. Expected pattern $datePattern" + } yield { + val mappedTransaction = MappedTransaction.create + .bank(t.this_account.bank) + .account(t.this_account.id) + .transactionId(t.id) + .transactionType(t.details.`type`) + .amount(convertToSmallestCurrencyUnits(tValueAsBigDecimal, currency)) + .newAccountBalance(convertToSmallestCurrencyUnits(newBalanceValueAsBigDecimal, currency)) + .currency(currency) + .tStartDate(postedDate) + .tFinishDate(completedDate) + .description(t.details.description) + .counterpartyAccountHolder(t.counterparty.flatMap(_.name).getOrElse("")) + .counterpartyAccountNumber(t.counterparty.flatMap(_.account_number).getOrElse("")) + + MappedSaveable(mappedTransaction) + } + } + +} diff --git a/src/main/scala/code/sandbox/OBPDataImport.scala b/src/main/scala/code/sandbox/OBPDataImport.scala index aa29a47c1..e0555e700 100644 --- a/src/main/scala/code/sandbox/OBPDataImport.scala +++ b/src/main/scala/code/sandbox/OBPDataImport.scala @@ -15,7 +15,7 @@ object OBPDataImport extends SimpleInjector { val importer = new Inject(buildOne _) {} - def buildOne : OBPDataImport = LocalConnectorDataImport + def buildOne : OBPDataImport = LocalMappedConnectorDataImport } diff --git a/src/main/scala/code/util/Helper.scala b/src/main/scala/code/util/Helper.scala index b52354ef6..4772625cb 100644 --- a/src/main/scala/code/util/Helper.scala +++ b/src/main/scala/code/util/Helper.scala @@ -58,4 +58,40 @@ object Helper{ } val deprecatedJsonGenerationMessage = "json generation handled elsewhere as it changes from api version to api version" + + /** + * Converts a number representing the smallest unit of a currency into a big decimal formatted according to the rules of + * that currency. E.g. JPY: 1000 units (yen) => 1000, EUR: 1000 units (cents) => 10.00 + */ + def smallestCurrencyUnitToBigDecimal(units : Long, currencyCode : String) = { + BigDecimal(units, currencyDecimalPlaces(currencyCode)) + } + + /** + * Returns the number of decimal places a currency has. E.g. "EUR" -> 2, "JPY" -> 0 + * @param currencyCode + * @return + */ + def currencyDecimalPlaces(currencyCode : String) = { + //this data was sourced from Wikipedia, so it might not all be correct, + //and some banking systems may still retain different units (e.g. CZK?) + //notable it doesn't cover non-traditional currencies (e.g. cryptocurrencies) + currencyCode match { + //TODO: handle MRO and MGA, which are non-decimal + case "CZK" | "JPY" | "KRW" => 0 + case "KWD" | "OMR" => 3 + case _ => 2 + } + } + + /** + * E.g. + * amount: BigDecimal("12.45"), currencyCode : "EUR" => 1245 + * amount: BigDecimal("9034"), currencyCode : "JPY" => 9034 + */ + def convertToSmallestCurrencyUnits(amount : BigDecimal, currencyCode : String) : Long = { + val decimalPlaces = Helper.currencyDecimalPlaces(currencyCode) + + (amount * BigDecimal("10").pow(decimalPlaces)).toLong + } } \ No newline at end of file diff --git a/src/test/scala/code/api/DefaultConnectorTestSetup.scala b/src/test/scala/code/api/DefaultConnectorTestSetup.scala index 5aa2d48b3..5a491d994 100644 --- a/src/test/scala/code/api/DefaultConnectorTestSetup.scala +++ b/src/test/scala/code/api/DefaultConnectorTestSetup.scala @@ -1,4 +1,4 @@ package code.api //Set the default connector setup here by extending it -trait DefaultConnectorTestSetup extends LocalConnectorTestSetup \ No newline at end of file +trait DefaultConnectorTestSetup extends LocalMappedConnectorTestSetup \ No newline at end of file diff --git a/src/test/scala/code/api/LocalMappedConnectorTestSetup.scala b/src/test/scala/code/api/LocalMappedConnectorTestSetup.scala new file mode 100644 index 000000000..c8cd52fef --- /dev/null +++ b/src/test/scala/code/api/LocalMappedConnectorTestSetup.scala @@ -0,0 +1,72 @@ +package code.api + +import java.util.Date +import bootstrap.liftweb.ToSchemify +import code.model.dataAccess._ +import code.model._ +import net.liftweb.mapper.MetaMapper +import net.liftweb.util.Helpers._ + +import scala.util.Random + +trait LocalMappedConnectorTestSetup extends LocalConnectorTestSetup { + + override protected def createBank(id : String) : Bank = { + MappedBank.create + .fullBankName(randomString(5)) + .shortBankName(randomString(5)) + .permalink(id) + .national_identifier(randomString(5)).saveMe + } + + override protected def createAccount(bankId: BankId, accountId : AccountId, currency : String) : BankAccount = { + MappedBankAccount.create + .bank(bankId.value) + .theAccountId(accountId.value) + .accountCurrency(currency) + .accountBalance(10000) + .holder(randomString(4)) + .accountNumber(randomString(4)) + .accountLabel(randomString(4)).saveMe + } + + override protected def createTransaction(account: BankAccount, startDate: Date, finishDate: Date) = { + //ugly + val mappedBankAccount = account.asInstanceOf[MappedBankAccount] + + val accountBalanceBefore = mappedBankAccount.accountBalance.get + val transactionAmount = Random.nextInt(1000).toLong + val accountBalanceAfter = accountBalanceBefore + transactionAmount + + mappedBankAccount.accountBalance(accountBalanceAfter).save + + MappedTransaction.create + .bank(account.bankId.value) + .account(account.accountId.value) + .transactionType(randomString(5)) + .tStartDate(startDate) + .tFinishDate(finishDate) + .currency(account.currency) + .amount(transactionAmount) + .newAccountBalance(accountBalanceAfter) + .description(randomString(5)) + .counterpartyAccountHolder(randomString(5)) + .counterpartyAccountKind(randomString(5)) + .counterpartyAccountNumber(randomString(5)) + .counterpartyBankName(randomString(5)) + .counterpartyIban(randomString(5)) + .counterpartyNationalId(randomString(5)) + .saveMe + .toTransaction.get + } + + override protected def wipeTestData() = { + //returns true if the model should not be wiped after each test + def exclusion(m : MetaMapper[_]) = { + m == Nonce || m == Token || m == Consumer || m == OBPUser || m == APIUser + } + + //empty the relational db tables after each test + ToSchemify.models.filterNot(exclusion).foreach(_.bulkDelete_!!()) + } +} diff --git a/src/test/scala/code/api/ServerSetup.scala b/src/test/scala/code/api/ServerSetup.scala index 5fd01cecc..b19456a21 100644 --- a/src/test/scala/code/api/ServerSetup.scala +++ b/src/test/scala/code/api/ServerSetup.scala @@ -56,6 +56,7 @@ trait ServerSetupWithTestData extends ServerSetup with DefaultConnectorTestSetup override def beforeEach() = { super.beforeEach() + implicit val dateFormats = net.liftweb.json.DefaultFormats //create fake data for the tests //fake banks diff --git a/src/test/scala/code/metrics/MetricsTest.scala b/src/test/scala/code/metrics/MetricsTest.scala index a9b47ef7d..f5e9809e1 100644 --- a/src/test/scala/code/metrics/MetricsTest.scala +++ b/src/test/scala/code/metrics/MetricsTest.scala @@ -4,7 +4,6 @@ import java.text.SimpleDateFormat import java.util.Date import code.api.test.ServerSetup -import net.liftweb.mongodb._ class MetricsTest extends ServerSetup with WipeMetrics { @@ -106,7 +105,6 @@ class MetricsTest extends ServerSetup with WipeMetrics { */ trait WipeMetrics { def wipeAllExistingMetrics() = { - //just drops all mongodb databases - MongoDB.getDb(DefaultMongoIdentifier).foreach(_.dropDatabase()) + MappedMetric.bulkDelete_!!() } }