feature/Regulated entities CRUD - WIP

This commit is contained in:
Marko Milić 2023-11-29 22:37:12 +01:00
parent f2c2d348c8
commit 9653a676bf
9 changed files with 422 additions and 15 deletions

View File

@ -29,7 +29,6 @@ package bootstrap.liftweb
import java.io.{File, FileInputStream}
import java.util.stream.Collectors
import java.util.{Locale, TimeZone}
import code.CustomerDependants.MappedCustomerDependant
import code.DynamicData.DynamicData
import code.DynamicEndpoint.DynamicEndpoint
@ -107,6 +106,7 @@ import code.productcollectionitem.MappedProductCollectionItem
import code.productfee.ProductFee
import code.products.MappedProduct
import code.ratelimiting.RateLimiting
import code.regulatedentities.MappedRegulatedEntity
import code.remotedata.RemotedataActors
import code.scheduler.{DataBaseCleanerScheduler, DatabaseDriverScheduler, JobScheduler, MetricsArchiveScheduler}
import code.scope.{MappedScope, MappedUserScope}
@ -135,6 +135,7 @@ import code.webuiprops.WebUiProps
import com.openbankproject.commons.model.ErrorMessage
import com.openbankproject.commons.util.Functions.Implicits._
import com.openbankproject.commons.util.{ApiVersion, Functions}
import javax.mail.internet.MimeMessage
import net.liftweb.common._
import net.liftweb.db.{DB, DBLogEntry}
@ -1034,6 +1035,7 @@ object ToSchemify {
AuthUser,
JobScheduler,
MappedETag,
MappedRegulatedEntity,
AtmAttribute,
Admin,
MappedBank,

View File

@ -52,6 +52,33 @@ import scala.collection.immutable.List
object SwaggerDefinitionsJSON {
lazy val regulatedEntitiesJsonV510: RegulatedEntitiesJsonV510 = RegulatedEntitiesJsonV510(List(regulatedEntityJsonV510))
lazy val regulatedEntityJsonV510: RegulatedEntityJsonV510 = RegulatedEntityJsonV510(
entity_id = "0af807d7-3c39-43ef-9712-82bcfde1b9ca",
certificate_authority_ca_owner_id = "CY_CBC",
entity_certificate_public_key = "-----BEGIN CERTIFICATE-----MIICsjCCAZqgAwIBAgIGAYwQ62R0MA0GCSqGSIb3DQEBCwUAMBoxGDAWBgNVBAMMD2FwcC5leGFtcGxlLmNvbTAeFw0yMzExMjcxMzE1MTFaFw0yNTExMjYxMzE1MTFaMBoxGDAWBgNVBAMMD2FwcC5leGFtcGxlLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAK9WIodZHWzKyCcf9YfWEhPURbfO6zKuMqzHN27GdqHsVVEGxP4F/J4mso+0ENcRr6ur4u81iREaVdCc40rHDHVJNEtniD8Icbz7tcsqAewIVhc/q6WXGqImJpCq7hA0m247dDsaZT0lb/MVBiMoJxDEmAE/GYYnWTEn84R35WhJsMvuQ7QmLvNg6RkChY6POCT/YKe9NKwa1NqI1U+oA5RFzAaFtytvZCE3jtp+aR0brL7qaGfgxm6B7dEpGyhg0NcVCV7xMQNq2JxZTVdAr6lcsRGaAFulakmW3aNnmK+L35Wu8uW+OxNxwUuC6f3b4FVBa276FMuUTRfu7gc+k6kCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAAU5CjEyAoyTn7PgFpQD48ZNPuUsEQ19gzYgJvHMzFIoZ7jKBodjO5mCzWBcR7A4mpeAsdyiNBl2sTiZscSnNqxk61jVzP5Ba1D7XtOjjr7+3iqowrThj6BY40QqhYh/6BSY9fDzVZQiHnvlo6ZUM5kUK6OavZOovKlp5DIl5sGqoP0qAJnpQ4nhB2WVVsKfPlOXc+2KSsbJ23g9l8zaTMr+X0umlvfEKqyEl1Fa2L1dO0y/KFQ+ILmxcZLpRdq1hRAjd0quq9qGC8ucXhRWDgM4hslVpau0da68g0aItWNez3mc5lB82b3dcZpFMzO41bgw7gvw10AvvTfQDqEYIuQ==-----END CERTIFICATE-----",
entity_code = "PSD_PICY_CBC!12345",
entity_type = "PSD_PI",
entity_address = "EXAMPLE COMPANY LTD, 5 SOME STREET",
entity_town_city = "SOME CITY",
entity_post_code = "1060",
entity_country = "CY",
entity_web_site = "www.example.com",
services = json.parse("""[{"CY":["PS_010","PS_020","PS_03C","PS_04C"]}]""")
)
lazy val regulatedEntityPostJsonV510: RegulatedEntityPostJsonV510 = RegulatedEntityPostJsonV510(
certificate_authority_ca_owner_id = "CY_CBC",
entity_certificate_public_key = "-----BEGIN CERTIFICATE-----MIICsjCCAZqgAwIBAgIGAYwQ62R0MA0GCSqGSIb3DQEBCwUAMBoxGDAWBgNVBAMMD2FwcC5leGFtcGxlLmNvbTAeFw0yMzExMjcxMzE1MTFaFw0yNTExMjYxMzE1MTFaMBoxGDAWBgNVBAMMD2FwcC5leGFtcGxlLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAK9WIodZHWzKyCcf9YfWEhPURbfO6zKuMqzHN27GdqHsVVEGxP4F/J4mso+0ENcRr6ur4u81iREaVdCc40rHDHVJNEtniD8Icbz7tcsqAewIVhc/q6WXGqImJpCq7hA0m247dDsaZT0lb/MVBiMoJxDEmAE/GYYnWTEn84R35WhJsMvuQ7QmLvNg6RkChY6POCT/YKe9NKwa1NqI1U+oA5RFzAaFtytvZCE3jtp+aR0brL7qaGfgxm6B7dEpGyhg0NcVCV7xMQNq2JxZTVdAr6lcsRGaAFulakmW3aNnmK+L35Wu8uW+OxNxwUuC6f3b4FVBa276FMuUTRfu7gc+k6kCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAAU5CjEyAoyTn7PgFpQD48ZNPuUsEQ19gzYgJvHMzFIoZ7jKBodjO5mCzWBcR7A4mpeAsdyiNBl2sTiZscSnNqxk61jVzP5Ba1D7XtOjjr7+3iqowrThj6BY40QqhYh/6BSY9fDzVZQiHnvlo6ZUM5kUK6OavZOovKlp5DIl5sGqoP0qAJnpQ4nhB2WVVsKfPlOXc+2KSsbJ23g9l8zaTMr+X0umlvfEKqyEl1Fa2L1dO0y/KFQ+ILmxcZLpRdq1hRAjd0quq9qGC8ucXhRWDgM4hslVpau0da68g0aItWNez3mc5lB82b3dcZpFMzO41bgw7gvw10AvvTfQDqEYIuQ==-----END CERTIFICATE-----",
entity_code = "PSD_PICY_CBC!12345",
entity_type = "PSD_PI",
entity_address = "EXAMPLE COMPANY LTD, 5 SOME STREET",
entity_town_city = "SOME CITY",
entity_post_code = "1060",
entity_country = "CY",
entity_web_site = "www.example.com",
services = """[{"CY":["PS_010","PS_020","PS_03C","PS_04C"]}]"""
)
val license = License(
id = licenseIdExample.value,

View File

@ -66,6 +66,11 @@ object RoleCombination {
object ApiRole extends MdcLoggable{
case class CanCreateRegulatedEntity(requiresBankId: Boolean = false) extends ApiRole
lazy val canCreateRegulatedEntity = CanCreateRegulatedEntity()
case class CanDeleteRegulatedEntity(requiresBankId: Boolean = false) extends ApiRole
lazy val canDeleteRegulatedEntity = CanDeleteRegulatedEntity()
case class CanSearchWarehouse(requiresBankId: Boolean = false) extends ApiRole
lazy val canSearchWarehouse = CanSearchWarehouse()

View File

@ -0,0 +1,62 @@
package code.api.util.newstyle
import code.api.util.APIUtil.{OBPReturnType, unboxFull}
import code.api.util.{APIUtil, CallContext}
import code.consumer.Consumers
import code.model.{AppType, Consumer}
import code.regulatedentities.MappedRegulatedEntityProvider
import com.openbankproject.commons.model.RegulatedEntityTrait
import scala.concurrent.Future
object RegulatedEntityNewStyle {
import com.openbankproject.commons.ExecutionContext.Implicits.global
def createRegulatedEntityNewStyle(certificateAuthorityCaOwnerId: Option[String],
entityCertificatePublicKey: Option[String],
entityCode: Option[String],
entityType: Option[String],
entityAddress: Option[String],
entityTownCity: Option[String],
entityPostCode: Option[String],
entityCountry: Option[String],
entityWebSite: Option[String],
services: Option[String],
callContext: Option[CallContext]): OBPReturnType[RegulatedEntityTrait] = {
Future {
MappedRegulatedEntityProvider.createRegulatedEntity(
certificateAuthorityCaOwnerId: Option[String],
entityCertificatePublicKey: Option[String],
entityCode: Option[String],
entityType: Option[String],
entityAddress: Option[String],
entityTownCity: Option[String],
entityPostCode: Option[String],
entityCountry: Option[String],
entityWebSite: Option[String],
services: Option[String]
) map {
(_, callContext)
}
} map {
unboxFull(_)
}
}
def deleteRegulatedEntityNewStyle(id: String,
callContext: Option[CallContext]
): OBPReturnType[Boolean] = {
Future {
MappedRegulatedEntityProvider.deleteRegulatedEntity(
id
) map {
(_, callContext)
}
} map {
unboxFull(_)
}
}
}

View File

@ -2,7 +2,7 @@ package code.api.v5_1_0
import code.api.Constant
import code.api.ResourceDocs1_4_0.SwaggerDefinitionsJSON.{apiCollectionJson400, apiCollectionsJson400, apiInfoJson400, postApiCollectionJson400, revokedConsentJsonV310, _}
import code.api.ResourceDocs1_4_0.SwaggerDefinitionsJSON._
import code.api.util.APIUtil._
import code.api.util.ApiRole._
import code.api.util.ApiTag._
@ -10,19 +10,20 @@ import code.api.util.ErrorMessages.{$UserNotLoggedIn, BankNotFound, ConsentNotFo
import code.api.util.FutureUtil.{EndpointContext, EndpointTimeout}
import code.api.util.NewStyle.HttpCode
import code.api.util._
import code.api.v2_0_0.{EntitlementJSONs, JSONFactory200}
import code.api.util.newstyle.RegulatedEntityNewStyle.{createRegulatedEntityNewStyle, deleteRegulatedEntityNewStyle}
import code.api.v3_0_0.JSONFactory300
import code.api.v3_0_0.JSONFactory300.createAggregateMetricJson
import code.api.v3_1_0.ConsentJsonV310
import code.api.v3_1_0.JSONFactory310.createBadLoginStatusJson
import code.api.v4_0_0.{JSONFactory400, PostApiCollectionJson400}
import code.api.v5_0_0.ConsentJsonV500
import code.api.v5_1_0.JSONFactory510.{createRegulatedEntitiesJson, createRegulatedEntityJson}
import code.atmattribute.AtmAttribute
import code.bankconnectors.Connector
import code.consent.Consents
import code.loginattempts.LoginAttempt
import code.metrics.APIMetrics
import code.model.dataAccess.MappedBankAccount
import code.regulatedentities.MappedRegulatedEntityProvider
import code.transactionrequests.TransactionRequests.TransactionRequestTypes.{apply => _}
import code.userlocks.UserLocksProvider
import code.users.Users
@ -32,12 +33,10 @@ import code.views.Views
import code.views.system.{AccountAccess, ViewDefinition}
import com.github.dwickern.macros.NameOf.nameOf
import com.openbankproject.commons.ExecutionContext.Implicits.global
import com.openbankproject.commons.dto.CustomerAndAttribute
import com.openbankproject.commons.model.enums.{AtmAttributeType, UserAttributeType}
import com.openbankproject.commons.model.{AtmId, AtmT, BankId, Permission}
import com.openbankproject.commons.model._
import com.openbankproject.commons.util.{ApiVersion, ScannedApiVersion}
import net.liftweb.common.{Box, Full}
import net.liftweb.http.S
import net.liftweb.common.Full
import net.liftweb.http.rest.RestHelper
import net.liftweb.json.parse
import net.liftweb.mapper.By
@ -134,19 +133,115 @@ trait APIMethods510 {
|* Regulated Entities
""",
EmptyBody,
EmptyBody,
regulatedEntitiesJsonV510,
List(UnknownError),
apiTagDirectory :: apiTagApi :: Nil)
lazy val regulatedEntities: OBPEndpoint = {
case "regulated-entities" :: Nil JsonGet _ =>
cc => implicit val ec = EndpointContext(Some(cc))
APIUtil.scalaFutureToBoxedJsonResponse(for {
regulatedEntities <- Future(APIUtil.getPropsValue("regulated_entities", "[]"))
for {
entities: List[RegulatedEntityTrait] <- Future(MappedRegulatedEntityProvider.getRegulatedEntities())
} yield {
(parse(regulatedEntities), HttpCode.`200`(cc.callContext))
})
(createRegulatedEntitiesJson(entities), HttpCode.`200`(cc.callContext))
}
}
staticResourceDocs += ResourceDoc(
createRegulatedEntity,
implementedInApiVersion,
nameOf(createRegulatedEntity),
"POST",
"/regulated-entities",
"Create Regulated Entity",
s""" Create Regulated Entity
|
|${authenticationRequiredMessage(true)}
|
|""",
List(regulatedEntityPostJsonV510),
regulatedEntitiesJsonV510,
List(
$UserNotLoggedIn,
UserHasMissingRoles,
InvalidJsonFormat,
UnknownError
),
List(apiTagDirectory, apiTagApi),
Some(List(canCreateRegulatedEntity))
)
lazy val createRegulatedEntity: OBPEndpoint = {
case "regulated-entities" :: Nil JsonPost json -> _ => {
cc =>
implicit val ec = EndpointContext(Some(cc))
val failMsg = s"$InvalidJsonFormat The Json body should be the $RegulatedEntityPostJsonV510 "
for {
postedData <- NewStyle.function.tryons(failMsg, 400, cc.callContext) {
json.extract[RegulatedEntityPostJsonV510]
}
failMsg = s"$InvalidJsonFormat The `services` field is not valid JSON"
_ <- NewStyle.function.tryons(failMsg, 400, cc.callContext) {
parse(postedData.services)
}
(entity, callContext) <- createRegulatedEntityNewStyle(
certificateAuthorityCaOwnerId = Some(postedData.certificate_authority_ca_owner_id),
entityCertificatePublicKey = Some(postedData.entity_certificate_public_key),
entityCode = Some(postedData.entity_code),
entityType = Some(postedData.entity_type),
entityAddress = Some(postedData.entity_address),
entityTownCity = Some(postedData.entity_town_city),
entityPostCode = Some(postedData.entity_post_code),
entityCountry = Some(postedData.entity_country),
entityWebSite = Some(postedData.entity_web_site),
services = Some(postedData.services),
cc.callContext
)
} yield {
(createRegulatedEntityJson(entity), HttpCode.`201`(callContext))
}
}
}
resourceDocs += ResourceDoc(
deleteRegulatedEntity,
implementedInApiVersion,
nameOf(deleteRegulatedEntity),
"DELETE",
"/regulated-entities/REGULATED_ENTITY_ID",
"Delete Regulated Entity",
s"""Delete Regulated Entity specified by REGULATED_ENTITY_ID
|
|${authenticationRequiredMessage(true)}
|""".stripMargin,
EmptyBody,
EmptyBody,
List(
$UserNotLoggedIn,
UserHasMissingRoles,
InvalidConnectorResponse,
UnknownError
),
List(apiTagDirectory, apiTagApi),
Some(List(canDeleteRegulatedEntity)))
lazy val deleteRegulatedEntity: OBPEndpoint = {
case "regulated-entities" :: regulatedEntityId :: Nil JsonDelete _ => {
cc =>
implicit val ec = EndpointContext(Some(cc))
for {
(deleted, callContext) <- deleteRegulatedEntityNewStyle(
regulatedEntityId: String,
cc.callContext: Option[CallContext]
)
} yield {
org.scalameta.logger.elem(deleted)
(Full(deleted), HttpCode.`204`(callContext))
}
}
}
staticResourceDocs += ResourceDoc(
waitingForGodot,

View File

@ -37,14 +37,15 @@ import code.atmattribute.AtmAttribute
import code.atms.Atms.Atm
import code.users.UserAttribute
import code.views.system.{AccountAccess, ViewDefinition}
import com.openbankproject.commons.model.{Address, AtmId, AtmT, BankId, BankIdAccountId, Customer, Location, Meta}
import com.openbankproject.commons.model.{Address, AtmId, AtmT, BankId, BankIdAccountId, Customer, Location, Meta, RegulatedEntityTrait}
import com.openbankproject.commons.util.{ApiVersion, ScannedApiVersion}
import java.util.Date
import code.consent.MappedConsent
import code.metrics.APIMetric
import net.liftweb.common.Box
import net.liftweb.json.parse
import net.liftweb.json
import net.liftweb.json.{JValue, parse}
import scala.collection.immutable.List
import scala.util.Try
@ -64,6 +65,34 @@ case class APIInfoJsonV510(
energy_source : EnergySource400,
resource_docs_requires_role: Boolean
)
case class RegulatedEntityJsonV510(
entity_id: String,
certificate_authority_ca_owner_id: String,
entity_certificate_public_key: String,
entity_code: String,
entity_type: String,
entity_address: String,
entity_town_city: String,
entity_post_code: String,
entity_country: String,
entity_web_site: String,
services: JValue
)
case class RegulatedEntityPostJsonV510(
certificate_authority_ca_owner_id: String,
entity_certificate_public_key: String,
entity_code: String,
entity_type: String,
entity_address: String,
entity_town_city: String,
entity_post_code: String,
entity_country: String,
entity_web_site: String,
services: String
)
case class RegulatedEntitiesJsonV510(entities: List[RegulatedEntityJsonV510])
case class WaitingForGodotJsonV510(sleep_in_milliseconds: Long)
case class CertificateInfoJsonV510(
@ -534,6 +563,25 @@ object JSONFactory510 extends CustomJsonFormats {
UserAttributesResponseJsonV510(userAttribute.map(createUserAttributeJson))
}
def createRegulatedEntityJson(entity: RegulatedEntityTrait): RegulatedEntityJsonV510 = {
RegulatedEntityJsonV510(
entity_id = entity.entityId,
certificate_authority_ca_owner_id = entity.certificateAuthorityCaOwnerId,
entity_certificate_public_key = entity.entityCertificatePublicKey,
entity_code = entity.entityCode,
entity_type = entity.entityType,
entity_address = entity.entityAddress,
entity_town_city = entity.entityTownCity,
entity_post_code = entity.entityPostCode,
entity_country = entity.entityCountry,
entity_web_site = entity.entityWebSite,
services = json.parse(entity.services)
)
}
def createRegulatedEntitiesJson(entities: List[RegulatedEntityTrait]): RegulatedEntitiesJsonV510 = {
RegulatedEntitiesJsonV510(entities.map(createRegulatedEntityJson))
}
def createMetricJson(metric: APIMetric): MetricJsonV510 = {
MetricJsonV510(
user_id = metric.getUserId(),

View File

@ -0,0 +1,120 @@
package code.regulatedentities
import code.util.MappedUUID
import com.openbankproject.commons.model.RegulatedEntityTrait
import net.liftweb.common.Box
import net.liftweb.common.Box.tryo
import net.liftweb.mapper._
import scala.concurrent.Future
object MappedRegulatedEntityProvider extends RegulatedEntityProvider {
def getRegulatedEntities(): List[RegulatedEntityTrait] = {
MappedRegulatedEntity.findAll()
}
override def createRegulatedEntity(certificateAuthorityCaOwnerId: Option[String],
entityCertificatePublicKey: Option[String],
entityCode: Option[String],
entityType: Option[String],
entityAddress: Option[String],
entityTownCity: Option[String],
entityPostCode: Option[String],
entityCountry: Option[String],
entityWebSite: Option[String],
services: Option[String]
): Box[RegulatedEntityTrait] = {
tryo {
val entity = MappedRegulatedEntity.create
certificateAuthorityCaOwnerId match {
case Some(v) => entity.CertificateAuthorityCaOwnerId(v)
case None =>
}
entityCertificatePublicKey match {
case Some(v) => entity.EntityCertificatePublicKey(v)
case None =>
}
entityCode match {
case Some(v) => entity.EntityCode(v)
case None =>
}
entityType match {
case Some(v) => entity.EntityType(v)
case None =>
}
entityAddress match {
case Some(v) => entity.EntityAddress(v)
case None =>
}
entityTownCity match {
case Some(v) => entity.EntityTownCity(v)
case None =>
}
entityPostCode match {
case Some(v) => entity.EntityPostCode(v)
case None =>
}
entityCountry match {
case Some(v) => entity.EntityCountry(v)
case None =>
}
entityWebSite match {
case Some(v) => entity.EntityWebSite(v)
case None =>
}
services match {
case Some(v) => entity.Services(v)
case None =>
}
if (entity.validate.isEmpty) {
entity.saveMe()
} else {
throw new Error(entity.validate.map(_.msg.toString()).mkString(";"))
}
}
}
override def deleteRegulatedEntity(id: String): Box[Boolean] = {
tryo(
MappedRegulatedEntity.bulkDelete_!!(By(MappedRegulatedEntity.EntityId, id))
)
}
}
class MappedRegulatedEntity extends RegulatedEntityTrait with LongKeyedMapper[MappedRegulatedEntity] with IdPK {
override def getSingleton = MappedRegulatedEntity
object EntityId extends MappedUUID(this)
object CertificateAuthorityCaOwnerId extends MappedString(this, 50)
object EntityCode extends MappedString(this, 50)
object EntityCertificatePublicKey extends MappedText(this)
object EntityType extends MappedString(this, 50)
object EntityAddress extends MappedString(this, 50)
object EntityTownCity extends MappedString(this, 50)
object EntityPostCode extends MappedString(this, 50)
object EntityCountry extends MappedString(this, 50)
object EntityWebSite extends MappedString(this, 50)
object Services extends MappedText(this)
override def entityId: String = EntityId.get
override def certificateAuthorityCaOwnerId: String = CertificateAuthorityCaOwnerId.get
override def entityCode: String = EntityCode.get
override def entityCertificatePublicKey: String = EntityCertificatePublicKey.get
override def entityType: String = EntityType.get
override def entityAddress: String = EntityAddress.get
override def entityTownCity: String = EntityTownCity.get
override def entityPostCode: String = EntityPostCode.get
override def entityCountry: String = EntityCountry.get
override def entityWebSite: String = EntityWebSite.get
override def services: String = Services.get
}
object MappedRegulatedEntity extends MappedRegulatedEntity with LongKeyedMetaMapper[MappedRegulatedEntity] {
override def dbTableName = "RegulatedEntity" // define the DB table name
override def dbIndexes = Index(CertificateAuthorityCaOwnerId) :: super.dbIndexes
}

View File

@ -0,0 +1,33 @@
package code.regulatedentities
import com.openbankproject.commons.model.RegulatedEntityTrait
import net.liftweb.common.{Box, Logger}
import net.liftweb.util.SimpleInjector
object RegulatedEntityX extends SimpleInjector {
val regulatedEntityProvider = new Inject(buildOne _) {}
def buildOne: RegulatedEntityProvider = MappedRegulatedEntityProvider
}
/* For ProductFee */
trait RegulatedEntityProvider {
private val logger = Logger(classOf[RegulatedEntityProvider])
def getRegulatedEntities(): List[RegulatedEntityTrait]
def createRegulatedEntity(certificateAuthorityCaOwnerId: Option[String],
entityCertificatePublicKey: Option[String],
entityCode: Option[String],
entityType: Option[String],
entityAddress: Option[String],
entityTownCity: Option[String],
entityPostCode: Option[String],
entityCountry: Option[String],
entityWebSite: Option[String],
services: Option[String]
): Box[RegulatedEntityTrait]
def deleteRegulatedEntity(id: String): Box[Boolean]
}

View File

@ -96,6 +96,21 @@ trait AccountApplication {
def status: String
}
trait RegulatedEntityTrait {
def entityId: String
def certificateAuthorityCaOwnerId: String
def entityCode: String
def entityCertificatePublicKey: String
def entityType: String
def entityAddress: String
def entityTownCity: String
def entityPostCode: String
def entityCountry: String
def entityWebSite: String
def services: String
}
trait UserAttributeTrait {
def userAttributeId: String
def userId: String