diff --git a/CHANGELOG.md b/CHANGELOG.md index 085f918b7..876b44ef1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,7 @@ # Changelog All notable changes to this project will be documented in this file. +## [6.19.0] (https://github.com/Backbase/stream-services/compare/6.18.0...6.19.0) +- Integrate Customer Profile Service into Legal Entity Saga and Legal Entity Saga V2 ingestion ## [6.18.0](https://github.com/Backbase/stream-services/compare/6.17.0...6.18.0) ### Added diff --git a/api/stream-legal-entity/openapi.yaml b/api/stream-legal-entity/openapi.yaml index 43d609b4b..f6a1bf515 100644 --- a/api/stream-legal-entity/openapi.yaml +++ b/api/stream-legal-entity/openapi.yaml @@ -1132,6 +1132,10 @@ components: type: array items: $ref: '#/components/schemas/JobProfileUser' + parties: + type: array + items: + $ref: '#/components/schemas/Party' referenceJobRoles: type: array items: @@ -1205,6 +1209,10 @@ components: type: array items: $ref: '#/components/schemas/User' + parties: + type: array + items: + $ref: '#/components/schemas/Party' masterServiceAgreement: $ref: '#/components/schemas/ServiceAgreementV2' contacts: @@ -3102,7 +3110,440 @@ components: description: Subscription identifier required: - identifier - + Party: + type: object + properties: + partyId: + type: string + description: Core System Identifier + maxLength: 100 + isCustomer: + type: boolean + description: Indicates if it is a customer or not. + partyType: + type: string + enum: + - PERSON + - ORGANISATION + description: Type of party in different business contexts. + state: + type: string + enum: + - ENROLLED + - EXITED + subState: + type: string + enum: + - OPENED + - APPROVED + - ACTIVE + - DORMANT + - CLOSED + description: Status of the party. + openingDateTime: + type: string + format: date-time + description: Date on which the party and related basic services are effectively operational for the party. + closingDateTime: + type: string + format: date-time + description: Date on which the party and related services cease effectively to be operational for the party. + liveDateTime: + type: string + format: date-time + description: Date of the first movement on the party. + approvedDateTime: + type: string + format: date-time + description: Date on which the party and related basic services are approved. + lastUpdatedDateTime: + type: string + format: date-time + description: Date on which there was any update to the party and related basic services. + preferredLanguage: + type: string + maxLength: 50 + description: Language preferred by party. + notes: + type: string + maxLength: 100 + description: Notes on the party. + organisation: + $ref: '#/components/schemas/Organization' + person: + $ref: '#/components/schemas/Person' + partyPartyRelationships: + type: array + items: + $ref: '#/components/schemas/PartyPartyRelationship' + electronicAddresses: + $ref: '#/components/schemas/ElectronicAddress' + postalAddresses: + type: array + items: + $ref: '#/components/schemas/PartyPostalAddress' + phoneNumbers: + type: array + items: + $ref: '#/components/schemas/PartyPhoneNumber' + customFields: + type: object + additionalProperties: + type: string + required: + - partyId + - isCustomer + - partyType + Organization: + type: object + properties: + name: + type: string + description: Organisation Name + maxLength: 64 + type: + type: string + maxLength: 64 + description: Specifies a type of organisation. + sector: + type: string + maxLength: 255 + description: Sector of business of the organisation, for example, pharmaceutical. (ISO20022) + establishmentDate: + type: string + format: date + description: Date when the organisation was established. + legalStructure: + type: object + properties: + type: + type: string + maxLength: 64 + description: Individual, Partnership and Corporation + required: + - type + identifications: + type: array + description: List of specific identification assigned to a party. + minItems: 1 + items: + $ref: '#/components/schemas/Identification' + required: + - name + - identifications + Identification: + type: object + properties: + identificationType: + type: string + enum: + - TaxIdentificationNumber + - NationalRegistrationNumber + - RegistrationAuthorityIdentification + - LegalEntityIdentifier + - AlienRegistrationNumber + - PassportNumber + - TaxExemptionIdentificationNumber + - CorporateIdentification + - DriverLicenseNumber + - ForeignInvestmentIdentityNumber + - SocialSecurityNumber + - IdentityCardNumber + - Concat + - NationalRegistrationIdentificationNumber + - CustomerIdentificationNumber + - EmployeeIdentificationNumber + - NationalIdentityNumber + - TelephoneNumber + - EmployerIdentificationNumber + - CentralBankIdentificationNumber + - ClearingIdentificationNumber + - BankPartyIdentification + - CertificateOfIncorporationNumber + - CountryIdentificationCode + - CustomerNumber + - DataUniversalNumberingSystem + - GS1GLNIdentifier + - ELF + - SIREN + - SIRET + - MIC + - BICFI + - DUNS + - EANGLN + description: Identification Type of the identity document. + identificationNumber: + type: string + maxLength: 100 + description: Number or code assigned by the government authority to an entity + issuingCountry: + type: string + maxLength: 100 + description: Identifies issuing country of the identity document. + issuingAuthority: + type: string + maxLength: 100 + description: Identifies issuing authority of the identity document. + issueDate: + type: string + format: date + description: Identifies issue date of the identity document. + expiryDate: + type: string + format: date + description: Identifies expiry date of the identity document. + required: + - identificationType + - identificationNumber + Person: + type: object + properties: + birthDate: + type: string + format: date + description: Indicates person birthdate + gender: + type: string + enum: + - MALE + - FEMALE + - NON_BINARY + description: Person's gender + personName: + $ref: '#/components/schemas/PersonName' + demographics: + $ref: '#/components/schemas/Demographics' + identifications: + type: array + description: List of specific identification assigned to a party. + minItems: 1 + items: + $ref: '#/components/schemas/Identification' + required: + - personName + - identifications + PersonName: + type: object + properties: + salutation: + type: string + maxLength: 20 + firstName: + type: string + description: First name of the person. + maxLength: 64 + middleName: + type: string + description: Middle name of the person. + maxLength: 64 + familyName: + type: string + description: Family name of the person. + maxLength: 64 + required: + - firstName + Demographics: + type: object + properties: + occupation: + type: object + properties: + employment: + type: string + description: Type of employment of the party. + maxLength: 100 + employer: + type: string + description: Details of the party's employment. + maxLength: 100 + education: + type: object + properties: + educationLevel: + type: string + description: Party's education level, such as qualifications and certifications. + maxLength: 100 + yearOfPassing: + type: string + maxLength: 20 + description: Year of completing the qualification. + PartyPartyRelationship: + type: object + properties: + partyId: + type: string + maxLength: 100 + description: Core System Id + partyType: + type: string + enum: + - PERSON + - ORGANISATION + description: Specifies the type of party in different business contexts. + partyRole: + type: string + description: Identifies role of a party + enum: + - COMPANY_ROLE + - COMPANY_OWNERSHIP + - COMPANY_CONTROL_PERSON + - COMPANY_SIGNATORY + - GUARDIAN + roleStartDate: + type: string + format: date + description: Identifies start date of the party role + roleEndDate: + type: string + format: date + description: Identifies end date of the party role + ownershipPercent: + type: integer + description: Identifies the percentage of stake/ownership for a related person/organisation in this organisation. Total ownership percent must not exceed 100% + minimum: 0 + maximum: 100 + required: + - partyId + - partyRole + ElectronicAddress: + type: object + description: Address which is accessed by electronic means. + properties: + emails: + type: array + description: Email(s) of the party. + items: + $ref: '#/components/schemas/Email' + urls: + type: array + description: Address for the Universal Resource Locator (URL), used over the www (HTTP) service. + items: + $ref: '#/components/schemas/Url' + Email: + type: object + properties: + type: + type: string + enum: + - WORK + - PERSONAL + - HOME + - OTHERS + description: Identifies the type of email. Possible values - WORK, PERSONAL, HOME, OTHERS. + primary: + type: boolean + description: Flag denoting whether this is the primary email address of the party. + address: + type: string + maxLength: 100 + description: Address for electronic mail (e-mail) (ISO20022). + required: + - type + - address + Url: + type: object + properties: + type: + type: string + enum: + - WORK + - PERSONAL + description: Identifies the type of URL. Possible Values - WORK, PERSONAL. + primary: + type: boolean + description: Flag denoting whether this is the primary url address of the party. + address: + type: string + maxLength: 255 + description: Address for the Universal Resource Locator (URL), used over the www (HTTP) service. + required: + - address + - type + PartyPostalAddress: + type: object + properties: + type: + type: string + enum: + - Business + - Correspondence + - DeliveryTo + - MailTo + - POBox + - Postal + - Residential + - Statement + description: Identifies the type of postal address. + primary: + type: boolean + description: Flag denoting whether this is the primary postal address of the party. + department: + type: string + maxLength: 100 + description: Identification of a division of a large organisation or building (ISO20022). + subDepartment: + type: string + maxLength: 100 + description: Identification of a sub-division of a large organisation or building (ISO20022). + addressLine: + type: string + maxLength: 100 + description: Information that locates and identifies a specific address, as defined by postal services, presented in free format text (ISO20022). + buildingNumber: + type: string + maxLength: 50 + description: Number that identifies the position of a building on a street (ISO20022). + streetName: + type: string + maxLength: 100 + description: Name of a street or thoroughfare (ISO20022). + townName: + type: string + maxLength: 100 + description: Name of a built-up area, with defined boundaries, and a local government (ISO20022). + postalCode: + type: string + maxLength: 12 + description: Identifier consisting of a group of letters and/or numbers that is added to a postal address to assist the sorting of mail (ISO20022). + countrySubDivision: + type: string + maxLength: 100 + description: Identifies name of a country subdivision such as state, region, county. For example - Oregon. + country: + type: string + maxLength: 100 + description: Country name. + required: + - type + PartyPhoneNumber: + type: object + properties: + type: + type: string + enum: + - MOBILE + - HOME + - WORK + - OTHER + description: Identifies the type of the phone address. For example - MOBILE, LANDLINE, HOME, WORK, FAX. + primary: + type: boolean + description: Flag denoting if its the primary phone address of the party. + number: + type: string + maxLength: 50 + description: Collection of information that identifies a phone address, as defined by telecom services (ISO20022). + countryCode: + type: string + maxLength: 3 + description: Phone’s country calling code. Country calling code can be obtained from https://countrycode.org/. + countryIsoCode: + type: string + maxLength: 3 + description: Phone’s ISO country code. Country code can be obtained from the United Nations (ISO 3166, Alpha-2 code)- https://www.iban.com/country-codes. + required: + - type + - number examples: RootLegalEntityHierarchyExample: description: "Example Request for setting up Root Legal Entity Structure as described on Backbase Community" diff --git a/pom.xml b/pom.xml index 3eb5baa97..c12340fec 100644 --- a/pom.xml +++ b/pom.xml @@ -35,6 +35,7 @@ stream-audiences stream-compositions stream-plan-manager + stream-customer-profile diff --git a/stream-customer-profile/customer-profile-core/pom.xml b/stream-customer-profile/customer-profile-core/pom.xml new file mode 100644 index 000000000..0de7b95b4 --- /dev/null +++ b/stream-customer-profile/customer-profile-core/pom.xml @@ -0,0 +1,57 @@ + + + 4.0.0 + + + com.backbase.stream + stream-customer-profile + 6.18.0 + + + customer-profile-core + jar + Stream :: Customer Profile Core + + + true + + + + + com.backbase.stream + legal-entity-model + ${project.version} + compile + + + + org.mapstruct + mapstruct + + + org.springframework.boot + spring-boot-configuration-processor + true + + + + com.backbase.stream + stream-dbs-clients + ${project.version} + compile + + + org.mapstruct + mapstruct-processor + provided + + + + com.backbase.stream + stream-test-support + ${project.version} + test + + + + diff --git a/stream-customer-profile/customer-profile-core/src/main/java/com/backbase/stream/configuration/CustomerProfileConfiguration.java b/stream-customer-profile/customer-profile-core/src/main/java/com/backbase/stream/configuration/CustomerProfileConfiguration.java new file mode 100644 index 000000000..5c457fd8b --- /dev/null +++ b/stream-customer-profile/customer-profile-core/src/main/java/com/backbase/stream/configuration/CustomerProfileConfiguration.java @@ -0,0 +1,22 @@ +package com.backbase.stream.configuration; + +import com.backbase.customerprofile.api.integration.v1.PartyManagementIntegrationApi; +import com.backbase.stream.clients.config.CustomerProfileClientConfig; +import com.backbase.stream.mapper.PartyMapper; +import com.backbase.stream.service.CustomerProfileService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +@Slf4j +@EnableConfigurationProperties(CustomerProfileClientConfig.class) +public class CustomerProfileConfiguration { + + @Bean + public CustomerProfileService createCustomerProfileService( + PartyManagementIntegrationApi partyManagementIntegrationApi, PartyMapper partyMapper) { + return new CustomerProfileService(partyManagementIntegrationApi, partyMapper); + } +} diff --git a/stream-customer-profile/customer-profile-core/src/main/java/com/backbase/stream/configuration/package-info.java b/stream-customer-profile/customer-profile-core/src/main/java/com/backbase/stream/configuration/package-info.java new file mode 100644 index 000000000..3b4bde136 --- /dev/null +++ b/stream-customer-profile/customer-profile-core/src/main/java/com/backbase/stream/configuration/package-info.java @@ -0,0 +1 @@ +package com.backbase.stream.configuration; \ No newline at end of file diff --git a/stream-customer-profile/customer-profile-core/src/main/java/com/backbase/stream/mapper/PartyMapper.java b/stream-customer-profile/customer-profile-core/src/main/java/com/backbase/stream/mapper/PartyMapper.java new file mode 100644 index 000000000..ed43eaea8 --- /dev/null +++ b/stream-customer-profile/customer-profile-core/src/main/java/com/backbase/stream/mapper/PartyMapper.java @@ -0,0 +1,15 @@ +package com.backbase.stream.mapper; + +import com.backbase.customerprofile.api.integration.v1.model.PartyUpsertDto; +import com.backbase.stream.legalentity.model.Party; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.MappingConstants; + +@Mapper(componentModel = MappingConstants.ComponentModel.SPRING) +public interface PartyMapper { + + @Mapping(target = "additions", source = "customFields") + PartyUpsertDto partyToPartyUpsertDto(Party party); + +} \ No newline at end of file diff --git a/stream-customer-profile/customer-profile-core/src/main/java/com/backbase/stream/service/CustomerProfileService.java b/stream-customer-profile/customer-profile-core/src/main/java/com/backbase/stream/service/CustomerProfileService.java new file mode 100644 index 000000000..dfc0ccdba --- /dev/null +++ b/stream-customer-profile/customer-profile-core/src/main/java/com/backbase/stream/service/CustomerProfileService.java @@ -0,0 +1,27 @@ +package com.backbase.stream.service; + +import com.backbase.customerprofile.api.integration.v1.PartyManagementIntegrationApi; +import com.backbase.customerprofile.api.integration.v1.model.PartyResponseUpsertDto; +import com.backbase.stream.legalentity.model.Party; +import com.backbase.stream.mapper.PartyMapper; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.util.StringUtils; +import reactor.core.publisher.Mono; + +@Slf4j +@RequiredArgsConstructor +public class CustomerProfileService { + + private final PartyManagementIntegrationApi partyManagementIntegrationApi; + + private final PartyMapper partyMapper; + + public Mono upsertParty(Party party, String legalEntityInternalId) { + var partyUpsertDto = partyMapper.partyToPartyUpsertDto(party); + if (StringUtils.hasText(legalEntityInternalId) && party.getIsCustomer()) { + partyUpsertDto.setLegalEntityId(legalEntityInternalId); + } + return partyManagementIntegrationApi.upsertParty(partyUpsertDto); + } +} diff --git a/stream-customer-profile/customer-profile-core/src/main/java/com/backbase/stream/service/package-info.java b/stream-customer-profile/customer-profile-core/src/main/java/com/backbase/stream/service/package-info.java new file mode 100644 index 000000000..29590942e --- /dev/null +++ b/stream-customer-profile/customer-profile-core/src/main/java/com/backbase/stream/service/package-info.java @@ -0,0 +1 @@ +package com.backbase.stream.service; \ No newline at end of file diff --git a/stream-customer-profile/customer-profile-core/src/test/java/com/backbase/stream/configuration/CustomerProfileConfigurationTest.java b/stream-customer-profile/customer-profile-core/src/test/java/com/backbase/stream/configuration/CustomerProfileConfigurationTest.java new file mode 100644 index 000000000..e768c9b6a --- /dev/null +++ b/stream-customer-profile/customer-profile-core/src/test/java/com/backbase/stream/configuration/CustomerProfileConfigurationTest.java @@ -0,0 +1,38 @@ +package com.backbase.stream.configuration; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; + +import com.backbase.buildingblocks.webclient.InterServiceWebClientConfiguration; +import com.backbase.customerprofile.api.integration.v1.PartyManagementIntegrationApi; +import com.backbase.stream.clients.config.CustomerProfileClientConfig; +import com.backbase.stream.mapper.PartyMapper; +import com.backbase.stream.service.CustomerProfileService; +import org.junit.jupiter.api.Test; +import org.springframework.boot.autoconfigure.web.reactive.function.client.WebClientAutoConfiguration; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; +import org.springframework.web.reactive.function.client.WebClient; + +@SpringJUnitConfig +class CustomerProfileConfigurationTest { + + ApplicationContextRunner contextRunner = new ApplicationContextRunner(); + + @Test + void configurationTest() { + contextRunner + .withBean(WebClientAutoConfiguration.class) + .withBean(InterServiceWebClientConfiguration.class) + .withBean(PartyManagementIntegrationApi.class, () -> mock(PartyManagementIntegrationApi.class)) + .withBean(PartyMapper.class, () -> mock(PartyMapper.class)) + .withUserConfiguration(CustomerProfileConfiguration.class) + .run(context -> { + assertThat(context).hasSingleBean(CustomerProfileService.class); + assertThat(context).hasSingleBean(PartyManagementIntegrationApi.class); + assertThat(context).hasSingleBean(WebClient.class); + assertThat(context).hasSingleBean(CustomerProfileClientConfig.class); + assertThat(context).hasSingleBean(PartyMapper.class); + }); + } +} \ No newline at end of file diff --git a/stream-customer-profile/customer-profile-core/src/test/java/com/backbase/stream/mapper/MapperTest.java b/stream-customer-profile/customer-profile-core/src/test/java/com/backbase/stream/mapper/MapperTest.java new file mode 100644 index 000000000..bd0845d80 --- /dev/null +++ b/stream-customer-profile/customer-profile-core/src/test/java/com/backbase/stream/mapper/MapperTest.java @@ -0,0 +1,202 @@ +package com.backbase.stream.mapper; + + +import static com.backbase.stream.FixtureUtils.reflectiveAlphaFixtureMonkey; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.backbase.stream.legalentity.model.Party; +import com.navercorp.fixturemonkey.FixtureMonkey; +import java.util.ArrayList; +import java.util.HashMap; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest(classes = PartyMapperImpl.class) +class MapperTest { + + @Autowired + private PartyMapper partyMapper; + private final FixtureMonkey fixtureMonkey = reflectiveAlphaFixtureMonkey; + + @Test + @DisplayName("Should map basic fields correctly when not null") + void shouldMapBasicFields() { + var party = fixtureMonkey.giveMeOne(Party.class); + + var resultDto = partyMapper.partyToPartyUpsertDto(party); + + assertNotNull(resultDto); + assertEquals(party.getPartyId(), resultDto.getPartyId()); + assertEquals(party.getIsCustomer(), resultDto.getIsCustomer()); + assertEquals(party.getPreferredLanguage(), resultDto.getPreferredLanguage()); + assertEquals(party.getNotes(), resultDto.getNotes()); + assertEquals(party.getClosingDateTime(), resultDto.getClosingDateTime()); + assertEquals(party.getApprovedDateTime(), resultDto.getApprovedDateTime()); + assertEquals(party.getLastUpdatedDateTime(), resultDto.getLastUpdatedDateTime()); + assertEquals(party.getOpeningDateTime(), resultDto.getOpeningDateTime()); + assertEquals(party.getLiveDateTime(), resultDto.getLiveDateTime()); + + assertNotNull(resultDto.getPartyType()); + assertEquals(party.getPartyType().getValue(), resultDto.getPartyType().getValue()); + assertNotNull(resultDto.getState()); + assertEquals(party.getState().getValue(), resultDto.getState().getValue()); + assertNotNull(resultDto.getSubState()); + assertEquals(party.getSubState().getValue(), resultDto.getSubState().getValue()); + } + + @Test + @DisplayName("Should map Person when PartyType is PERSON and Person is not null") + void shouldMapPersonWhenPartyTypeIsPerson() { + var party = fixtureMonkey.giveMeBuilder(Party.class) + .set("partyType", Party.PartyTypeEnum.PERSON) + .setNotNull("person") + .setNotNull("person.personName") + .set("person.personName.firstName", "John") + .set("person.personName.familyName", "Doe") + .setNull("organisation") + .sample(); + + var resultDto = partyMapper.partyToPartyUpsertDto(party); + + assertNotNull(resultDto.getPerson()); + assertEquals("John", resultDto.getPerson().getPersonName().getFirstName()); + assertEquals("Doe", resultDto.getPerson().getPersonName().getFamilyName()); + if (party.getPerson().getIdentifications() != null) { + assertNotNull(resultDto.getPerson().getIdentifications()); + assertEquals(party.getPerson().getIdentifications().size(), + resultDto.getPerson().getIdentifications().size()); + } + assertNull(resultDto.getOrganisation(), "Organisation should be null if PartyType is PERSON"); + } + + @Test + @DisplayName("Should handle null Person from source") + void shouldHandleNullPerson() { + var party = fixtureMonkey.giveMeBuilder(Party.class) + .setNull("person") + .sample(); + + var resultDto = partyMapper.partyToPartyUpsertDto(party); + + assertNull(resultDto.getPerson()); + } + + @Test + @DisplayName("Should map Organisation when PartyType is ORGANISATION and Organisation is not null") + void shouldMapOrganisationWhenPartyTypeIsOrganisation() { + var party = fixtureMonkey.giveMeBuilder(Party.class) + .set("partyType", Party.PartyTypeEnum.ORGANISATION) + .setNotNull("organisation") + .set("organisation.name", "My Company") + .setNull("person") + .sample(); + var resultDto = partyMapper.partyToPartyUpsertDto(party); + assertNotNull(resultDto.getOrganisation()); + assertEquals("My Company", resultDto.getOrganisation().getName()); + assertNull(resultDto.getPerson()); + } + + @Test + @DisplayName("Should handle null Organisation from source") + void shouldHandleNullOrganisation() { + Party party = fixtureMonkey.giveMeBuilder(Party.class) + .setNull("organisation") + .sample(); + var resultDto = partyMapper.partyToPartyUpsertDto(party); + assertNull(resultDto.getOrganisation()); + } + + @Test + @DisplayName("Should map populated collections correctly") + void shouldMapPopulatedCollections() { + + var party = fixtureMonkey.giveMeBuilder(Party.class) + .size("phoneNumbers", 2) + .size("postalAddresses", 1) + .size("customFields", 3) + .size("partyPartyRelationships", 1) + .sample(); + + var resultDto = partyMapper.partyToPartyUpsertDto(party); + + assertNotNull(resultDto.getPhoneNumbers()); + assertEquals(2, resultDto.getPhoneNumbers().size()); + + assertNotNull(resultDto.getPostalAddresses()); + assertEquals(1, resultDto.getPostalAddresses().size()); + + assertNotNull(resultDto.getAdditions()); + assertEquals(3, resultDto.getAdditions().size()); + + assertNotNull(resultDto.getPartyPartyRelationships()); + assertEquals(1, resultDto.getPartyPartyRelationships().size()); + } + + @Test + @DisplayName("Should map empty collections correctly") + void shouldMapEmptyCollections() { + var party = fixtureMonkey.giveMeBuilder(Party.class) + .set("phoneNumbers", new ArrayList<>()) + .set("postalAddresses", new ArrayList<>()) + .set("customFields", new HashMap<>()) + .set("partyPartyRelationships", new ArrayList<>()) + .sample(); + + var resultDto = partyMapper.partyToPartyUpsertDto(party); + + assertNotNull(resultDto.getPhoneNumbers()); + assertTrue(resultDto.getPhoneNumbers().isEmpty()); + + assertNotNull(resultDto.getPostalAddresses()); + assertTrue(resultDto.getPostalAddresses().isEmpty()); + + assertNotNull(resultDto.getAdditions()); + assertTrue(resultDto.getAdditions().isEmpty()); + + assertNotNull(resultDto.getPartyPartyRelationships()); + assertTrue(resultDto.getPartyPartyRelationships().isEmpty()); + } + + @Test + @DisplayName("Should map empty collections correctly") + void shouldMapNullCollections() { + + var party = fixtureMonkey.giveMeBuilder(Party.class) + .setNull("phoneNumbers") + .setNull("postalAddresses") + .setNull("customFields") + .setNull("partyPartyRelationships") + .setNull("electronicAddresses.emails") + .setNull("electronicAddresses.urls") + .setNull("person.identifications") + .setNull("organisation.identifications") + .sample(); + + var resultDto = partyMapper.partyToPartyUpsertDto(party); + + assertNotNull(resultDto.getPhoneNumbers()); + assertTrue(resultDto.getPhoneNumbers().isEmpty()); + + assertNotNull(resultDto.getPostalAddresses()); + assertTrue(resultDto.getPostalAddresses().isEmpty()); + + assertNotNull(resultDto.getAdditions()); + assertTrue(resultDto.getAdditions().isEmpty()); + + assertNotNull(resultDto.getPartyPartyRelationships()); + assertTrue(resultDto.getPartyPartyRelationships().isEmpty()); + + assertNotNull(resultDto.getElectronicAddresses()); + assertNotNull(resultDto.getElectronicAddresses().getEmails()); + assertTrue(resultDto.getElectronicAddresses().getEmails().isEmpty()); + + assertNotNull(resultDto.getElectronicAddresses().getUrls()); + assertTrue(resultDto.getElectronicAddresses().getUrls().isEmpty()); + } + +} diff --git a/stream-customer-profile/customer-profile-core/src/test/java/com/backbase/stream/service/CustomerProfileServiceTest.java b/stream-customer-profile/customer-profile-core/src/test/java/com/backbase/stream/service/CustomerProfileServiceTest.java new file mode 100644 index 000000000..ea657d0f0 --- /dev/null +++ b/stream-customer-profile/customer-profile-core/src/test/java/com/backbase/stream/service/CustomerProfileServiceTest.java @@ -0,0 +1,101 @@ +package com.backbase.stream.service; + +import static com.backbase.stream.FixtureUtils.reflectiveAlphaFixtureMonkey; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; + +import com.backbase.customerprofile.api.integration.v1.PartyManagementIntegrationApi; +import com.backbase.customerprofile.api.integration.v1.model.PartyResponseUpsertDto; +import com.backbase.customerprofile.api.integration.v1.model.PartyUpsertDto; +import com.backbase.stream.legalentity.model.Party; +import com.backbase.stream.mapper.PartyMapper; +import com.navercorp.fixturemonkey.FixtureMonkey; +import java.nio.charset.StandardCharsets; +import java.util.UUID; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mapstruct.factory.Mappers; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.http.HttpStatus; +import org.springframework.web.reactive.function.client.WebClientResponseException; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; + +@ExtendWith(MockitoExtension.class) +class CustomerProfileServiceTest { + + private CustomerProfileService customerProfileService; + + private final FixtureMonkey fixtureMonkey = reflectiveAlphaFixtureMonkey; + @Mock + private PartyManagementIntegrationApi partyManagementIntegrationApiMock; + + + @BeforeEach + void setup() { + customerProfileService = new CustomerProfileService(partyManagementIntegrationApiMock, Mappers.getMapper(PartyMapper.class)); + } + + @Test + @DisplayName("Upsert party should return PartyResponseUpsertDto when API call is successful") + void createCustomer_success() { + var legalEntityId = UUID.randomUUID().toString(); + var requestDto = fixtureMonkey.giveMeBuilder(Party.class) + .set("legalEntityId", legalEntityId) + .sample(); + var expectedResponseDto = fixtureMonkey.giveMeBuilder(PartyResponseUpsertDto.class) + .set("partyReferenceId", legalEntityId).sample(); + + when(partyManagementIntegrationApiMock.upsertParty(any(PartyUpsertDto.class))) + .thenReturn(Mono.just(expectedResponseDto)); + + var result = customerProfileService.upsertParty(requestDto, legalEntityId); + + StepVerifier.create(result) + .assertNext(response -> { + assertNotNull(response); + assertNotNull(response.getCustomerReferenceId()); + assertNotNull(response.getPartyReferenceId()); + }) + .verifyComplete(); + } + + @Test + @DisplayName("Upsert party should propagate WebClientResponseException when API call fails") + void upsertParty_apiError() { + var requestDto = fixtureMonkey.giveMeOne(Party.class); + var expectedException = new WebClientResponseException( + HttpStatus.BAD_REQUEST.value(), + "Bad Request from API", + null, + null, + StandardCharsets.UTF_8); + when(partyManagementIntegrationApiMock.upsertParty(any(PartyUpsertDto.class))) + .thenReturn(Mono.error(expectedException)); + var result = customerProfileService.upsertParty(requestDto, null); + StepVerifier.create(result) + .expectErrorMatches(throwable -> + throwable instanceof WebClientResponseException && + ((WebClientResponseException) throwable).getStatusCode() == HttpStatus.BAD_REQUEST && + throwable.getMessage().contains("Bad Request from API") + ) + .verify(); + } + + @Test + @DisplayName("Upsert party should propagate other RuntimeExceptions when API call fails unexpectedly") + void upsertParty_otherError() { + var requestDto = fixtureMonkey.giveMeOne(Party.class); + var expectedException = new RuntimeException("Unexpected error"); + when(partyManagementIntegrationApiMock.upsertParty(any(PartyUpsertDto.class))) + .thenReturn(Mono.error(expectedException)); + var result = customerProfileService.upsertParty(requestDto, null); + StepVerifier.create(result) + .expectErrorMatches(throwable -> throwable == expectedException) + .verify(); + } +} diff --git a/stream-customer-profile/pom.xml b/stream-customer-profile/pom.xml new file mode 100644 index 000000000..a83364cce --- /dev/null +++ b/stream-customer-profile/pom.xml @@ -0,0 +1,19 @@ + + + 4.0.0 + + + com.backbase.stream + stream-services + 6.18.0 + + + stream-customer-profile + + pom + Stream :: Customer Profile + + + customer-profile-core + + diff --git a/stream-dbs-clients/pom.xml b/stream-dbs-clients/pom.xml index b588976c1..0c7a9eee8 100644 --- a/stream-dbs-clients/pom.xml +++ b/stream-dbs-clients/pom.xml @@ -218,6 +218,15 @@ ${project.build.directory}/yaml true + + com.backbase.flow.customer-profile.api + customer-profile + 1.17.1 + api + zip + ${project.build.directory}/yaml + true + **/*.yaml, **/*.json @@ -482,6 +491,19 @@ com.backbase.tailoredvalue.planmanager.service.api.v1.model + + generate-customer-profile-integration-api-code + + generate-webclient-embedded + + generate-sources + + REFACTOR_ALLOF_WITH_PROPERTIES_ONLY=true + ${project.build.directory}/yaml/customer-profile/customer-profile-integration-api-v*.yaml + com.backbase.customerprofile.api.integration.v1 + com.backbase.customerprofile.api.integration.v1.model + + diff --git a/stream-dbs-clients/src/main/java/com/backbase/stream/clients/autoconfigure/DbsApiClientsAutoConfiguration.java b/stream-dbs-clients/src/main/java/com/backbase/stream/clients/autoconfigure/DbsApiClientsAutoConfiguration.java index 2547408b9..e2e23b076 100644 --- a/stream-dbs-clients/src/main/java/com/backbase/stream/clients/autoconfigure/DbsApiClientsAutoConfiguration.java +++ b/stream-dbs-clients/src/main/java/com/backbase/stream/clients/autoconfigure/DbsApiClientsAutoConfiguration.java @@ -4,6 +4,7 @@ import com.backbase.stream.clients.config.ApprovalClientConfig; import com.backbase.stream.clients.config.ArrangementManagerClientConfig; import com.backbase.stream.clients.config.ContactManagerClientConfig; +import com.backbase.stream.clients.config.CustomerProfileClientConfig; import com.backbase.stream.clients.config.IdentityIntegrationClientConfig; import com.backbase.stream.clients.config.InstrumentApiConfiguration; import com.backbase.stream.clients.config.LimitsClientConfig; @@ -42,7 +43,8 @@ UserProfileManagerClientConfig.class, InstrumentApiConfiguration.class, PortfolioApiConfiguration.class, - PlanManagerClientConfig.class + PlanManagerClientConfig.class, + CustomerProfileClientConfig.class }) @EnableConfigurationProperties public class DbsApiClientsAutoConfiguration { diff --git a/stream-dbs-clients/src/main/java/com/backbase/stream/clients/config/CustomerProfileClientConfig.java b/stream-dbs-clients/src/main/java/com/backbase/stream/clients/config/CustomerProfileClientConfig.java new file mode 100644 index 000000000..d4057680a --- /dev/null +++ b/stream-dbs-clients/src/main/java/com/backbase/stream/clients/config/CustomerProfileClientConfig.java @@ -0,0 +1,41 @@ +package com.backbase.stream.clients.config; + + +import com.backbase.customerprofile.api.integration.ApiClient; +import com.backbase.customerprofile.api.integration.v1.CustomerLifeCycleManagementIntegrationApi; +import com.backbase.customerprofile.api.integration.v1.CustomerManagementIntegrationApi; +import com.backbase.customerprofile.api.integration.v1.CustomerProfileManagementIntegrationApi; +import com.backbase.customerprofile.api.integration.v1.PartyManagementIntegrationApi; +import com.fasterxml.jackson.databind.ObjectMapper; +import java.text.DateFormat; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +@ConfigurationProperties("backbase.communication.services.customer-profile") +public class CustomerProfileClientConfig extends CompositeApiClientConfig { + + public static final String CUSTOMER_PROFILE_SERVICE_ID = "customer-profile"; + + public CustomerProfileClientConfig() { + super(CUSTOMER_PROFILE_SERVICE_ID); + } + + + @Bean + @ConditionalOnMissingBean + public ApiClient customerProfileApiIntegrationClient( + ObjectMapper objectMapper, DateFormat dateFormat) { + return new ApiClient(getWebClient(), objectMapper, dateFormat) + .setBasePath(createBasePath()); + } + + @Bean + @ConditionalOnMissingBean + public PartyManagementIntegrationApi partyManagementIntegrationApi( + ApiClient customerProfileApiIntegrationClient) { + return new PartyManagementIntegrationApi(customerProfileApiIntegrationClient); + } +} diff --git a/stream-dbs-clients/src/test/java/com/backbase/stream/clients/config/CompositeApiClientConfigTest.java b/stream-dbs-clients/src/test/java/com/backbase/stream/clients/config/CompositeApiClientConfigTest.java index 2cbb80637..213c5a923 100644 --- a/stream-dbs-clients/src/test/java/com/backbase/stream/clients/config/CompositeApiClientConfigTest.java +++ b/stream-dbs-clients/src/test/java/com/backbase/stream/clients/config/CompositeApiClientConfigTest.java @@ -98,5 +98,72 @@ void shouldReturnDefaultServicePortWhenServicePortIsEmptyTest() { assertEquals("http://user-profile-manager:8181", config.createBasePath()); }); } + @Test + void shouldReturnServiceIdWhenCustomerProfileWithLoadBalancerTest() { + contextRunner + .withBean(Factory.class, () -> loadBalancerFactory) + .withBean(WebClientAutoConfiguration.class) + .withBean(InterServiceWebClientConfiguration.class) + .withUserConfiguration(CustomerProfileClientConfig.class) + .run(context -> { + var config = context.getBean(CustomerProfileClientConfig.class); + assertEquals("http://customer-profile", config.createBasePath()); + }); + } + + @Test + void shouldReturnDirectUriWhenCustomerProfileWithoutLoadBalancerAndWithDirectUriTest() { + contextRunner + .withPropertyValues("backbase.communication.services.customer-profile.direct-uri=http://custom-profile-uri/api") + .withBean(WebClientAutoConfiguration.class) + .withBean(InterServiceWebClientConfiguration.class) + .withBean(CustomerProfileClientConfig.class) + .run(context -> { + var config = context.getBean(CustomerProfileClientConfig.class); + assertEquals("http://custom-profile-uri/api", config.createBasePath()); + }); + } + + @Test + void shouldReturnServiceIdWhenCustomerProfileWithLoadBalancerAndWithDirectUriTest() { + contextRunner + .withPropertyValues("backbase.communication.services.customer-profile.direct-uri=http://custom-profile-uri/api") + .withBean(Factory.class, () -> loadBalancerFactory) + .withBean(WebClientAutoConfiguration.class) + .withBean(InterServiceWebClientConfiguration.class) + .withUserConfiguration(CustomerProfileClientConfig.class) + .run(context -> { + var config = context.getBean(CustomerProfileClientConfig.class); + assertEquals("http://customer-profile", config.createBasePath()); + }); + } + + @Test + void shouldNotReturnDefaultServicePortWhenCustomerProfileServicePortIsSetTest() { + contextRunner + .withPropertyValues("backbase.communication.http.default-service-port=8181", + "backbase.communication.services.customer-profile.service-port=8080") + .withBean(Factory.class, () -> loadBalancerFactory) + .withBean(WebClientAutoConfiguration.class) + .withBean(InterServiceWebClientConfiguration.class) + .withUserConfiguration(CustomerProfileClientConfig.class) + .run(context -> { + var config = context.getBean(CustomerProfileClientConfig.class); + assertEquals("http://customer-profile:8080", config.createBasePath()); + }); + } + + @Test + void shouldReturnDefaultServicePortWhenCustomerProfileServicePortIsEmptyTest() { + contextRunner + .withPropertyValues("backbase.communication.http.default-service-port=8080") + .withBean(WebClientAutoConfiguration.class) + .withBean(InterServiceWebClientConfiguration.class) + .withUserConfiguration(CustomerProfileClientConfig.class) + .run(context -> { + var config = context.getBean(CustomerProfileClientConfig.class); + assertEquals("http://customer-profile:8080", config.createBasePath()); + }); + } } diff --git a/stream-legal-entity/legal-entity-core/pom.xml b/stream-legal-entity/legal-entity-core/pom.xml index 8842c0e08..a43006e67 100644 --- a/stream-legal-entity/legal-entity-core/pom.xml +++ b/stream-legal-entity/legal-entity-core/pom.xml @@ -40,6 +40,13 @@ ${project.version} + + com.backbase.stream + customer-profile-core + ${project.version} + compile + + com.backbase.stream legal-entity-model @@ -105,14 +112,9 @@ - io.projectreactor - reactor-test - test - - - - com.backbase.buildingblocks - service-sdk-starter-test + com.backbase.stream + stream-test-support + ${project.version} test diff --git a/stream-legal-entity/legal-entity-core/src/main/java/com/backbase/stream/HelperProcessor.java b/stream-legal-entity/legal-entity-core/src/main/java/com/backbase/stream/HelperProcessor.java new file mode 100644 index 000000000..539598f34 --- /dev/null +++ b/stream-legal-entity/legal-entity-core/src/main/java/com/backbase/stream/HelperProcessor.java @@ -0,0 +1,146 @@ +package com.backbase.stream; + +import static com.backbase.stream.product.utils.StreamUtils.nullableCollectionToStream; +import static org.springframework.util.CollectionUtils.isEmpty; + +import com.backbase.stream.legalentity.model.Party; +import com.backbase.stream.service.CustomerProfileService; +import com.backbase.stream.worker.model.StreamTask; +import java.util.List; +import java.util.Objects; +import java.util.concurrent.CopyOnWriteArrayList; +import lombok.extern.slf4j.Slf4j; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +@Slf4j +class HelperProcessor { + + protected static final String LEGAL_ENTITY = "LEGAL_ENTITY"; + protected static final String PROCESS_CUSTOMER_PROFILE = "process-customer-profile"; + protected static final String PARTY = "party"; + protected static final String FAILED = "failed"; + + protected static final String SERVICE_AGREEMENT = "SERVICE_AGREEMENT"; + protected static final String BUSINESS_FUNCTION_GROUP = "BUSINESS_FUNCTION_GROUP"; + protected static final String USER = "USER"; + protected static final String DEFAULT_DATA_GROUP = "Default data group"; + protected static final String DEFAULT_DATA_DESCRIPTION = "Default data group description"; + protected static final String UPSERT_LEGAL_ENTITY = "upsert-legal-entity"; + protected static final String EXISTS = "exists"; + protected static final String CREATED = "created"; + + protected static final String UPDATED = "updated"; + protected static final String PROCESS_PRODUCTS = "process-products"; + protected static final String PROCESS_JOB_PROFILES = "process-job-profiles"; + protected static final String PROCESS_LIMITS = "process-limits"; + protected static final String PROCESS_CONTACTS = "process-contacts"; + protected static final String REJECTED = "rejected"; + protected static final String UPSERT = "upsert"; + protected static final String SETUP_SERVICE_AGREEMENT = "setup-service-agreement"; + protected static final String BATCH_PRODUCT_GROUP_ID = "batch_product_group_task-"; + + protected static final String LEGAL_ENTITY_E_TYPE = "LE"; + protected static final String SERVICE_AGREEMENT_E_TYPE = "SA"; + protected static final String FUNCTION_GROUP_E_TYPE = "FAG"; + protected static final String FUNCTION_E_TYPE = "FUN"; + protected static final String PRIVILEGE_E_TYPE = "PRV"; + protected static final String JOB_ROLE_LIMITS = "job-role-limits"; + protected static final String USER_JOB_ROLE_LIMITS = "user-job-role-limits"; + protected static final String LEGAL_ENTITY_LIMITS = "legal-entity-limits"; + protected static final String IDENTITY_USER = "IDENTITY_USER"; + + private boolean isValidParty( + T legalEntityTask, + List parties, + String legalEntityInternalId, + String legalEntityExternalId) { + if (isEmpty(parties)) { + legalEntityTask.info(LEGAL_ENTITY, PROCESS_CUSTOMER_PROFILE, "skipped", legalEntityExternalId, + legalEntityInternalId, "No parties found in Legal Entity to process."); + return false; + } + return true; + } + + + public Mono processParties( + T legalEntityTask, + List parties, + String legalEntityInternalId, + String legalEntityExternalId, + CustomerProfileService customerProfileService + ) { + log.info("Processing Customer Profile Parties for LE: {}", legalEntityExternalId); + + if (!isValidParty(legalEntityTask, parties, legalEntityInternalId, legalEntityExternalId)) { + return Mono.just(legalEntityTask); + } + + var processingErrors = new CopyOnWriteArrayList(); + + return Flux.fromStream(nullableCollectionToStream(parties)) + .filter(Objects::nonNull) + .concatMap(party -> handlePartyUpsert( + party, + legalEntityInternalId, + legalEntityExternalId, + customerProfileService, + processingErrors, + legalEntityTask + )) + .then(Mono.fromRunnable(() -> logFinalProcessingStatus( + processingErrors, + legalEntityInternalId, + legalEntityExternalId, + legalEntityTask + ))) + .thenReturn(legalEntityTask); + } + + private void logFinalProcessingStatus( + List processingErrors, + String legalEntityInternalId, + String legalEntityExternalId, + T legalEntityTask + ) { + if (!processingErrors.isEmpty()) { + int errorCount = processingErrors.size(); + log.warn("Completed processing parties for LE {} with {} error(s).", legalEntityExternalId, errorCount); + legalEntityTask.warn(LEGAL_ENTITY, PROCESS_CUSTOMER_PROFILE, "completed_with_errors", + legalEntityExternalId, legalEntityInternalId, + "Party processing completed with %d error(s).", errorCount); + } else { + log.info("Successfully processed all parties for LE {}.", legalEntityExternalId); + legalEntityTask.info(LEGAL_ENTITY, PROCESS_CUSTOMER_PROFILE, "completed_successfully", + legalEntityExternalId, legalEntityInternalId, + "Party processing completed successfully."); + } + } + + private Mono handlePartyUpsert( + Party party, + String legalEntityInternalId, + String legalEntityExternalId, + CustomerProfileService customerProfileService, + List processingErrors, + T legalEntityTask + ) { + String partyId = party.getPartyId(); + log.debug("Attempting to upsert party with partyId: {}", partyId); + + return customerProfileService.upsertParty(party, legalEntityInternalId) + .doOnSuccess(result -> + legalEntityTask.info(PARTY, PROCESS_CUSTOMER_PROFILE, "upserted", partyId, null, + "Successfully upserted party: %s for LE: %s", partyId, legalEntityExternalId) + ) + .doOnError(throwable -> { + log.error("Failed to upsert party {}: {}", partyId, throwable.getMessage(), throwable); + processingErrors.add(throwable); + legalEntityTask.error(PARTY, PROCESS_CUSTOMER_PROFILE, FAILED, partyId, null, throwable, + throwable.getMessage(), "Error upserting party: %s for LE: %s", partyId, legalEntityExternalId); + }) + .onErrorResume(throwable -> Mono.empty()) + .then(); + } +} diff --git a/stream-legal-entity/legal-entity-core/src/main/java/com/backbase/stream/LegalEntitySaga.java b/stream-legal-entity/legal-entity-core/src/main/java/com/backbase/stream/LegalEntitySaga.java index b19be0b00..4e9e4bd10 100644 --- a/stream-legal-entity/legal-entity-core/src/main/java/com/backbase/stream/LegalEntitySaga.java +++ b/stream-legal-entity/legal-entity-core/src/main/java/com/backbase/stream/LegalEntitySaga.java @@ -64,6 +64,7 @@ import com.backbase.stream.product.task.ProductGroupTask; import com.backbase.stream.product.utils.StreamUtils; import com.backbase.stream.service.AccessGroupService; +import com.backbase.stream.service.CustomerProfileService; import com.backbase.stream.service.LegalEntityService; import com.backbase.stream.service.UserProfileService; import com.backbase.stream.service.UserService; @@ -103,38 +104,8 @@ * After the users are created / retrieved and enriched with their internal Ids we can setup the Master Service */ @Slf4j -public class LegalEntitySaga implements StreamTaskExecutor { - - public static final String LEGAL_ENTITY = "LEGAL_ENTITY"; - public static final String IDENTITY_USER = "IDENTITY_USER"; - public static final String SERVICE_AGREEMENT = "SERVICE_AGREEMENT"; - public static final String BUSINESS_FUNCTION_GROUP = "BUSINESS_FUNCTION_GROUP"; - public static final String USER = "USER"; - private static final String DEFAULT_DATA_GROUP = "Default data group"; - private static final String DEFAULT_DATA_DESCRIPTION = "Default data group description"; - public static final String UPSERT_LEGAL_ENTITY = "upsert-legal-entity"; - public static final String FAILED = "failed"; - public static final String EXISTS = "exists"; - public static final String CREATED = "created"; - - public static final String UPDATED = "updated"; - public static final String PROCESS_PRODUCTS = "process-products"; - public static final String PROCESS_JOB_PROFILES = "process-job-profiles"; - public static final String PROCESS_LIMITS = "process-limits"; - public static final String PROCESS_CONTACTS = "process-contacts"; - public static final String REJECTED = "rejected"; - public static final String UPSERT = "upsert"; - public static final String SETUP_SERVICE_AGREEMENT = "setup-service-agreement"; - private static final String BATCH_PRODUCT_GROUP_ID = "batch_product_group_task-"; - - private static final String LEGAL_ENTITY_E_TYPE = "LE"; - private static final String SERVICE_AGREEMENT_E_TYPE = "SA"; - private static final String FUNCTION_GROUP_E_TYPE = "FAG"; - private static final String FUNCTION_E_TYPE = "FUN"; - private static final String PRIVILEGE_E_TYPE = "PRV"; - private static final String JOB_ROLE_LIMITS = "job-role-limits"; - private static final String USER_JOB_ROLE_LIMITS = "user-job-role-limits"; - private static final String LEGAL_ENTITY_LIMITS = "legal-entity-limits"; +public class LegalEntitySaga extends HelperProcessor implements StreamTaskExecutor { + private final BusinessFunctionGroupMapper businessFunctionGroupMapper = Mappers.getMapper(BusinessFunctionGroupMapper.class); private final UserProfileMapper userProfileMapper = Mappers.getMapper(UserProfileMapper.class); @@ -148,10 +119,11 @@ public class LegalEntitySaga implements StreamTaskExecutor { private final ContactsSaga contactsSaga; private final LegalEntitySagaConfigurationProperties legalEntitySagaConfigurationProperties; private final UserKindSegmentationSaga userKindSegmentationSaga; - + private final CustomerProfileService customerProfileService; private static final ExternalContactMapper externalContactMapper = ExternalContactMapper.INSTANCE; - public LegalEntitySaga(LegalEntityService legalEntityService, + public LegalEntitySaga( + LegalEntityService legalEntityService, UserService userService, UserProfileService userProfileService, AccessGroupService accessGroupService, @@ -159,7 +131,8 @@ public LegalEntitySaga(LegalEntityService legalEntityService, LimitsSaga limitsSaga, ContactsSaga contactsSaga, LegalEntitySagaConfigurationProperties legalEntitySagaConfigurationProperties, - UserKindSegmentationSaga userKindSegmentationSaga) { + UserKindSegmentationSaga userKindSegmentationSaga, + CustomerProfileService customerProfileService) { this.legalEntityService = legalEntityService; this.userService = userService; this.userProfileService = userProfileService; @@ -169,12 +142,14 @@ public LegalEntitySaga(LegalEntityService legalEntityService, this.contactsSaga = contactsSaga; this.legalEntitySagaConfigurationProperties = legalEntitySagaConfigurationProperties; this.userKindSegmentationSaga = userKindSegmentationSaga; + this.customerProfileService = customerProfileService; } @Override public Mono executeTask(@SpanTag(value = "streamTask") LegalEntityTask streamTask) { return upsertLegalEntity(streamTask) .flatMap(this::linkLegalEntityToRealm) + .flatMap(this::setupParties) .flatMap(this::setupAdministrators) .flatMap(this::setupUsers) .flatMap(this::processAudiencesSegmentation) @@ -1091,6 +1066,16 @@ private Mono processSubsidiaries(LegalEntityTask streamTask) { }); } + private Mono setupParties(LegalEntityTask legalEntityTask) { + + var legalEntity = legalEntityTask.getData(); + + return processParties(legalEntityTask, legalEntity.getParties(), + legalEntity.getInternalId(), + legalEntity.getExternalId(), customerProfileService); + + } + private Mono linkLegalEntityToRealm(LegalEntityTask streamTask) { return Mono.just(streamTask) .filter(task -> legalEntitySagaConfigurationProperties.isUseIdentityIntegration()) diff --git a/stream-legal-entity/legal-entity-core/src/main/java/com/backbase/stream/LegalEntitySagaV2.java b/stream-legal-entity/legal-entity-core/src/main/java/com/backbase/stream/LegalEntitySagaV2.java index 3e77dd1fc..eed85a83e 100644 --- a/stream-legal-entity/legal-entity-core/src/main/java/com/backbase/stream/LegalEntitySagaV2.java +++ b/stream-legal-entity/legal-entity-core/src/main/java/com/backbase/stream/LegalEntitySagaV2.java @@ -47,6 +47,7 @@ import com.backbase.stream.mapper.UserProfileMapper; import com.backbase.stream.product.utils.StreamUtils; import com.backbase.stream.service.AccessGroupService; +import com.backbase.stream.service.CustomerProfileService; import com.backbase.stream.service.LegalEntityService; import com.backbase.stream.service.UserProfileService; import com.backbase.stream.service.UserService; @@ -56,7 +57,9 @@ import io.micrometer.tracing.annotation.SpanTag; import java.util.ArrayList; import java.util.List; +import java.util.Objects; import java.util.Optional; +import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.atomic.AtomicInteger; import java.util.stream.Stream; import lombok.extern.slf4j.Slf4j; @@ -74,26 +77,7 @@ * After the users are created / retrieved and enriched with their internal Ids we can setup the Master Service */ @Slf4j -public class LegalEntitySagaV2 implements StreamTaskExecutor { - - public static final String LEGAL_ENTITY = "LEGAL_ENTITY"; - public static final String IDENTITY_USER = "IDENTITY_USER"; - public static final String USER = "USER"; - public static final String UPSERT_LEGAL_ENTITY = "upsert-legal-entity"; - public static final String FAILED = "failed"; - public static final String EXISTS = "exists"; - public static final String CREATED = "created"; - - public static final String UPDATED = "updated"; - public static final String PROCESS_LIMITS = "process-limits"; - public static final String PROCESS_CONTACTS = "process-contacts"; - public static final String UPSERT = "upsert"; - - private static final String LEGAL_ENTITY_E_TYPE = "LE"; - private static final String SERVICE_AGREEMENT_E_TYPE = "SA"; - private static final String FUNCTION_E_TYPE = "FUN"; - private static final String PRIVILEGE_E_TYPE = "PRV"; - private static final String LEGAL_ENTITY_LIMITS = "legal-entity-limits"; +public class LegalEntitySagaV2 extends HelperProcessor implements StreamTaskExecutor { private final UserProfileMapper userProfileMapper = Mappers.getMapper(UserProfileMapper.class); private final LegalEntityV2toV1Mapper leV2Mapper = Mappers.getMapper(LegalEntityV2toV1Mapper.class); @@ -107,17 +91,20 @@ public class LegalEntitySagaV2 implements StreamTaskExecutor private final ContactsSaga contactsSaga; private final LegalEntitySagaConfigurationProperties legalEntitySagaConfigurationProperties; private final UserKindSegmentationSaga userKindSegmentationSaga; + private final CustomerProfileService customerProfileService; private static final ExternalContactMapper externalContactMapper = ExternalContactMapper.INSTANCE; - public LegalEntitySagaV2(LegalEntityService legalEntityService, + public LegalEntitySagaV2( + LegalEntityService legalEntityService, UserService userService, UserProfileService userProfileService, AccessGroupService accessGroupService, LimitsSaga limitsSaga, ContactsSaga contactsSaga, LegalEntitySagaConfigurationProperties legalEntitySagaConfigurationProperties, - UserKindSegmentationSaga userKindSegmentationSaga) { + UserKindSegmentationSaga userKindSegmentationSaga, + CustomerProfileService customerProfileService) { this.legalEntityService = legalEntityService; this.userService = userService; this.userProfileService = userProfileService; @@ -126,12 +113,14 @@ public LegalEntitySagaV2(LegalEntityService legalEntityService, this.contactsSaga = contactsSaga; this.legalEntitySagaConfigurationProperties = legalEntitySagaConfigurationProperties; this.userKindSegmentationSaga = userKindSegmentationSaga; + this.customerProfileService = customerProfileService; } @Override public Mono executeTask(@SpanTag(value = "streamTask") LegalEntityTaskV2 streamTask) { return upsertLegalEntity(streamTask) .flatMap(this::linkLegalEntityToRealm) + .flatMap(this::setupParties) .flatMap(this::setupAdministrators) .flatMap(this::setupUsers) .flatMap(this::processAudiencesSegmentation) @@ -580,6 +569,15 @@ private Mono setupLegalEntityLevelBusinessFunctionLimits(Lega }); } + private Mono setupParties(LegalEntityTaskV2 legalEntityTaskV2) { + + var legalEntity = legalEntityTaskV2.getData(); + + return processParties(legalEntityTaskV2, legalEntity.getParties(), + legalEntity.getInternalId(), + legalEntity.getExternalId(), customerProfileService); + } + private Stream createLimitsTask(LegalEntityTaskV2 streamTask, String legalEntityId, BusinessFunctionLimit businessFunctionLimit) { diff --git a/stream-legal-entity/legal-entity-core/src/main/java/com/backbase/stream/configuration/LegalEntitySagaConfiguration.java b/stream-legal-entity/legal-entity-core/src/main/java/com/backbase/stream/configuration/LegalEntitySagaConfiguration.java index d39239923..97129abc4 100644 --- a/stream-legal-entity/legal-entity-core/src/main/java/com/backbase/stream/configuration/LegalEntitySagaConfiguration.java +++ b/stream-legal-entity/legal-entity-core/src/main/java/com/backbase/stream/configuration/LegalEntitySagaConfiguration.java @@ -13,12 +13,13 @@ import com.backbase.stream.product.ProductIngestionSagaConfiguration; import com.backbase.stream.product.configuration.ProductConfiguration; import com.backbase.stream.service.AccessGroupService; +import com.backbase.stream.service.CustomerProfileService; import com.backbase.stream.service.LegalEntityService; import com.backbase.stream.service.UserProfileService; import com.backbase.stream.service.UserService; import com.backbase.stream.worker.repository.impl.InMemoryReactiveUnitOfWorkRepository; -import com.backbase.streams.tailoredvalue.configuration.PlanServiceConfiguration; import com.backbase.streams.tailoredvalue.PlansService; +import com.backbase.streams.tailoredvalue.configuration.PlanServiceConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; @@ -34,8 +35,8 @@ ContactsServiceConfiguration.class, LoansServiceConfiguration.class, AudiencesSegmentationConfiguration.class, - PlanServiceConfiguration.class - + PlanServiceConfiguration.class, + CustomerProfileConfiguration.class }) @EnableConfigurationProperties( {LegalEntitySagaConfigurationProperties.class} @@ -51,7 +52,8 @@ public LegalEntitySaga reactiveLegalEntitySaga(LegalEntityService legalEntitySer LimitsSaga limitsSaga, ContactsSaga contactsSaga, LegalEntitySagaConfigurationProperties sinkConfigurationProperties, - UserKindSegmentationSaga userKindSegmentationSaga + UserKindSegmentationSaga userKindSegmentationSaga, + CustomerProfileService customerProfileService ) { return new LegalEntitySaga( legalEntityService, @@ -62,7 +64,8 @@ public LegalEntitySaga reactiveLegalEntitySaga(LegalEntityService legalEntitySer limitsSaga, contactsSaga, sinkConfigurationProperties, - userKindSegmentationSaga + userKindSegmentationSaga, + customerProfileService ); } @@ -74,7 +77,8 @@ public LegalEntitySagaV2 reactiveLegalEntitySagaV2(LegalEntityService legalEntit LimitsSaga limitsSaga, ContactsSaga contactsSaga, LegalEntitySagaConfigurationProperties sinkConfigurationProperties, - UserKindSegmentationSaga userKindSegmentationSaga + UserKindSegmentationSaga userKindSegmentationSaga, + CustomerProfileService customerProfileService ) { return new LegalEntitySagaV2( legalEntityService, @@ -84,7 +88,8 @@ public LegalEntitySagaV2 reactiveLegalEntitySagaV2(LegalEntityService legalEntit limitsSaga, contactsSaga, sinkConfigurationProperties, - userKindSegmentationSaga + userKindSegmentationSaga, + customerProfileService ); } @@ -103,7 +108,7 @@ public ServiceAgreementSagaV2 reactiveServiceAgreementV2Saga(LegalEntityService batchProductIngestionSaga, limitsSaga, contactsSaga, - plansService, + plansService, sinkConfigurationProperties ); } diff --git a/stream-legal-entity/legal-entity-core/src/test/java/com/backbase/stream/HelperProcessorTest.java b/stream-legal-entity/legal-entity-core/src/test/java/com/backbase/stream/HelperProcessorTest.java new file mode 100644 index 000000000..990458429 --- /dev/null +++ b/stream-legal-entity/legal-entity-core/src/test/java/com/backbase/stream/HelperProcessorTest.java @@ -0,0 +1,188 @@ +package com.backbase.stream; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.backbase.customerprofile.api.integration.v1.model.PartyResponseUpsertDto; +import com.backbase.stream.legalentity.model.Party; +import com.backbase.stream.legalentity.model.Party.PartyTypeEnum; +import com.backbase.stream.service.CustomerProfileService; +import com.backbase.stream.worker.model.StreamTask; +import java.util.Collections; +import java.util.List; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; + +@ExtendWith(MockitoExtension.class) +class HelperProcessorTest { + + @Mock + private CustomerProfileService customerProfileService; + + @Mock + private StreamTask streamTask; + + @InjectMocks + private HelperProcessor helperProcessor; + + private Party party1; + private Party party2; + private final String internalId = "int-123"; + private final String externalId = "ext-abc"; + + @BeforeEach + void setUp() { + party1 = new Party("party-001", true, PartyTypeEnum.PERSON); + party2 = new Party("party-002", true, PartyTypeEnum.PERSON); + + } + + @Nested + @DisplayName("When parties list is empty or null") + class EmptyOrNullParties { + + @Test + @DisplayName("should log skipped and return immediately for empty list") + void processParties_emptyList_shouldSkip() { + List emptyList = Collections.emptyList(); + + Mono result = helperProcessor.processParties( + streamTask, emptyList, internalId, externalId, customerProfileService + ); + + StepVerifier.create(result) + .expectNext(streamTask) + .verifyComplete(); + + verify(customerProfileService, never()).upsertParty(any(), anyString()); + } + + @Test + @DisplayName("should log skipped and return immediately for null list") + void processParties_nullList_shouldSkip() { + Mono result = helperProcessor.processParties( + streamTask, null, internalId, externalId, customerProfileService + ); + + StepVerifier.create(result) + .expectNext(streamTask) + .verifyComplete(); + + verify(customerProfileService, never()).upsertParty(any(), anyString()); + } + } + + @Nested + @DisplayName("When processing valid parties") + class ValidParties { + + @Test + @DisplayName("should process all parties successfully") + void processParties_allSuccess() { + List parties = List.of(party1, party2); + when(customerProfileService.upsertParty(any(Party.class), eq(internalId))) + .thenReturn(Mono.just(new PartyResponseUpsertDto())); + + Mono result = helperProcessor.processParties( + streamTask, parties, internalId, externalId, customerProfileService + ); + + StepVerifier.create(result) + .expectNext(streamTask) + .verifyComplete(); + + verify(customerProfileService, times(2)).upsertParty(any(Party.class), eq(internalId)); + + verify(streamTask, never()).warn(anyString(), anyString(), anyString(), anyString(), anyString(), + anyString(), any()); + verify(streamTask, never()).error(anyString(), anyString(), anyString(), anyString(), any(), + any(Throwable.class), anyString(), any()); + } + + @Test + @DisplayName("should handle one error and complete with warning") + void processParties_oneError() { + List parties = List.of(party1, party2); + RuntimeException dbError = new RuntimeException("DB connection failed"); + + when(customerProfileService.upsertParty(eq(party1), eq(internalId))) + .thenReturn(Mono.just(new PartyResponseUpsertDto())); + when(customerProfileService.upsertParty(eq(party2), eq(internalId))) + .thenReturn(Mono.error(dbError)); + + Mono result = helperProcessor.processParties( + streamTask, parties, internalId, externalId, customerProfileService + ); + + StepVerifier.create(result) + .expectNext(streamTask) + .verifyComplete(); + + verify(customerProfileService, times(2)).upsertParty(any(Party.class), eq(internalId)); + + verify(streamTask, never()).info(anyString(), anyString(), eq("completed_successfully"), anyString(), + anyString(), anyString(), any()); + } + + @Test + @DisplayName("should handle all errors and complete with warning") + void processParties_allErrors() { + List parties = List.of(party1, party2); + RuntimeException error1 = new RuntimeException("Error 1"); + RuntimeException error2 = new RuntimeException("Error 2"); + + when(customerProfileService.upsertParty(eq(party1), eq(internalId))) + .thenReturn(Mono.error(error1)); + when(customerProfileService.upsertParty(eq(party2), eq(internalId))) + .thenReturn(Mono.error(error2)); + + Mono result = helperProcessor.processParties( + streamTask, parties, internalId, externalId, customerProfileService + ); + + StepVerifier.create(result) + .expectNext(streamTask) + .verifyComplete(); + + verify(customerProfileService, times(2)).upsertParty(any(Party.class), eq(internalId)); + + verify(streamTask, never()).info(anyString(), anyString(), eq("upserted"), anyString(), any(), anyString(), + any()); + verify(streamTask, never()).info(anyString(), anyString(), eq("completed_successfully"), anyString(), + anyString(), anyString(), any()); + } + + @Test + @DisplayName("should filter out null parties from the list") + void processParties_withNullInList() { + List parties = List.of(party1, party2); + when(customerProfileService.upsertParty(any(Party.class), eq(internalId))) + .thenReturn(Mono.just(new PartyResponseUpsertDto())); + + Mono result = helperProcessor.processParties( + streamTask, parties, internalId, externalId, customerProfileService + ); + + StepVerifier.create(result) + .expectNext(streamTask) + .verifyComplete(); + + verify(customerProfileService, times(2)).upsertParty(any(Party.class), eq(internalId)); + verify(customerProfileService).upsertParty(eq(party1), eq(internalId)); + verify(customerProfileService).upsertParty(eq(party2), eq(internalId)); + } + } +} diff --git a/stream-legal-entity/legal-entity-core/src/test/java/com/backbase/stream/LegalEntitySagaTest.java b/stream-legal-entity/legal-entity-core/src/test/java/com/backbase/stream/LegalEntitySagaTest.java index 0db62d36f..242fea463 100644 --- a/stream-legal-entity/legal-entity-core/src/test/java/com/backbase/stream/LegalEntitySagaTest.java +++ b/stream-legal-entity/legal-entity-core/src/test/java/com/backbase/stream/LegalEntitySagaTest.java @@ -1,16 +1,20 @@ package com.backbase.stream; +import static com.backbase.stream.FixtureUtils.reflectiveAlphaFixtureMonkey; import static com.backbase.stream.service.UserService.REMOVED_PREFIX; import static java.util.Collections.singletonList; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.isNull; +import static org.mockito.Mockito.lenient; import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; -import static org.mockito.Mockito.lenient; +import com.backbase.customerprofile.api.integration.v1.model.PartyResponseUpsertDto; import com.backbase.dbs.accesscontrol.api.service.v3.model.ServiceAgreementParticipantsGetResponseBody; import com.backbase.dbs.contact.api.service.v2.model.AccessContextScope; import com.backbase.dbs.contact.api.service.v2.model.ContactsBulkPostRequestBody; @@ -45,6 +49,7 @@ import com.backbase.stream.legalentity.model.Limit; import com.backbase.stream.legalentity.model.Loan; import com.backbase.stream.legalentity.model.Multivalued; +import com.backbase.stream.legalentity.model.Party; import com.backbase.stream.legalentity.model.PhoneNumber; import com.backbase.stream.legalentity.model.Privilege; import com.backbase.stream.legalentity.model.ProductGroup; @@ -58,10 +63,12 @@ import com.backbase.stream.product.task.BatchProductGroupTask; import com.backbase.stream.product.task.ProductGroupTask; import com.backbase.stream.service.AccessGroupService; +import com.backbase.stream.service.CustomerProfileService; import com.backbase.stream.service.LegalEntityService; import com.backbase.stream.service.UserProfileService; import com.backbase.stream.service.UserService; import com.backbase.stream.worker.exception.StreamTaskException; +import com.navercorp.fixturemonkey.FixtureMonkey; import java.math.BigDecimal; import java.nio.charset.Charset; import java.time.Duration; @@ -122,10 +129,17 @@ class LegalEntitySagaTest { @Mock private UserKindSegmentationSaga userKindSegmentationSaga; + @Mock + private CustomerProfileService customerProfileService; + @Spy private final LegalEntitySagaConfigurationProperties legalEntitySagaConfigurationProperties = getLegalEntitySagaConfigurationProperties(); + private final FixtureMonkey fixtureMonkey = reflectiveAlphaFixtureMonkey; + + private static final int PARTY_SIZE = 10; + String leExternalId = "someLeExternalId"; String leParentExternalId = "someParentLeExternalId"; String leInternalId = "someLeInternalId"; @@ -338,6 +352,9 @@ void masterServiceAgreementCreation_activateSingleServiceAgreement() { void testCustomServiceAgreement_IfFetchedServiceAgreementExists_ThenSettingUp() { var task = setupLegalEntityTask(); + when(customerProfileService.upsertParty(any(Party.class), anyString())).thenReturn( + Mono.just(fixtureMonkey.giveMeOne(PartyResponseUpsertDto.class))); + mockAccessGroupService(userId); mockUserService(userId); @@ -351,6 +368,8 @@ void testCustomServiceAgreement_IfFetchedServiceAgreementExists_ThenSettingUp() void testCustomServiceAgreement_IfNoCustomServiceAgreementExists_ThenCreateMaster() { var task = setupLegalEntityTask(); + when(customerProfileService.upsertParty(any(Party.class), anyString())).thenReturn( + Mono.just(fixtureMonkey.giveMeOne(PartyResponseUpsertDto.class))); when(accessGroupService.getUserContextsByUserId(userId)).thenReturn(Mono.empty()); mockUserService(userId); @@ -364,6 +383,9 @@ void testCustomServiceAgreement_IfNoCustomServiceAgreementExists_ThenCreateMaste void testCustomServiceAgreement_IfNoMatchingCustomServiceAgreementExists_ThenCreateMaster() { LegalEntityTask task = setupLegalEntityTask(); + when(customerProfileService.upsertParty(any(Party.class), anyString())).thenReturn( + Mono.just(fixtureMonkey.giveMeOne(PartyResponseUpsertDto.class))); + when(accessGroupService.getUserContextsByUserId(userId)) .thenReturn( Mono.just(List.of(new ServiceAgreement().internalId("sa_id").externalId("sa_ext_id").purpose("TEST")))); @@ -380,6 +402,8 @@ void testCustomServiceAgreement_IfNoUserDataFoundWhileServiceAgreementFetch_thro LegalEntityTask task = setupLegalEntityTask(); when(userService.getUsersByLegalEntity(any(), anyInt(), anyInt())) .thenReturn(Mono.just(new GetUsersList().totalElements(0L).users(null))); + when(customerProfileService.upsertParty(any(Party.class), anyString())).thenReturn( + Mono.just(fixtureMonkey.giveMeOne(PartyResponseUpsertDto.class))); Assertions.assertThrows( StreamTaskException.class, () -> executeLegalEntityTaskAndBlock(task), @@ -388,6 +412,49 @@ void testCustomServiceAgreement_IfNoUserDataFoundWhileServiceAgreementFetch_thro verifyUserService(); } + @Test + void testSetupParties_IfPartyFound_ThenUpsertParty() { + var task = setupLegalEntityTask(); + when(customerProfileService.upsertParty(any(Party.class), anyString())).thenReturn( + Mono.just(fixtureMonkey.giveMeOne(PartyResponseUpsertDto.class))); + mockAccessGroupService(userId); + mockUserService(userId); + legalEntitySaga.executeTask(task).block(); + verify(customerProfileService, times(PARTY_SIZE)).upsertParty(any(Party.class), anyString()); + } + + @Test + void testProcessCustomerProfile_IfUpsertPartyError_ThenTrowException() { + var task = setupLegalEntityTask(); + var mockException = new WebClientResponseException( + "CPS Error", + 400, + "Bad Request", + null, + null, + null + ); + when(customerProfileService.upsertParty(any(Party.class), anyString())).thenReturn(Mono.error(mockException)); + + mockAccessGroupService(userId); + mockUserService(userId); + legalEntitySaga.executeTask(task).block(); + verify(customerProfileService, times(PARTY_SIZE)).upsertParty(any(Party.class), anyString()); + verify(task, times(PARTY_SIZE)).error( + eq(LegalEntitySaga.PARTY), + eq(LegalEntitySaga.PROCESS_CUSTOMER_PROFILE), + eq("failed"), + anyString(), + isNull(), + any(Throwable.class), + anyString(), + eq("Error upserting party: %s for LE: %s"), + anyString(), + anyString() + ); + } + + private void mockUserService(String userId) { when(userService.getUsersByLegalEntity(any(), anyInt(), anyInt())) .thenReturn(Mono.just(new GetUsersList().totalElements(1L) @@ -422,31 +489,32 @@ private LegalEntityTask setupLegalEntityTask() { var jobRole = new JobRole("someJobRole", "someJobRole"); jobRole.setFunctionGroups(singletonList(functionGroup)); - legalEntity = new LegalEntity("le_name", null, null); - legalEntity.setInternalId(leInternalId); - legalEntity.setExternalId(leExternalId); - legalEntity.setParentExternalId(leExternalId); - legalEntity.setProductGroups(singletonList(productGroup)); + var setuplegalEntity = new LegalEntity("le_name", null, null); + setuplegalEntity.setInternalId(leInternalId); + setuplegalEntity.setExternalId(leExternalId); + setuplegalEntity.setParentExternalId(leExternalId); + setuplegalEntity.setProductGroups(singletonList(productGroup)); + setuplegalEntity.setParties(fixtureMonkey.giveMe(Party.class, PARTY_SIZE)); var sa = new ServiceAgreement().externalId(customSaExId).addJobRolesItem(jobRole) .creatorLegalEntity(leExternalId); - var task = mockLegalEntityTask(legalEntity); + var task = mockLegalEntityTask(setuplegalEntity); - when(task.getLegalEntity()).thenReturn(legalEntity); + when(task.getLegalEntity()).thenReturn(setuplegalEntity); when(legalEntityService.getLegalEntityByExternalId(leExternalId)).thenReturn(Mono.empty()); - when(legalEntityService.getLegalEntityByInternalId(leInternalId)).thenReturn(Mono.just(legalEntity)); + when(legalEntityService.getLegalEntityByInternalId(leInternalId)).thenReturn(Mono.just(setuplegalEntity)); when(legalEntityService.getMasterServiceAgreementForInternalLegalEntityId(leInternalId)).thenReturn( Mono.empty()); - when(legalEntityService.createLegalEntity(any())).thenReturn(Mono.just(legalEntity)); + when(legalEntityService.createLegalEntity(any())).thenReturn(Mono.just(setuplegalEntity)); when(accessGroupService.setupJobRole(any(), any(), any())).thenReturn(Mono.just(jobRole)); when(accessGroupService.createServiceAgreement(any(), any())).thenReturn(Mono.just(sa)); when(batchProductIngestionSaga.process(any(ProductGroupTask.class))).thenReturn(productGroupTaskMono); when(legalEntitySagaConfigurationProperties.getServiceAgreementPurposes()).thenReturn( Set.of("FAMILY_BANKING")); when(userService.setupRealm(task.getLegalEntity())).thenReturn(Mono.just(new Realm())); - when(userService.linkLegalEntityToRealm(task.getLegalEntity())).thenReturn(Mono.just(legalEntity)); - when(legalEntityService.getLegalEntityByExternalId(leExternalId)).thenReturn(Mono.just(legalEntity)); - when(legalEntityService.putLegalEntity(any())).thenReturn(Mono.just(legalEntity)); + when(userService.linkLegalEntityToRealm(task.getLegalEntity())).thenReturn(Mono.just(setuplegalEntity)); + when(legalEntityService.getLegalEntityByExternalId(leExternalId)).thenReturn(Mono.just(setuplegalEntity)); + when(legalEntityService.putLegalEntity(any())).thenReturn(Mono.just(setuplegalEntity)); return task; } diff --git a/stream-legal-entity/legal-entity-core/src/test/java/com/backbase/stream/LegalEntitySagaV2Test.java b/stream-legal-entity/legal-entity-core/src/test/java/com/backbase/stream/LegalEntitySagaV2Test.java index 9380ab548..431a9680f 100644 --- a/stream-legal-entity/legal-entity-core/src/test/java/com/backbase/stream/LegalEntitySagaV2Test.java +++ b/stream-legal-entity/legal-entity-core/src/test/java/com/backbase/stream/LegalEntitySagaV2Test.java @@ -1,15 +1,18 @@ package com.backbase.stream; +import static com.backbase.stream.FixtureUtils.reflectiveAlphaFixtureMonkey; import static com.backbase.stream.service.UserService.REMOVED_PREFIX; import static java.util.Collections.singletonList; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import com.backbase.customerprofile.api.integration.v1.model.PartyResponseUpsertDto; import com.backbase.dbs.contact.api.service.v2.model.AccessContextScope; import com.backbase.dbs.contact.api.service.v2.model.ContactsBulkPostRequestBody; import com.backbase.dbs.contact.api.service.v2.model.ContactsBulkPostResponseBody; @@ -29,6 +32,7 @@ import com.backbase.stream.legalentity.model.LegalEntity; import com.backbase.stream.legalentity.model.LegalEntityType; import com.backbase.stream.legalentity.model.LegalEntityV2; +import com.backbase.stream.legalentity.model.Party; import com.backbase.stream.legalentity.model.SavingsAccount; import com.backbase.stream.legalentity.model.ServiceAgreement; import com.backbase.stream.legalentity.model.ServiceAgreementV2; @@ -36,9 +40,11 @@ import com.backbase.stream.mapper.LegalEntityV2toV1Mapper; import com.backbase.stream.mapper.ServiceAgreementV2ToV1Mapper; import com.backbase.stream.service.AccessGroupService; +import com.backbase.stream.service.CustomerProfileService; import com.backbase.stream.service.LegalEntityService; import com.backbase.stream.service.UserService; import com.backbase.stream.worker.exception.StreamTaskException; +import com.navercorp.fixturemonkey.FixtureMonkey; import java.nio.charset.Charset; import java.util.ArrayList; import java.util.Collections; @@ -84,10 +90,17 @@ class LegalEntitySagaV2Test { @Mock private UserKindSegmentationSaga userKindSegmentationSaga; + @Mock + private CustomerProfileService customerProfileService; + @Spy private final LegalEntitySagaConfigurationProperties legalEntitySagaConfigurationProperties = getLegalEntitySagaConfigurationProperties(); + private final FixtureMonkey fixtureMonkey = reflectiveAlphaFixtureMonkey; + + private static final int PARTY_SIZE = 20; + String leExternalId = "someLeExternalId"; String leParentExternalId = "someParentLeExternalId"; String leInternalId = "someLeInternalId"; @@ -352,6 +365,7 @@ void getMockLegalEntity() { legalEntityV2 = new LegalEntityV2() .internalId(leInternalId) .externalId(leExternalId) + .parties(fixtureMonkey.giveMe(Party.class, PARTY_SIZE)) .addAdministratorsItem(adminUser) .parentExternalId(leParentExternalId) .users(singletonList(regularUser)) @@ -383,6 +397,9 @@ void test_PostLegalContacts() { LegalEntityTaskV2 task = mockLegalEntityTask(legalEntityV2); when(contactsSaga.executeTask(any(ContactsTask.class))).thenReturn(getContactsTask(AccessContextScope.LE)); + when(customerProfileService.upsertParty(any(Party.class), anyString())).thenReturn( + Mono.just(fixtureMonkey.giveMeOne(PartyResponseUpsertDto.class))); + LegalEntityTaskV2 result = legalEntitySaga.executeTask(task).block(); Assertions.assertNotNull(result); @@ -442,6 +459,9 @@ void test_PostUserContacts() { when(contactsSaga.executeTask(any(ContactsTask.class))).thenReturn(getContactsTask(AccessContextScope.USER)); + when(customerProfileService.upsertParty(any(Party.class), anyString())).thenReturn( + Mono.just(fixtureMonkey.giveMeOne(PartyResponseUpsertDto.class))); + LegalEntityTaskV2 result = legalEntitySaga.executeTask(task).block(); Assertions.assertNotNull(result); @@ -456,6 +476,9 @@ void userKindSegmentationIsDisabled() { when(userKindSegmentationSaga.isEnabled()).thenReturn(false); + when(customerProfileService.upsertParty(any(Party.class), anyString())).thenReturn( + Mono.just(fixtureMonkey.giveMeOne(PartyResponseUpsertDto.class))); + legalEntitySaga.executeTask(mockLegalEntityTask(legalEntityV2)).block(); verify(userKindSegmentationSaga, never()).executeTask(Mockito.any()); @@ -468,6 +491,9 @@ void userKindSegmentationUsesLegalEntityCustomerCategory() { when(userKindSegmentationSaga.isEnabled()).thenReturn(true); + when(customerProfileService.upsertParty(any(Party.class), anyString())).thenReturn( + Mono.just(fixtureMonkey.giveMeOne(PartyResponseUpsertDto.class))); + legalEntitySaga.executeTask(mockLegalEntityTask(legalEntityV2)).block(); verify(userKindSegmentationSaga, times(0)).getDefaultCustomerCategory(); @@ -482,6 +508,8 @@ void userKindSegmentationUsesDefaultCustomerCategory() { when(userKindSegmentationSaga.getDefaultCustomerCategory()).thenReturn(CustomerCategory.RETAIL.getValue()); when(userKindSegmentationSaga.executeTask(any())).thenReturn( Mono.just(Mockito.mock(UserKindSegmentationTask.class))); + when(customerProfileService.upsertParty(any(Party.class), anyString())).thenReturn( + Mono.just(fixtureMonkey.giveMeOne(PartyResponseUpsertDto.class))); legalEntitySaga.executeTask(mockLegalEntityTask(legalEntityV2)).block(); @@ -496,6 +524,9 @@ void whenUserKindSegmentationIsEnabledAndNoCustomerCategoryCanBeDeterminedReturn when(userKindSegmentationSaga.isEnabled()).thenReturn(true); when(userKindSegmentationSaga.getDefaultCustomerCategory()).thenReturn(null); + when(customerProfileService.upsertParty(any(Party.class), anyString())).thenReturn( + Mono.just(fixtureMonkey.giveMeOne(PartyResponseUpsertDto.class))); + var task = mockLegalEntityTask(legalEntityV2); Assertions.assertThrows( @@ -505,6 +536,37 @@ void whenUserKindSegmentationIsEnabledAndNoCustomerCategoryCanBeDeterminedReturn ); } + @Test + void testSetupParties_IfPartyFound_ThenUpsertParty() { + getMockLegalEntity(); + var task = mockLegalEntityTask(legalEntityV2); + when(contactsSaga.executeTask(any(ContactsTask.class))).thenReturn(getContactsTask(AccessContextScope.LE)); + when(customerProfileService.upsertParty(any(Party.class), anyString())).thenReturn( + Mono.just(fixtureMonkey.giveMeOne(PartyResponseUpsertDto.class))); + var result = legalEntitySaga.executeTask(task).block(); + verify(customerProfileService, times(PARTY_SIZE)).upsertParty(any(Party.class), anyString()); + Assertions.assertNotNull(result); + } + + @Test + void testProcessCustomerProfile_IfUpsertPartyError_ThenTrowException() { + getMockLegalEntity(); + var task = mockLegalEntityTask(legalEntityV2); + when(contactsSaga.executeTask(any(ContactsTask.class))).thenReturn(getContactsTask(AccessContextScope.LE)); + var mockException = new WebClientResponseException( + "CPS Error", + 400, + "Bad Request", + null, + null, + null + ); + when(customerProfileService.upsertParty(any(Party.class), anyString())).thenReturn(Mono.error(mockException)); + var result = legalEntitySaga.executeTask(task).block(); + verify(customerProfileService, times(PARTY_SIZE)).upsertParty(any(Party.class), anyString()); + Assertions.assertNotNull(result); + } + @ParameterizedTest @MethodSource("parameters_upster_error") void upster_error(Exception ex, String error) { diff --git a/stream-legal-entity/legal-entity-http/src/test/java/com/backbase/stream/controller/ServiceAgreementControllerTest.java b/stream-legal-entity/legal-entity-http/src/test/java/com/backbase/stream/controller/ServiceAgreementControllerTest.java index df271a0b5..f8d0e2ced 100644 --- a/stream-legal-entity/legal-entity-http/src/test/java/com/backbase/stream/controller/ServiceAgreementControllerTest.java +++ b/stream-legal-entity/legal-entity-http/src/test/java/com/backbase/stream/controller/ServiceAgreementControllerTest.java @@ -7,6 +7,7 @@ import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.when; +import com.backbase.customerprofile.api.integration.v1.PartyManagementIntegrationApi; import com.backbase.dbs.accesscontrol.api.service.v3.LegalEntitiesApi; import com.backbase.dbs.accesscontrol.api.service.v3.model.FunctionGroupItem; import com.backbase.dbs.arrangement.api.service.v3.ArrangementsApi; @@ -18,6 +19,7 @@ import com.backbase.dbs.user.api.service.v2.model.GetUser; import com.backbase.loan.inbound.api.service.v1.LoansApi; import com.backbase.stream.audiences.UserKindSegmentationSaga; +import com.backbase.stream.clients.config.CustomerProfileClientConfig; import com.backbase.stream.config.LegalEntityHttpConfiguration; import com.backbase.stream.configuration.LegalEntitySagaConfiguration; import com.backbase.stream.configuration.UpdatedServiceAgreementSagaConfiguration; @@ -32,6 +34,7 @@ import com.backbase.stream.legalentity.model.UpdatedServiceAgreement; import com.backbase.stream.legalentity.model.UpdatedServiceAgreementResponse; import com.backbase.stream.legalentity.model.User; +import com.backbase.stream.mapper.PartyMapper; import com.backbase.stream.product.task.BatchProductGroupTask; import com.backbase.stream.product.task.ProductGroupTask; import com.backbase.stream.service.AccessGroupService; @@ -82,6 +85,9 @@ class ServiceAgreementControllerTest { @MockBean private com.backbase.identity.integration.api.service.ApiClient identityApiClient; + @MockBean + private com.backbase.customerprofile.api.integration.ApiClient customerProfileApiClient; + @MockBean private LimitsServiceApi limitsApi; @@ -106,6 +112,9 @@ class ServiceAgreementControllerTest { @MockBean private UserProfileManagementApi userProfileManagementApi; + @MockBean + private PartyManagementIntegrationApi partyManagementIntegrationApi; + @MockBean private com.backbase.dbs.user.profile.api.service.v2.UserProfileManagementApi userProfileManagement; @@ -121,6 +130,12 @@ class ServiceAgreementControllerTest { @MockBean private PlansService plansService; + @MockBean + private PartyMapper partyMapper; + + @MockBean + private CustomerProfileClientConfig customerProfileClientConfig; + @Autowired private WebTestClient webTestClient; diff --git a/stream-sdk/stream-parent/stream-test-support/pom.xml b/stream-sdk/stream-parent/stream-test-support/pom.xml index 2161d0eb7..b46a66139 100644 --- a/stream-sdk/stream-parent/stream-test-support/pom.xml +++ b/stream-sdk/stream-parent/stream-test-support/pom.xml @@ -25,6 +25,12 @@ 1.2 + + com.navercorp.fixturemonkey + fixture-monkey-starter + 1.1.11 + + org.iban4j iban4j @@ -82,6 +88,12 @@ wiremock-jre8-standalone 2.35.1 + + + com.backbase.buildingblocks + service-sdk-starter-test + test + diff --git a/stream-sdk/stream-parent/stream-test-support/src/main/java/com/backbase/stream/FixtureUtils.java b/stream-sdk/stream-parent/stream-test-support/src/main/java/com/backbase/stream/FixtureUtils.java new file mode 100644 index 000000000..7f433845e --- /dev/null +++ b/stream-sdk/stream-parent/stream-test-support/src/main/java/com/backbase/stream/FixtureUtils.java @@ -0,0 +1,23 @@ +package com.backbase.stream; + +import com.navercorp.fixturemonkey.FixtureMonkey; +import com.navercorp.fixturemonkey.api.introspector.FieldReflectionArbitraryIntrospector; +import com.navercorp.fixturemonkey.api.jqwik.JavaTypeArbitraryGenerator; +import com.navercorp.fixturemonkey.api.jqwik.JqwikPlugin; +import net.jqwik.api.Arbitraries; +import net.jqwik.api.arbitraries.StringArbitrary; + +public class FixtureUtils { + + public static final FixtureMonkey reflectiveAlphaFixtureMonkey = FixtureMonkey.builder() + .objectIntrospector(FieldReflectionArbitraryIntrospector.INSTANCE) + .defaultNotNull(true) + .plugin(new JqwikPlugin().javaTypeArbitraryGenerator(new JavaTypeArbitraryGenerator() { + @Override + public StringArbitrary strings() { + return Arbitraries.strings().alpha(); + } + })) + .build(); + +}