mirror of
https://github.com/OpenBankProject/OBP-API.git
synced 2026-02-06 19:36:50 +00:00
Products: Use code instead of Id, check for duplicates on sandbox load
This commit is contained in:
parent
61d1d17f44
commit
e9720c5ebf
@ -10,7 +10,7 @@ import code.products.Products.{Product}
|
||||
|
||||
import code.customerinfo.{CustomerMessage, CustomerInfo}
|
||||
import code.model.BankId
|
||||
import code.products.Products.ProductId
|
||||
import code.products.Products.ProductCode
|
||||
|
||||
object JSONFactory1_4_0 {
|
||||
|
||||
@ -139,8 +139,7 @@ object JSONFactory1_4_0 {
|
||||
// Products
|
||||
|
||||
|
||||
case class ProductJson(id : String,
|
||||
code : String,
|
||||
case class ProductJson(code : String,
|
||||
name : String,
|
||||
category: String,
|
||||
family : String,
|
||||
@ -153,8 +152,7 @@ object JSONFactory1_4_0 {
|
||||
|
||||
|
||||
def createProductJson(product: Product) : ProductJson = {
|
||||
ProductJson(product.productId.value,
|
||||
product.code,
|
||||
ProductJson(product.code.value,
|
||||
product.name,
|
||||
product.category,
|
||||
product.family,
|
||||
|
||||
@ -3,7 +3,7 @@ package code.products
|
||||
import code.products.Products._
|
||||
import code.common.{License, Meta}
|
||||
import code.model.BankId
|
||||
import code.products.Products.ProductId
|
||||
import code.products.Products.ProductCode
|
||||
import code.util.DefaultStringField
|
||||
import net.liftweb.mapper._
|
||||
|
||||
@ -12,8 +12,11 @@ import code.products.Products.Product
|
||||
|
||||
object MappedProductsProvider extends ProductsProvider {
|
||||
|
||||
override protected def getProductFromProvider(productId: ProductId): Option[Product] =
|
||||
MappedProduct.find(By(MappedProduct.mProductId, productId.value))
|
||||
override protected def getProductFromProvider(bankId: BankId, productCode: ProductCode): Option[Product] =
|
||||
MappedProduct.find(
|
||||
By(MappedProduct.mBankId, bankId.value),
|
||||
By(MappedProduct.mCode, productCode.value)
|
||||
)
|
||||
|
||||
override protected def getProductsFromProvider(bankId: BankId): Option[List[Product]] = {
|
||||
Some(MappedProduct.findAll(By(MappedProduct.mBankId, bankId.value)))
|
||||
@ -26,11 +29,12 @@ class MappedProduct extends Product with LongKeyedMapper[MappedProduct] with IdP
|
||||
|
||||
override def getSingleton = MappedProduct
|
||||
|
||||
object mBankId extends DefaultStringField(this)
|
||||
object mCode extends DefaultStringField(this)
|
||||
object mBankId extends DefaultStringField(this) // combination of this
|
||||
object mCode extends DefaultStringField(this) // and this is unique
|
||||
|
||||
object mName extends DefaultStringField(this)
|
||||
|
||||
object mProductId extends DefaultStringField(this)
|
||||
// Note we have an database pk called id but don't expose it
|
||||
|
||||
// Exposed inside address. See below
|
||||
object mCategory extends DefaultStringField(this)
|
||||
@ -42,11 +46,9 @@ class MappedProduct extends Product with LongKeyedMapper[MappedProduct] with IdP
|
||||
object mLicenseId extends DefaultStringField(this)
|
||||
object mLicenseName extends DefaultStringField(this)
|
||||
|
||||
override def productId: ProductId = ProductId(mProductId.get)
|
||||
|
||||
override def bankId: BankId = BankId(mBankId.get)
|
||||
|
||||
override def code: String = mCode.get
|
||||
override def code: ProductCode = ProductCode(mCode.get)
|
||||
override def name: String = mName.get
|
||||
|
||||
override def category: String = mCategory.get
|
||||
@ -67,6 +69,6 @@ class MappedProduct extends Product with LongKeyedMapper[MappedProduct] with IdP
|
||||
|
||||
//
|
||||
object MappedProduct extends MappedProduct with LongKeyedMetaMapper[MappedProduct] {
|
||||
override def dbIndexes = UniqueIndex(mBankId, mProductId) :: Index(mBankId) :: super.dbIndexes
|
||||
override def dbIndexes = UniqueIndex(mBankId, mCode) :: Index(mBankId) :: super.dbIndexes
|
||||
}
|
||||
|
||||
|
||||
@ -4,7 +4,7 @@ package code.products
|
||||
|
||||
// Need to import these one by one because in same package!
|
||||
|
||||
import code.products.Products.{Product, ProductId}
|
||||
import code.products.Products.{Product, ProductCode}
|
||||
import code.model.BankId
|
||||
import code.common.{Address, Location, Meta}
|
||||
import net.liftweb.common.Logger
|
||||
@ -12,12 +12,12 @@ import net.liftweb.util.SimpleInjector
|
||||
|
||||
object Products extends SimpleInjector {
|
||||
|
||||
case class ProductId(value : String)
|
||||
// Good to have this as a class because when passing as argument, we get compiler error if passing the wrong type.
|
||||
case class ProductCode(value : String)
|
||||
|
||||
trait Product {
|
||||
def productId : ProductId
|
||||
def code : ProductCode
|
||||
def bankId : BankId
|
||||
def code : String
|
||||
def name : String
|
||||
def category: String
|
||||
def family : String
|
||||
@ -51,17 +51,16 @@ trait ProductsProvider {
|
||||
|
||||
/*
|
||||
Common logic for returning products.
|
||||
Use adminView = true to get all Products, else only ones with license returned.
|
||||
*/
|
||||
final def getProducts(bankId : BankId) : Option[List[Product]] = {
|
||||
// If we get products filter them
|
||||
|
||||
final def getProducts(bankId : BankId, adminView: Boolean = false) : Option[List[Product]] = {
|
||||
logger.info(s"Hello from getProducts bankId is: $bankId")
|
||||
|
||||
getProductsFromProvider(bankId) match {
|
||||
case Some(products) => {
|
||||
|
||||
val productsWithLicense = for {
|
||||
product <- products if product.meta.license.name.size > 3 && product.meta.license.name.size > 3
|
||||
// Only return products that have a license set unless its for an admin view
|
||||
product <- products if (adminView || (product.meta.license.name.size > 3 && product.meta.license.name.size > 3))
|
||||
} yield product
|
||||
Option(productsWithLicense)
|
||||
}
|
||||
@ -70,14 +69,14 @@ trait ProductsProvider {
|
||||
}
|
||||
|
||||
/*
|
||||
Return one Product
|
||||
Return one Product at a bank
|
||||
*/
|
||||
final def getProduct(productId : ProductId) : Option[Product] = {
|
||||
final def getProduct(bankId : BankId, productCode : ProductCode, adminView: Boolean = false) : Option[Product] = {
|
||||
// Filter out if no license data
|
||||
getProductFromProvider(productId).filter(x => x.meta.license.id != "" && x.meta.license.name != "")
|
||||
getProductFromProvider(bankId, productCode).filter(x => (adminView || (x.meta.license.id != "" && x.meta.license.name != "")))
|
||||
}
|
||||
|
||||
protected def getProductFromProvider(productId : ProductId) : Option[Product]
|
||||
protected def getProductFromProvider(bankId : BankId, productCode : ProductCode) : Option[Product]
|
||||
protected def getProductsFromProvider(bank : BankId) : Option[List[Product]]
|
||||
|
||||
// End of Trait
|
||||
|
||||
@ -6,7 +6,7 @@ import code.model.dataAccess.{MappedBankAccount, MappedBank}
|
||||
import code.model.{MappedTransaction, AccountId, BankId}
|
||||
import code.branches.{MappedBranch}
|
||||
import code.products.MappedProduct
|
||||
import code.products.Products.ProductId
|
||||
import code.products.Products.ProductCode
|
||||
|
||||
// , MappedDataLicense
|
||||
import code.util.Helper.convertToSmallestCurrencyUnits
|
||||
@ -126,7 +126,6 @@ object LocalMappedConnectorDataImport extends OBPDataImport with CreateViewImpls
|
||||
protected def createSaveableProducts(data : List[SandboxProductImport]) : Box[List[Saveable[ProductType]]] = {
|
||||
val mappedProducts = data.map(product => {
|
||||
MappedProduct.create
|
||||
.mProductId(product.id)
|
||||
.mBankId(product.bank_id)
|
||||
.mCode(product.code)
|
||||
.mName(product.name)
|
||||
@ -141,7 +140,7 @@ object LocalMappedConnectorDataImport extends OBPDataImport with CreateViewImpls
|
||||
val validationErrors = mappedProducts.flatMap(_.validate)
|
||||
|
||||
if (validationErrors.nonEmpty) {
|
||||
logger.error(s"Problem saving ${mappedProducts.flatMap(_.code)}")
|
||||
logger.error(s"Problem saving ${mappedProducts.flatMap(_.code.value)}")
|
||||
Failure(s"Errors: ${validationErrors.map(_.msg)}")
|
||||
} else {
|
||||
Full(mappedProducts.map(MappedSaveable(_)))
|
||||
|
||||
@ -2,7 +2,9 @@ package code.sandbox
|
||||
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.UUID
|
||||
import code.products.Products.Product
|
||||
import code.metadata.counterparties.{Counterparties, MapperCounterparties}
|
||||
import code.products.Products
|
||||
import code.products.Products.{ProductCode, Product}
|
||||
import code.bankconnectors.{OBPOffset, OBPLimit, Connector}
|
||||
import code.model.dataAccess.{APIUser, MappedAccountHolder, ViewImpl, OBPUser}
|
||||
import code.model._
|
||||
@ -18,7 +20,6 @@ object OBPDataImport extends SimpleInjector {
|
||||
|
||||
val importer = new Inject(buildOne _) {}
|
||||
|
||||
// TODO put this in props like main connector
|
||||
def buildOne : OBPDataImport = LocalMappedConnectorDataImport
|
||||
|
||||
}
|
||||
@ -242,7 +243,30 @@ trait OBPDataImport extends Loggable {
|
||||
logger.info("Hello from createProducts")
|
||||
// TODO Check the data.products is OK before calling the following
|
||||
|
||||
createSaveableProducts(data.products)
|
||||
logger.debug("Get existing products that match the bank id and product code")
|
||||
val existing = data.products.flatMap(p => Products.productsProvider.vend.getProduct(BankId(p.bank_id), ProductCode(p.code), true))
|
||||
|
||||
val allNewCodes = data.products.map(_.code)
|
||||
val emptyCodes = allNewCodes.filter(_.isEmpty)
|
||||
val uniqueNewCodes = data.products.map(_.code).distinct
|
||||
val duplicateCodes = allNewCodes diff uniqueNewCodes
|
||||
|
||||
if(!existing.isEmpty) {
|
||||
val existingCodes = existing.map(_.code.value)
|
||||
Failure(s"Existing Product codes were found for the bank $existingCodes")
|
||||
} else if (!emptyCodes.isEmpty){
|
||||
Failure(s"Product(s) with empty codes are not allowed")
|
||||
} else if(!duplicateCodes.isEmpty) {
|
||||
|
||||
val duplicateProducts = duplicateCodes.flatMap(d => data.products.filter(_.code == d))
|
||||
|
||||
duplicateProducts.foreach (dc => logger.error (s"Duplicate products found (duplicate code) in data.products Code: ${dc.code} Name: ${dc.name} Category: ${dc.category}"))
|
||||
Failure(s"Products must have unique codes. Duplicates found: $duplicateCodes")
|
||||
} else {
|
||||
createSaveableProducts(data.products)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
@ -313,6 +337,8 @@ trait OBPDataImport extends Loggable {
|
||||
final protected def createSaveableAccountResults(accs : List[SandboxAccountImport], banks : List[BankType])
|
||||
: Box[List[(Saveable[AccountType], List[Saveable[ViewType]], List[AccountOwnerEmail])]] = {
|
||||
|
||||
logger.info("Hello from createSaveableAccountResults")
|
||||
|
||||
val saveableAccounts =
|
||||
for(acc <- accs)
|
||||
yield for {
|
||||
@ -383,6 +409,7 @@ trait OBPDataImport extends Loggable {
|
||||
def randomCounterpartyHolderName(accNumber: Option[String]) : String = {
|
||||
val name = s"unknown_${UUID.randomUUID.toString}"
|
||||
accNumber.foreach(emptyHoldersAccNums.put(_, name))
|
||||
logger.debug(s"randomCounterpartyHolderName will return $name")
|
||||
name
|
||||
}
|
||||
|
||||
@ -400,7 +427,10 @@ trait OBPDataImport extends Loggable {
|
||||
case Some(accNum) if accNum.nonEmpty => {
|
||||
val existing = emptyHoldersAccNums.get(accNum)
|
||||
existing match {
|
||||
case Some(e) => e //holder already generated for an empty-name counterparty with the same account number
|
||||
case Some(existingValue) => {
|
||||
logger.debug (s"counterpartyHolder will be $existingValue")
|
||||
existingValue
|
||||
} //holder already generated for an empty-name counterparty with the same account number
|
||||
case None => randomCounterpartyHolderName(Some(accNum)) //generate a new counterparty name
|
||||
}
|
||||
}
|
||||
@ -468,11 +498,25 @@ trait OBPDataImport extends Loggable {
|
||||
logger.info(s"importData is saving ${transactions.size} transactions (and loading them again)")
|
||||
transactions.foreach { t =>
|
||||
t.save()
|
||||
//load it to force creation of metadata
|
||||
Connector.connector.vend.getTransaction(t.value.theBankId, t.value.theAccountId, t.value.theTransactionId)
|
||||
//load it to force creation of metadata (If we are using Mapped connector, MappedCounterpartyMetadata.create will be called)
|
||||
val lt = Connector.connector.vend.getTransaction(t.value.theBankId, t.value.theAccountId, t.value.theTransactionId)
|
||||
|
||||
|
||||
// Listing counterparties for debugging purposes.
|
||||
val counterParties = Connector.connector.vend.getOtherBankAccounts(lt.map(_.bankId).get,lt.map(_.accountId).get)
|
||||
|
||||
counterParties.foreach {
|
||||
cp => logger.debug(s"Label: ${cp.label} PublicAlias: ${cp.metadata.getPublicAlias} MoreInfo: ${cp.metadata.getMoreInfo} ImageURL: ${cp.metadata.getImageURL} URL: ${cp.metadata.getUrl}")
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
logger.info("Done")
|
||||
}
|
||||
|
||||
|
||||
@ -557,7 +601,7 @@ case class SandboxTransactionImport(
|
||||
details : SandboxAccountDetailsImport)
|
||||
|
||||
case class SandboxTransactionCounterparty(
|
||||
name : Option[String],
|
||||
name : Option[String], // Also known as Label
|
||||
account_number : Option[String])
|
||||
|
||||
case class SandboxAccountIdImport(
|
||||
@ -584,7 +628,6 @@ case class SandboxAtmImport(
|
||||
|
||||
|
||||
case class SandboxProductImport(
|
||||
id : String,
|
||||
bank_id : String,
|
||||
code: String,
|
||||
name : String,
|
||||
|
||||
@ -5,7 +5,7 @@ import java.util.Locale.Category
|
||||
import code.api.v1_4_0.JSONFactory1_4_0.{ProductJson, ProductsJson}
|
||||
import code.common.{License, Meta}
|
||||
import code.model.BankId
|
||||
import code.products.Products.ProductId
|
||||
import code.products.Products.ProductCode
|
||||
import code.products.Products.Product
|
||||
import code.products.{Products, ProductsProvider}
|
||||
import dispatch._
|
||||
@ -17,9 +17,8 @@ class ProductsTest extends V140ServerSetup {
|
||||
val BankWithoutLicense = BankId("bank-without-license")
|
||||
|
||||
// Have to repeat the constructor parameters from the trait
|
||||
case class ProductImpl(productId : ProductId,
|
||||
bankId: BankId,
|
||||
code : String,
|
||||
case class ProductImpl(bankId: BankId,
|
||||
code : ProductCode,
|
||||
name : String,
|
||||
category: String,
|
||||
family : String,
|
||||
@ -42,11 +41,11 @@ class ProductsTest extends V140ServerSetup {
|
||||
}
|
||||
|
||||
|
||||
val fakeProduct1 = ProductImpl(ProductId("prod1"), BankWithLicense, "code 1", "name 1", "cat 1", "family 1", "super family 1", "http://www.example.com/moreinfo1.html", fakeMeta)
|
||||
val fakeProduct2 = ProductImpl(ProductId("prod2"), BankWithLicense, "code 2", "name 2", "cat 1", "family 1", "super family 1", "http://www.example.com/moreinfo2.html", fakeMeta)
|
||||
val fakeProduct1 = ProductImpl(BankWithLicense, ProductCode("prod1"), "name 1", "cat 1", "family 1", "super family 1", "http://www.example.com/moreinfo1.html", fakeMeta)
|
||||
val fakeProduct2 = ProductImpl(BankWithLicense, ProductCode("prod2"), "name 2", "cat 1", "family 1", "super family 1", "http://www.example.com/moreinfo2.html", fakeMeta)
|
||||
|
||||
// Should not be returned (no license)
|
||||
val fakeProduct3 = ProductImpl(ProductId("prod3"), BankWithoutLicense, "code 3", "name 3", "cat 1", "family 1", "super family 1", "http://www.example.com/moreinfo3.html", fakeMetaNoLicense)
|
||||
val fakeProduct3 = ProductImpl(BankWithoutLicense, ProductCode("prod3"), "name 3", "cat 1", "family 1", "super family 1", "http://www.example.com/moreinfo3.html", fakeMetaNoLicense)
|
||||
|
||||
|
||||
// This mock provider is returning same branches for the fake banks
|
||||
@ -60,8 +59,8 @@ class ProductsTest extends V140ServerSetup {
|
||||
}
|
||||
|
||||
// Mock a badly behaving connector that returns data that doesn't have license.
|
||||
override protected def getProductFromProvider(productId: ProductId): Option[Product] = {
|
||||
productId match {
|
||||
override protected def getProductFromProvider(bank: BankId, code: ProductCode): Option[Product] = {
|
||||
bank match {
|
||||
case BankWithLicense => Some(fakeProduct1)
|
||||
case BankWithoutLicense=> Some(fakeProduct3) // In case the connector returns, the API should guard
|
||||
case _ => None
|
||||
@ -133,10 +132,10 @@ class ProductsTest extends V140ServerSetup {
|
||||
// Order of Products in the list is arbitrary
|
||||
products.size should equal(2)
|
||||
val first = products(0)
|
||||
if (first.id == fakeProduct1.productId.value) {
|
||||
if (first.code == fakeProduct1.code.value) {
|
||||
verifySameData(fakeProduct1, first)
|
||||
verifySameData(fakeProduct2, products(1))
|
||||
} else if (first.id == fakeProduct2.productId.value) {
|
||||
} else if (first.code == fakeProduct2.code.value) {
|
||||
verifySameData(fakeProduct2, first)
|
||||
verifySameData(fakeProduct1, products(1))
|
||||
} else {
|
||||
|
||||
@ -33,7 +33,6 @@ class MappedProductsProviderTest extends ServerSetup {
|
||||
.mBankId(bankIdX)
|
||||
.mCode("code-unlicensed")
|
||||
.mName("Name Unlicensed")
|
||||
.mProductId("id-u")
|
||||
.mCategory("Cat U")
|
||||
.mFamily("Family U")
|
||||
.mSuperFamily("Super Fam U")
|
||||
@ -48,7 +47,6 @@ class MappedProductsProviderTest extends ServerSetup {
|
||||
.mBankId(bankIdX)
|
||||
.mCode("code-1")
|
||||
.mName("Product Name 1")
|
||||
.mProductId("id-1")
|
||||
.mCategory("Cat 1")
|
||||
.mFamily("Family 1")
|
||||
.mSuperFamily("Super Fam 1")
|
||||
@ -61,7 +59,6 @@ class MappedProductsProviderTest extends ServerSetup {
|
||||
.mBankId(bankIdX)
|
||||
.mCode("code-2")
|
||||
.mName("Product Name 2")
|
||||
.mProductId("id-2")
|
||||
.mCategory("Cat 2")
|
||||
.mFamily("Family 2")
|
||||
.mSuperFamily("Super Fam 2")
|
||||
|
||||
@ -43,11 +43,11 @@ import code.branches.Branches
|
||||
import code.branches.Branches.{Branch, BranchId, countOfBranches}
|
||||
|
||||
import code.products.Products
|
||||
import code.products.Products.{Product, ProductId, countOfProducts}
|
||||
import code.products.Products.{Product, ProductCode, countOfProducts}
|
||||
|
||||
import code.model.dataAccess._
|
||||
import code.model.{TransactionId, AccountId, BankId}
|
||||
import code.products.Products.ProductId
|
||||
import code.products.Products.ProductCode
|
||||
import code.users.Users
|
||||
import code.views.Views
|
||||
import dispatch._
|
||||
@ -219,19 +219,19 @@ class SandboxDataLoadingTest extends FlatSpec with SendServerRequests with Shoul
|
||||
|
||||
// Get ids from input
|
||||
val bankId = BankId(product.bank_id)
|
||||
val productId = ProductId(product.id)
|
||||
val code = ProductCode(product.code)
|
||||
|
||||
println(s"bankId is $bankId")
|
||||
println(s"productId is $productId")
|
||||
println(s"code is $code")
|
||||
|
||||
|
||||
// check we have found a branch
|
||||
val foundProductOpt: Option[Product] = Products.productsProvider.vend.getProduct(productId)
|
||||
// check we have found a product
|
||||
val foundProductOpt: Option[Product] = Products.productsProvider.vend.getProduct(bankId, code)
|
||||
foundProductOpt.isDefined should equal(true)
|
||||
|
||||
val foundProduct = foundProductOpt.get
|
||||
foundProduct.bankId.toString should equal (product.bank_id)
|
||||
foundProduct.code should equal(product.code)
|
||||
foundProduct.code.value should equal(product.code)
|
||||
foundProduct.name should equal(product.name)
|
||||
foundProduct.category should equal(product.category)
|
||||
foundProduct.family should equal(product.family)
|
||||
@ -428,7 +428,6 @@ class SandboxDataLoadingTest extends FlatSpec with SendServerRequests with Shoul
|
||||
|
||||
|
||||
val product1AtBank1 = SandboxProductImport(
|
||||
id = "product1",
|
||||
bank_id = "bank1",
|
||||
code = "prd1",
|
||||
name = "product 1",
|
||||
@ -440,7 +439,6 @@ class SandboxDataLoadingTest extends FlatSpec with SendServerRequests with Shoul
|
||||
)
|
||||
|
||||
val product2AtBank1 = SandboxProductImport(
|
||||
id = "product2",
|
||||
bank_id = "bank1",
|
||||
code = "prd2",
|
||||
name = "Product 2",
|
||||
|
||||
Loading…
Reference in New Issue
Block a user