diff --git a/.aliases b/.aliases index be378ea2..705dbe38 100644 --- a/.aliases +++ b/.aliases @@ -82,7 +82,7 @@ alias pg-sql-restore='gunzip --stdout | docker exec -i hsadmin-ng-postgres psql alias fp='grep -r '@Accepts' src | sed -e 's/^.*@/@/g' | sort -u | wc -l' alias gw-spotless='./gradlew spotlessApply -x pitest -x test -x :processResources' -alias gw-test='. .aliases; ./gradlew test importOfficeData' +alias gw-test='. .aliases; ./gradlew test' alias gw-check='. .aliases; gw test importOfficeData check -x pitest -x :dependencyCheckAnalyze' # etc/docker-compose.yml limits CPUs+MEM and includes a PostgreSQL config for analysing slow queries diff --git a/.run/ImportOfficeData.run.xml b/.run/ImportOfficeData.run.xml index 6dfa1d1d..c796a3c3 100644 --- a/.run/ImportOfficeData.run.xml +++ b/.run/ImportOfficeData.run.xml @@ -67,4 +67,37 @@ true - \ No newline at end of file + + + + + + + false + true + + + + false + true + + + diff --git a/.tc-environment b/.tc-environment index 5c7b8d42..3f700f6d 100644 --- a/.tc-environment +++ b/.tc-environment @@ -1,4 +1,5 @@ export HSADMINNG_POSTGRES_JDBC_URL=jdbc:tc:postgresql:15.5-bookworm:///spring_boot_testcontainers +unset HSADMINNG_POSTGRES_JDBC_URL export HSADMINNG_POSTGRES_ADMIN_USERNAME=admin export HSADMINNG_POSTGRES_ADMIN_PASSWORD= export HSADMINNG_POSTGRES_RESTRICTED_USERNAME=restricted diff --git a/.unset-environment b/.unset-environment new file mode 100644 index 00000000..a9e4ee81 --- /dev/null +++ b/.unset-environment @@ -0,0 +1,8 @@ +unset HSADMINNG_POSTGRES_JDBC_URL +unset HSADMINNG_POSTGRES_ADMIN_USERNAME +unset HSADMINNG_POSTGRES_ADMIN_PASSWORD +unset HSADMINNG_POSTGRES_RESTRICTED_USERNAME +unset HSADMINNG_SUPERUSER +unset HSADMINNG_MIGRATION_DATA_PATH +unset LIQUIBASE_CONTEXT + diff --git a/src/test/java/net/hostsharing/hsadminng/hs/migration/BaseOfficeDataImport.java b/src/test/java/net/hostsharing/hsadminng/hs/migration/BaseOfficeDataImport.java new file mode 100644 index 00000000..758ab68d --- /dev/null +++ b/src/test/java/net/hostsharing/hsadminng/hs/migration/BaseOfficeDataImport.java @@ -0,0 +1,1209 @@ +package net.hostsharing.hsadminng.hs.migration; + +import net.hostsharing.hsadminng.hs.office.bankaccount.HsOfficeBankAccountEntity; +import net.hostsharing.hsadminng.hs.office.contact.HsOfficeContactRealEntity; +import net.hostsharing.hsadminng.hs.office.coopassets.HsOfficeCoopAssetsTransactionEntity; +import net.hostsharing.hsadminng.hs.office.coopassets.HsOfficeCoopAssetsTransactionType; +import net.hostsharing.hsadminng.hs.office.coopshares.HsOfficeCoopSharesTransactionEntity; +import net.hostsharing.hsadminng.hs.office.coopshares.HsOfficeCoopSharesTransactionType; +import net.hostsharing.hsadminng.hs.office.debitor.HsOfficeDebitorEntity; +import net.hostsharing.hsadminng.hs.office.membership.HsOfficeMembershipEntity; +import net.hostsharing.hsadminng.hs.office.membership.HsOfficeMembershipStatus; +import net.hostsharing.hsadminng.hs.office.partner.HsOfficePartnerDetailsEntity; +import net.hostsharing.hsadminng.hs.office.partner.HsOfficePartnerEntity; +import net.hostsharing.hsadminng.hs.office.person.HsOfficePersonEntity; +import net.hostsharing.hsadminng.hs.office.person.HsOfficePersonType; +import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelation; +import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationRealEntity; +import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationType; +import net.hostsharing.hsadminng.hs.office.sepamandate.HsOfficeSepaMandateEntity; +import net.hostsharing.hsadminng.rbac.rbacobject.BaseEntity; +import org.apache.commons.lang3.ArrayUtils; +import org.apache.commons.lang3.StringUtils; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; + +import java.io.Reader; +import java.time.LocalDate; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import static java.util.Arrays.stream; +import static java.util.Optional.ofNullable; +import static net.hostsharing.hsadminng.mapper.PostgresDateRange.toPostgresDateRange; +import static org.apache.commons.lang3.StringUtils.isBlank; +import static org.apache.commons.lang3.StringUtils.isNotBlank; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assumptions.assumeThat; +import static org.assertj.core.api.Fail.fail; + +/// Actual import of office data tables without config, for use as superclas of ImportOfficeData and ImportHostingAssets. +public abstract class BaseOfficeDataImport extends CsvDataImport { + + private static final String[] SUBSCRIBER_ROLES = new String[] { + "subscriber:operations-discussion", + "subscriber:operations-announce", + "subscriber:generalversammlung", + "subscriber:members-announce", + "subscriber:members-discussion", + "subscriber:customers-announce" + }; + private static final String[] KNOWN_ROLES = ArrayUtils.addAll( + new String[] { "partner", "vip-contact", "ex-partner", "billing", "contractual", "operation" }, + SUBSCRIBER_ROLES); + + // at least as the number of lines in business_partners.csv from test-data, but less than real data partner count + public static final int MAX_NUMBER_OF_TEST_DATA_PARTNERS = 100; + public static final int DELIBERATELY_BROKEN_BUSINESS_PARTNER_ID = 199; + + static int INITIAL_RELATION_ID = 2000000; + static int relationId = INITIAL_RELATION_ID; + + private static final List IGNORE_BUSINESS_PARTNERS = Arrays.asList( + 512167, // 11139, partner without contractual contact + 512170, // 11142, partner without contractual contact + 511725, // 10764, partner without contractual contact + // 512171, // 11143, partner without partner contact -- exc + -1 + ); + + private static final List IGNORE_CONTACTS = Arrays.asList( + 90547, // Kontakt hat keine Rolle + -1 + ); + + static Map contacts = new WriteOnceMap<>(); + static Map persons = new WriteOnceMap<>(); + static Map partners = new WriteOnceMap<>(); + static Map debitors = new WriteOnceMap<>(); + static Map memberships = new WriteOnceMap<>(); + + static Map relations = new WriteOnceMap<>(); + static Map sepaMandates = new WriteOnceMap<>(); + static Map bankAccounts = new WriteOnceMap<>(); + static Map coopShares = new WriteOnceMap<>(); + static Map coopAssets = new WriteOnceMap<>(); + + protected static void reset() { + contacts.clear(); + persons.clear(); + partners.clear(); + debitors.clear(); + memberships.clear(); + relations.clear(); + sepaMandates.clear(); + bankAccounts.clear(); + coopShares.clear(); + coopAssets.clear(); + relationId = INITIAL_RELATION_ID; + } + + @BeforeAll + static void resetOfficeImports() { + reset(); + } + + @Test + @Order(1) + void verifyInitialDatabase() { + // SQL DELETE for thousands of records takes too long, so we make sure, we only start with initial or test data + final var contactCount = (Integer) em.createNativeQuery("select count(*) from hs_office_contact", Integer.class) + .getSingleResult(); + assertThat(contactCount).isLessThan(20); + } + + @Test + @Order(1010) + void importBusinessPartners() { + + try (Reader reader = resourceReader(MIGRATION_DATA_PATH + "/office/business_partners.csv")) { + final var lines = readAllLines(reader); + importBusinessPartners(justHeader(lines), withoutHeader(lines)); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + @Test + @Order(1019) + void verifyBusinessPartners() { + assumeThatWeAreImportingControlledTestData(); + + // no contacts yet => mostly null values + assertThat(toJsonFormattedString(partners)).isEqualToIgnoringWhitespace(""" + { + 100=partner(P-10003: null null, null), + 120=partner(P-10020: null null, null), + 122=partner(P-11022: null null, null), + 132=partner(P-10152: null null, null), + 190=partner(P-19090: null null, null), + 199=partner(P-19999: null null, null), + 213=partner(P-10000: null null, null), + 541=partner(P-11018: null null, null), + 542=partner(P-11019: null null, null) + } + """); + assertThat(toJsonFormattedString(contacts)).isEqualTo("{}"); + assertThat(toJsonFormattedString(debitors)).isEqualToIgnoringWhitespace(""" + { + 100=debitor(D-1000300: rel(anchor='null null, null', type='DEBITOR'), mim), + 120=debitor(D-1002000: rel(anchor='null null, null', type='DEBITOR'), xyz), + 122=debitor(D-1102200: rel(anchor='null null, null', type='DEBITOR'), xxx), + 132=debitor(D-1015200: rel(anchor='null null, null', type='DEBITOR'), rar), + 190=debitor(D-1909000: rel(anchor='null null, null', type='DEBITOR'), yyy), + 199=debitor(D-1999900: rel(anchor='null null, null', type='DEBITOR'), zzz), + 213=debitor(D-1000000: rel(anchor='null null, null', type='DEBITOR'), hsh), + 541=debitor(D-1101800: rel(anchor='null null, null', type='DEBITOR'), wws), + 542=debitor(D-1101900: rel(anchor='null null, null', type='DEBITOR'), dph) + } + """); + assertThat(toJsonFormattedString(memberships)).isEqualToIgnoringWhitespace(""" + { + 100=Membership(M-1000300, P-10003, [2000-12-06,), ACTIVE), + 120=Membership(M-1002000, P-10020, [2000-12-06,2016-01-01), UNKNOWN), + 122=Membership(M-1102200, P-11022, [2021-04-01,), ACTIVE), + 132=Membership(M-1015200, P-10152, [2003-07-12,), ACTIVE), + 541=Membership(M-1101800, P-11018, [2021-05-17,), ACTIVE), + 542=Membership(M-1101900, P-11019, [2021-05-25,), ACTIVE) + } + """); + } + + @Test + @Order(1020) + void importContacts() { + try (Reader reader = resourceReader(MIGRATION_DATA_PATH + "/office/contacts.csv")) { + final var lines = readAllLines(reader); + importContacts(justHeader(lines), withoutHeader(lines)); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + @Test + @Order(1029) + void verifyContacts() { + assumeThatWeAreImportingControlledTestData(); + + assertThat(toJsonFormattedString(partners)).isEqualToIgnoringWhitespace(""" + { + 100=partner(P-10003: ?? Michael Mellis, Herr Michael Mellis , Michael Mellis), + 120=partner(P-10020: LP JM GmbH, Herr Philip Meyer-Contract , JM GmbH), + 122=partner(P-11022: ?? Test PS, Petra Schmidt , Test PS), + 132=partner(P-10152: ?? Ragnar IT-Beratung, Herr Ragnar Richter , Ragnar IT-Beratung), + 190=partner(P-19090: NP Camus, Cecilia, Frau Cecilia Camus ), + 199=partner(P-19999: null null, null), + 213=partner(P-10000: LP Hostsharing e.G., Firma Hostmaster Hostsharing , Hostsharing e.G.), + 541=partner(P-11018: ?? Wasserwerk Südholstein, Frau Christiane Milberg , Wasserwerk Südholstein), + 542=partner(P-11019: ?? Das Perfekte Haus, Herr Richard Wiese , Das Perfekte Haus) + } + """); + assertThat(toJsonFormattedString(contacts)).isEqualToIgnoringWhitespace(""" + { + 100=contact(caption='Herr Michael Mellis , Michael Mellis', emailAddresses='{ "main": "michael@Mellis.example.org"}'), + 1200=contact(caption='JM e.K.', emailAddresses='{ "main": "jm-ex-partner@example.org"}'), + 1201=contact(caption='Frau Dr. Jenny Meyer-Billing , JM GmbH', emailAddresses='{ "main": "jm-billing@example.org"}'), + 1202=contact(caption='Herr Andrew Meyer-Operation , JM GmbH', emailAddresses='{ "main": "am-operation@example.org"}'), + 1203=contact(caption='Herr Philip Meyer-Contract , JM GmbH', emailAddresses='{ "main": "pm-partner@example.org"}'), + 1204=contact(caption='Frau Tammy Meyer-VIP , JM GmbH', emailAddresses='{ "main": "tm-vip@example.org"}'), + 1301=contact(caption='Petra Schmidt , Test PS', emailAddresses='{ "main": "ps@example.com"}'), + 132=contact(caption='Herr Ragnar Richter , Ragnar IT-Beratung', emailAddresses='{ "main": "hostsharing@ragnar-richter.de"}'), + 1401=contact(caption='Frau Frauke Fanninga ', emailAddresses='{ "main": "ff@example.org"}'), + 1501=contact(caption='Frau Cecilia Camus ', emailAddresses='{ "main": "cc@example.org"}'), + 212=contact(caption='Firma Hostmaster Hostsharing , Hostsharing e.G.', emailAddresses='{ "main": "hostmaster@hostsharing.net"}'), + 90436=contact(caption='Frau Christiane Milberg , Wasserwerk Südholstein', emailAddresses='{ "main": "rechnung@ww-sholst.example.org"}'), + 90437=contact(caption='Herr Richard Wiese , Das Perfekte Haus', emailAddresses='{ "main": "admin@das-perfekte-haus.example.org"}'), + 90438=contact(caption='Herr Karim Metzger , Wasswerwerk Südholstein', emailAddresses='{ "main": "karim.metzger@ww-sholst.example.org"}'), + 90590=contact(caption='Herr Inhaber R. Wiese , Das Perfekte Haus', emailAddresses='{ "main": "515217@kkemail.example.org"}'), + 90629=contact(caption='Ragnar Richter ', emailAddresses='{ "main": "mail@ragnar-richter..example.org"}'), + 90677=contact(caption='Eike Henning ', emailAddresses='{ "main": "hostsharing@eike-henning..example.org"}'), + 90698=contact(caption='Jan Henning ', emailAddresses='{ "main": "mail@jan-henning.example.org"}') + } + """); + assertThat(toJsonFormattedString(persons)).isEqualToIgnoringWhitespace(""" + { + 100=person(personType='??', tradeName='Michael Mellis', familyName='Mellis', givenName='Michael'), + 1200=person(personType='LP', tradeName='JM e.K.'), + 1201=person(personType='LP', tradeName='JM GmbH', familyName='Meyer-Billing', givenName='Jenny'), + 1202=person(personType='LP', tradeName='JM GmbH', familyName='Meyer-Operation', givenName='Andrew'), + 1203=person(personType='LP', tradeName='JM GmbH', familyName='Meyer-Contract', givenName='Philip'), + 1204=person(personType='LP', tradeName='JM GmbH', familyName='Meyer-VIP', givenName='Tammy'), + 1301=person(personType='??', tradeName='Test PS', familyName='Schmidt', givenName='Petra'), + 132=person(personType='??', tradeName='Ragnar IT-Beratung', familyName='Richter', givenName='Ragnar'), + 1401=person(personType='NP', familyName='Fanninga', givenName='Frauke'), + 1501=person(personType='NP', familyName='Camus', givenName='Cecilia'), + 212=person(personType='LP', tradeName='Hostsharing e.G.', familyName='Hostsharing', givenName='Hostmaster'), + 90436=person(personType='??', tradeName='Wasserwerk Südholstein', familyName='Milberg', givenName='Christiane'), + 90437=person(personType='??', tradeName='Das Perfekte Haus', familyName='Wiese', givenName='Richard'), + 90438=person(personType='??', tradeName='Wasswerwerk Südholstein', familyName='Metzger', givenName='Karim'), + 90590=person(personType='??', tradeName='Das Perfekte Haus', familyName='Wiese', givenName='Inhaber R.'), + 90629=person(personType='NP', familyName='Richter', givenName='Ragnar'), + 90677=person(personType='NP', familyName='Henning', givenName='Eike'), + 90698=person(personType='NP', familyName='Henning', givenName='Jan') + } + """); + assertThat(toJsonFormattedString(debitors)).isEqualToIgnoringWhitespace(""" + { + 100=debitor(D-1000300: rel(anchor='?? Michael Mellis', type='DEBITOR', holder='?? Michael Mellis'), mim), + 120=debitor(D-1002000: rel(anchor='LP JM GmbH', type='DEBITOR', holder='LP JM GmbH'), xyz), + 122=debitor(D-1102200: rel(anchor='?? Test PS', type='DEBITOR', holder='?? Test PS'), xxx), + 132=debitor(D-1015200: rel(anchor='?? Ragnar IT-Beratung', type='DEBITOR', holder='?? Ragnar IT-Beratung'), rar), + 190=debitor(D-1909000: rel(anchor='NP Camus, Cecilia', type='DEBITOR', holder='NP Camus, Cecilia'), yyy), + 199=debitor(D-1999900: rel(anchor='null null, null', type='DEBITOR'), zzz), + 213=debitor(D-1000000: rel(anchor='LP Hostsharing e.G.', type='DEBITOR', holder='LP Hostsharing e.G.'), hsh), + 541=debitor(D-1101800: rel(anchor='?? Wasserwerk Südholstein', type='DEBITOR', holder='?? Wasserwerk Südholstein'), wws), + 542=debitor(D-1101900: rel(anchor='?? Das Perfekte Haus', type='DEBITOR', holder='?? Das Perfekte Haus'), dph) + } + """); + assertThat(toJsonFormattedString(memberships)).isEqualToIgnoringWhitespace(""" + { + 100=Membership(M-1000300, P-10003, [2000-12-06,), ACTIVE), + 120=Membership(M-1002000, P-10020, [2000-12-06,2016-01-01), UNKNOWN), + 122=Membership(M-1102200, P-11022, [2021-04-01,), ACTIVE), + 132=Membership(M-1015200, P-10152, [2003-07-12,), ACTIVE), + 541=Membership(M-1101800, P-11018, [2021-05-17,), ACTIVE), + 542=Membership(M-1101900, P-11019, [2021-05-25,), ACTIVE) + } + """); + assertThat(toJsonFormattedString(relations)).isEqualToIgnoringWhitespace(""" + { + 2000000=rel(anchor='LP Hostsharing e.G.', type='PARTNER', holder='?? Michael Mellis', contact='Herr Michael Mellis , Michael Mellis'), + 2000001=rel(anchor='?? Michael Mellis', type='DEBITOR', holder='?? Michael Mellis', contact='Herr Michael Mellis , Michael Mellis'), + 2000002=rel(anchor='LP Hostsharing e.G.', type='PARTNER', holder='?? Ragnar IT-Beratung', contact='Herr Ragnar Richter , Ragnar IT-Beratung'), + 2000003=rel(anchor='?? Ragnar IT-Beratung', type='DEBITOR', holder='?? Ragnar IT-Beratung', contact='Herr Ragnar Richter , Ragnar IT-Beratung'), + 2000004=rel(anchor='LP Hostsharing e.G.', type='PARTNER', holder='LP Hostsharing e.G.', contact='Firma Hostmaster Hostsharing , Hostsharing e.G.'), + 2000005=rel(anchor='LP Hostsharing e.G.', type='DEBITOR', holder='LP Hostsharing e.G.', contact='Firma Hostmaster Hostsharing , Hostsharing e.G.'), + 2000006=rel(anchor='LP Hostsharing e.G.', type='PARTNER', holder='?? Wasserwerk Südholstein', contact='Frau Christiane Milberg , Wasserwerk Südholstein'), + 2000007=rel(anchor='?? Wasserwerk Südholstein', type='DEBITOR', holder='?? Wasserwerk Südholstein', contact='Frau Christiane Milberg , Wasserwerk Südholstein'), + 2000008=rel(anchor='LP Hostsharing e.G.', type='PARTNER', holder='?? Das Perfekte Haus', contact='Herr Richard Wiese , Das Perfekte Haus'), + 2000009=rel(anchor='?? Das Perfekte Haus', type='DEBITOR', holder='?? Das Perfekte Haus', contact='Herr Inhaber R. Wiese , Das Perfekte Haus'), + 2000010=rel(anchor='LP Hostsharing e.G.', type='PARTNER', holder='LP JM GmbH', contact='Herr Philip Meyer-Contract , JM GmbH'), + 2000011=rel(anchor='LP JM GmbH', type='DEBITOR', holder='LP JM GmbH', contact='Frau Dr. Jenny Meyer-Billing , JM GmbH'), + 2000012=rel(anchor='LP Hostsharing e.G.', type='PARTNER', holder='?? Test PS', contact='Petra Schmidt , Test PS'), + 2000013=rel(anchor='?? Test PS', type='DEBITOR', holder='?? Test PS', contact='Petra Schmidt , Test PS'), + 2000014=rel(anchor='LP Hostsharing e.G.', type='PARTNER', holder='NP Camus, Cecilia', contact='Frau Cecilia Camus '), + 2000015=rel(anchor='NP Camus, Cecilia', type='DEBITOR', holder='NP Camus, Cecilia', contact='Frau Cecilia Camus '), + 2000016=rel(anchor='LP Hostsharing e.G.', type='PARTNER', holder='null null, null'), + 2000017=rel(anchor='null null, null', type='DEBITOR'), + 2000018=rel(anchor='LP Hostsharing e.G.', type='OPERATIONS', holder='LP Hostsharing e.G.', contact='Firma Hostmaster Hostsharing , Hostsharing e.G.'), + 2000019=rel(anchor='LP Hostsharing e.G.', type='REPRESENTATIVE', holder='LP Hostsharing e.G.', contact='Firma Hostmaster Hostsharing , Hostsharing e.G.'), + 2000020=rel(anchor='?? Michael Mellis', type='OPERATIONS', holder='?? Michael Mellis', contact='Herr Michael Mellis , Michael Mellis'), + 2000021=rel(anchor='?? Michael Mellis', type='REPRESENTATIVE', holder='?? Michael Mellis', contact='Herr Michael Mellis , Michael Mellis'), + 2000022=rel(anchor='?? Michael Mellis', type='SUBSCRIBER', mark='operations-discussion', holder='?? Michael Mellis', contact='Herr Michael Mellis , Michael Mellis'), + 2000023=rel(anchor='?? Michael Mellis', type='SUBSCRIBER', mark='operations-announce', holder='?? Michael Mellis', contact='Herr Michael Mellis , Michael Mellis'), + 2000024=rel(anchor='?? Michael Mellis', type='SUBSCRIBER', mark='generalversammlung', holder='?? Michael Mellis', contact='Herr Michael Mellis , Michael Mellis'), + 2000025=rel(anchor='?? Michael Mellis', type='SUBSCRIBER', mark='members-announce', holder='?? Michael Mellis', contact='Herr Michael Mellis , Michael Mellis'), + 2000026=rel(anchor='?? Michael Mellis', type='SUBSCRIBER', mark='members-discussion', holder='?? Michael Mellis', contact='Herr Michael Mellis , Michael Mellis'), + 2000027=rel(anchor='?? Ragnar IT-Beratung', type='OPERATIONS', holder='?? Ragnar IT-Beratung', contact='Herr Ragnar Richter , Ragnar IT-Beratung'), + 2000028=rel(anchor='?? Ragnar IT-Beratung', type='SUBSCRIBER', mark='operations-discussion', holder='?? Ragnar IT-Beratung', contact='Herr Ragnar Richter , Ragnar IT-Beratung'), + 2000029=rel(anchor='?? Ragnar IT-Beratung', type='SUBSCRIBER', mark='operations-announce', holder='?? Ragnar IT-Beratung', contact='Herr Ragnar Richter , Ragnar IT-Beratung'), + 2000030=rel(anchor='LP JM GmbH', type='EX_PARTNER', holder='LP JM e.K.', contact='JM e.K.'), + 2000031=rel(anchor='LP JM GmbH', type='OPERATIONS', holder='LP JM GmbH', contact='Herr Andrew Meyer-Operation , JM GmbH'), + 2000032=rel(anchor='LP JM GmbH', type='VIP_CONTACT', holder='LP JM GmbH', contact='Herr Andrew Meyer-Operation , JM GmbH'), + 2000033=rel(anchor='LP JM GmbH', type='SUBSCRIBER', mark='operations-announce', holder='LP JM GmbH', contact='Herr Andrew Meyer-Operation , JM GmbH'), + 2000034=rel(anchor='LP JM GmbH', type='REPRESENTATIVE', holder='LP JM GmbH', contact='Herr Philip Meyer-Contract , JM GmbH'), + 2000035=rel(anchor='LP JM GmbH', type='SUBSCRIBER', mark='members-announce', holder='LP JM GmbH', contact='Herr Philip Meyer-Contract , JM GmbH'), + 2000036=rel(anchor='LP JM GmbH', type='SUBSCRIBER', mark='customers-announce', holder='LP JM GmbH', contact='Herr Philip Meyer-Contract , JM GmbH'), + 2000037=rel(anchor='LP JM GmbH', type='VIP_CONTACT', holder='LP JM GmbH', contact='Frau Tammy Meyer-VIP , JM GmbH'), + 2000038=rel(anchor='?? Test PS', type='OPERATIONS', holder='?? Test PS', contact='Petra Schmidt , Test PS'), + 2000039=rel(anchor='?? Test PS', type='REPRESENTATIVE', holder='?? Test PS', contact='Petra Schmidt , Test PS'), + 2000040=rel(anchor='LP JM GmbH', type='SUBSCRIBER', mark='operations-announce', holder='NP Fanninga, Frauke', contact='Frau Frauke Fanninga '), + 2000041=rel(anchor='NP Camus, Cecilia', type='OPERATIONS', holder='NP Camus, Cecilia', contact='Frau Cecilia Camus '), + 2000042=rel(anchor='NP Camus, Cecilia', type='REPRESENTATIVE', holder='NP Camus, Cecilia', contact='Frau Cecilia Camus '), + 2000043=rel(anchor='?? Wasserwerk Südholstein', type='REPRESENTATIVE', holder='?? Wasserwerk Südholstein', contact='Frau Christiane Milberg , Wasserwerk Südholstein'), + 2000044=rel(anchor='?? Wasserwerk Südholstein', type='SUBSCRIBER', mark='generalversammlung', holder='?? Wasserwerk Südholstein', contact='Frau Christiane Milberg , Wasserwerk Südholstein'), + 2000045=rel(anchor='?? Wasserwerk Südholstein', type='SUBSCRIBER', mark='members-announce', holder='?? Wasserwerk Südholstein', contact='Frau Christiane Milberg , Wasserwerk Südholstein'), + 2000046=rel(anchor='?? Wasserwerk Südholstein', type='SUBSCRIBER', mark='members-discussion', holder='?? Wasserwerk Südholstein', contact='Frau Christiane Milberg , Wasserwerk Südholstein'), + 2000047=rel(anchor='?? Das Perfekte Haus', type='OPERATIONS', holder='?? Das Perfekte Haus', contact='Herr Richard Wiese , Das Perfekte Haus'), + 2000048=rel(anchor='?? Das Perfekte Haus', type='REPRESENTATIVE', holder='?? Das Perfekte Haus', contact='Herr Richard Wiese , Das Perfekte Haus'), + 2000049=rel(anchor='?? Das Perfekte Haus', type='SUBSCRIBER', mark='operations-discussion', holder='?? Das Perfekte Haus', contact='Herr Richard Wiese , Das Perfekte Haus'), + 2000050=rel(anchor='?? Das Perfekte Haus', type='SUBSCRIBER', mark='operations-announce', holder='?? Das Perfekte Haus', contact='Herr Richard Wiese , Das Perfekte Haus'), + 2000051=rel(anchor='?? Das Perfekte Haus', type='SUBSCRIBER', mark='generalversammlung', holder='?? Das Perfekte Haus', contact='Herr Richard Wiese , Das Perfekte Haus'), + 2000052=rel(anchor='?? Das Perfekte Haus', type='SUBSCRIBER', mark='members-announce', holder='?? Das Perfekte Haus', contact='Herr Richard Wiese , Das Perfekte Haus'), + 2000053=rel(anchor='?? Das Perfekte Haus', type='SUBSCRIBER', mark='members-discussion', holder='?? Das Perfekte Haus', contact='Herr Richard Wiese , Das Perfekte Haus'), + 2000054=rel(anchor='?? Wasserwerk Südholstein', type='OPERATIONS', holder='?? Wasswerwerk Südholstein', contact='Herr Karim Metzger , Wasswerwerk Südholstein'), + 2000055=rel(anchor='?? Wasserwerk Südholstein', type='SUBSCRIBER', mark='operations-discussion', holder='?? Wasswerwerk Südholstein', contact='Herr Karim Metzger , Wasswerwerk Südholstein'), + 2000056=rel(anchor='?? Wasserwerk Südholstein', type='SUBSCRIBER', mark='operations-announce', holder='?? Wasswerwerk Südholstein', contact='Herr Karim Metzger , Wasswerwerk Südholstein'), + 2000057=rel(anchor='?? Ragnar IT-Beratung', type='REPRESENTATIVE', holder='NP Richter, Ragnar', contact='Ragnar Richter '), + 2000058=rel(anchor='?? Ragnar IT-Beratung', type='SUBSCRIBER', mark='generalversammlung', holder='NP Richter, Ragnar', contact='Ragnar Richter '), + 2000059=rel(anchor='?? Ragnar IT-Beratung', type='SUBSCRIBER', mark='members-announce', holder='NP Richter, Ragnar', contact='Ragnar Richter '), + 2000060=rel(anchor='?? Ragnar IT-Beratung', type='SUBSCRIBER', mark='members-discussion', holder='NP Richter, Ragnar', contact='Ragnar Richter '), + 2000061=rel(anchor='?? Ragnar IT-Beratung', type='OPERATIONS', holder='NP Henning, Eike', contact='Eike Henning '), + 2000062=rel(anchor='?? Ragnar IT-Beratung', type='SUBSCRIBER', mark='operations-discussion', holder='NP Henning, Eike', contact='Eike Henning '), + 2000063=rel(anchor='?? Ragnar IT-Beratung', type='SUBSCRIBER', mark='operations-announce', holder='NP Henning, Eike', contact='Eike Henning '), + 2000064=rel(anchor='?? Ragnar IT-Beratung', type='OPERATIONS', holder='NP Henning, Jan', contact='Jan Henning ') + } + """); + } + + @Test + @Order(1030) + void importSepaMandates() { + try (Reader reader = resourceReader(MIGRATION_DATA_PATH + "/office/sepa_mandates.csv")) { + final var lines = readAllLines(reader); + importSepaMandates(justHeader(lines), withoutHeader(lines)); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + @Test + @Order(1039) + void verifySepaMandates() { + assumeThatWeAreImportingControlledTestData(); + + assertThat(toJsonFormattedString(bankAccounts)).isEqualToIgnoringWhitespace(""" + { + 132=bankAccount(DE37500105177419788228: holder='Michael Mellis', bic='GENODEF1HH2'), + 234234=bankAccount(DE37500105177419788228: holder='Michael Mellis', bic='INGDDEFFXXX'), + 235600=bankAccount(DE02300209000106531065: holder='JM e.K.', bic='CMCIDEDD'), + 235662=bankAccount(DE49500105174516484892: holder='JM GmbH', bic='INGDDEFFXXX'), + 30=bankAccount(DE02300209000106531065: holder='Ragnar Richter', bic='GENODEM1GLS'), + 386=bankAccount(DE49500105174516484892: holder='Wasserwerk Suedholstein', bic='NOLADE21WHO'), + 387=bankAccount(DE89370400440532013000: holder='Richard Wiese Das Perfekte Haus', bic='COBADEFFXXX') + } + """); + assertThat(toJsonFormattedString(sepaMandates)).isEqualToIgnoringWhitespace(""" + { + 132=SEPA-Mandate(DE37500105177419788228, HS-10003-20140801, 2013-12-01, [2013-12-01,)), + 234234=SEPA-Mandate(DE37500105177419788228, MH12345, 2004-06-12, [2004-06-15,)), + 235600=SEPA-Mandate(DE02300209000106531065, JM33344, 2004-01-15, [2004-01-20,2005-06-28)), + 235662=SEPA-Mandate(DE49500105174516484892, JM33344, 2005-06-28, [2005-07-01,)), + 30=SEPA-Mandate(DE02300209000106531065, HS-10152-20140801, 2013-12-01, [2013-12-01,2016-02-16)), + 386=SEPA-Mandate(DE49500105174516484892, HS-11018-20210512, 2021-05-12, [2021-05-17,)), + 387=SEPA-Mandate(DE89370400440532013000, HS-11019-20210519, 2021-05-19, [2021-05-25,)) + } + """); + } + + @Test + @Order(1040) + void importCoopShares() { + try (Reader reader = resourceReader(MIGRATION_DATA_PATH + "/office/share_transactions.csv")) { + final var lines = readAllLines(reader); + importCoopShares(justHeader(lines), withoutHeader(lines)); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + @Test + @Order(1041) + void verifyCoopShares() { + assumeThatWeAreImportingControlledTestData(); + + assertThat(toJsonFormattedString(coopShares)).isEqualToIgnoringWhitespace(""" + { + 241=CoopShareTransaction(M-1000300: 2011-12-05, SUBSCRIPTION, 16, 1000300), + 279=CoopShareTransaction(M-1015200: 2013-10-21, SUBSCRIPTION, 1, 1015200), + 33451=CoopShareTransaction(M-1002000: 2000-12-06, SUBSCRIPTION, 2, 1002000, initial share subscription), + 33701=CoopShareTransaction(M-1000300: 2005-01-10, SUBSCRIPTION, 40, 1000300, increase), + 33810=CoopShareTransaction(M-1002000: 2016-12-31, CANCELLATION, 22, 1002000, membership ended), + 3=CoopShareTransaction(M-1000300: 2000-12-06, SUBSCRIPTION, 80, 1000300, initial share subscription), + 523=CoopShareTransaction(M-1000300: 2020-12-08, SUBSCRIPTION, 96, 1000300, Kapitalerhoehung), + 562=CoopShareTransaction(M-1101800: 2021-05-17, SUBSCRIPTION, 4, 1101800, Beitritt), + 563=CoopShareTransaction(M-1101900: 2021-05-25, SUBSCRIPTION, 1, 1101900, Beitritt), + 721=CoopShareTransaction(M-1000300: 2023-10-10, SUBSCRIPTION, 96, 1000300, Kapitalerhoehung), + 90=CoopShareTransaction(M-1015200: 2003-07-12, SUBSCRIPTION, 1, 1015200) + } + """); + } + + @Test + @Order(1050) + void importCoopAssets() { + try (Reader reader = resourceReader(MIGRATION_DATA_PATH + "/office/asset_transactions.csv")) { + final var lines = readAllLines(reader); + importCoopAssets(justHeader(lines), withoutHeader(lines)); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + @Test + @Order(1059) + void verifyCoopAssets() { + assumeThatWeAreImportingControlledTestData(); + + assertThat(toJsonFormattedString(coopAssets)).isEqualToIgnoringWhitespace(""" + { + 1093=CoopAssetsTransaction(M-1000300: 2023-10-05, DEPOSIT, 3072, 1000300, Kapitalerhoehung - Ueberweisung), + 1094=CoopAssetsTransaction(M-1000300: 2023-10-06, DEPOSIT, 3072, 1000300, Kapitalerhoehung - Ueberweisung), + 31000=CoopAssetsTransaction(M-1002000: 2000-12-06, DEPOSIT, 128.00, 1002000, for subscription B), + 32000=CoopAssetsTransaction(M-1000300: 2005-01-10, DEPOSIT, 2560.00, 1000300, for subscription C), + 33001=CoopAssetsTransaction(M-1000300: 2005-01-10, TRANSFER, -512.00, 1000300, for transfer to 10), + 33002=CoopAssetsTransaction(M-1002000: 2005-01-10, ADOPTION, 512.00, 1002000, for transfer from 7), + 34001=CoopAssetsTransaction(M-1002000: 2016-12-31, CLEARING, -8.00, 1002000, for cancellation D), + 34002=CoopAssetsTransaction(M-1002000: 2016-12-31, DISBURSAL, -100.00, 1002000, for cancellation D), + 34003=CoopAssetsTransaction(M-1002000: 2016-12-31, LOSS, -20.00, 1002000, for cancellation D), + 35001=CoopAssetsTransaction(M-1909000: 2024-01-15, DEPOSIT, 128.00, 1909000, for subscription E), + 35002=CoopAssetsTransaction(M-1909000: 2024-01-20, ADJUSTMENT, -128.00, 1909000, chargeback for subscription E, M-1909000:DEP:+128.00), + 358=CoopAssetsTransaction(M-1000300: 2000-12-06, DEPOSIT, 5120, 1000300, for subscription A), + 442=CoopAssetsTransaction(M-1015200: 2003-07-07, DEPOSIT, 64, 1015200), + 577=CoopAssetsTransaction(M-1000300: 2011-12-12, DEPOSIT, 1024, 1000300), + 632=CoopAssetsTransaction(M-1015200: 2013-10-21, DEPOSIT, 64, 1015200), + 885=CoopAssetsTransaction(M-1000300: 2020-12-15, DEPOSIT, 6144, 1000300, Einzahlung), + 924=CoopAssetsTransaction(M-1101800: 2021-05-21, DEPOSIT, 256, 1101800, Beitritt - Lastschrift), + 925=CoopAssetsTransaction(M-1101900: 2021-05-31, DEPOSIT, 64, 1101900, Beitritt - Lastschrift) + } + """); + } + + @Test + @Order(1099) + void verifyMemberships() { + assumeThatWeAreImportingControlledTestData(); + + assertThat(toJsonFormattedString(memberships)).isEqualToIgnoringWhitespace(""" + { + 100=Membership(M-1000300, P-10003, [2000-12-06,), ACTIVE), + 120=Membership(M-1002000, P-10020, [2000-12-06,2016-01-01), UNKNOWN), + 122=Membership(M-1102200, P-11022, [2021-04-01,), ACTIVE), + 132=Membership(M-1015200, P-10152, [2003-07-12,), ACTIVE), + 190=Membership(M-1909000, P-19090, empty, INVALID), + 541=Membership(M-1101800, P-11018, [2021-05-17,), ACTIVE), + 542=Membership(M-1101900, P-11019, [2021-05-25,), ACTIVE) + } + """); + } + + @Test + @Order(2000) + void verifyAllPartnersHavePersons() { + partners.forEach((id, p) -> { + final var partnerRel = p.getPartnerRel(); + assertThat(partnerRel).describedAs("partner " + id + " without partnerRel").isNotNull(); + if (id != DELIBERATELY_BROKEN_BUSINESS_PARTNER_ID) { + logError(() -> { + assertThat(partnerRel.getContact()).describedAs("partner " + id + " without partnerRel.contact") + .isNotNull(); + assertThat(partnerRel.getContact().getCaption()).describedAs( + "partner " + id + " without valid partnerRel.contact").isNotNull(); + }); + logError(() -> { + assertThat(partnerRel.getHolder()).describedAs("partner " + id + " without partnerRel.relHolder") + .isNotNull(); + assertThat(partnerRel.getHolder().getPersonType()).describedAs( + "partner " + id + " without valid partnerRel.relHolder").isNotNull(); + }); + } + }); + } + + @Test + @Order(3001) + void removeSelfRepresentativeRelations() { + + // this happens if a natural person is marked as 'contractual' for itself + final var idsToRemove = new HashSet(); + relations.forEach((id, r) -> { + if (r.getHolder() == r.getAnchor()) { + idsToRemove.add(id); + } + }); + + // remove self-representatives + idsToRemove.forEach(id -> { + System.out.println("removing self representative relation: " + relations.get(id).toString()); + relations.remove(id); + }); + } + + @Test + @Order(3002) + void removeEmptyRelations() { + + // avoid a error when persisting the deliberately invalid partner entry #99 + final var idsToRemove = new HashSet(); + relations.forEach((id, r) -> { + if (r.getContact() == null || r.getContact().getCaption() == null || + r.getHolder() == null || r.getHolder().getPersonType() == null) { + idsToRemove.add(id); + } + }); + + // expected relations created from partner #99 + Hostsharing eG itself + idsToRemove.forEach(id -> { + System.out.println("removing unused relation: " + relations.get(id).toString()); + relations.remove(id); + }); + } + + @Test + @Order(3003) + void removeEmptyPartners() { + + // avoid a error when persisting the deliberately invalid partner entry #99 + final var idsToRemove = new HashSet(); + partners.forEach((id, r) -> { + final var partnerRole = r.getPartnerRel(); + + // such a record is in test data to test error messages + if (partnerRole.getContact() == null || partnerRole.getContact().getCaption() == null || + partnerRole.getHolder() == null | partnerRole.getHolder().getPersonType() == null) { + idsToRemove.add(id); + } + }); + + // expected partners created from partner #99 + Hostsharing eG itself + idsToRemove.forEach(id -> { + System.out.println("removing unused partner: " + partners.get(id).toString()); + partners.remove(id); + }); + } + + @Test + @Order(3004) + void removeEmptyDebitors() { + + // avoid a error when persisting the deliberately invalid partner entry #99 + final var idsToRemove = new HashSet(); + debitors.forEach((id, d) -> { + final var debitorRel = d.getDebitorRel(); + if (debitorRel.getContact() == null || debitorRel.getContact().getCaption() == null || + debitorRel.getAnchor() == null || debitorRel.getAnchor().getPersonType() == null || + debitorRel.getHolder() == null || debitorRel.getHolder().getPersonType() == null) { + idsToRemove.add(id); + } + }); + idsToRemove.forEach(id -> debitors.remove(id)); + + assumeThatWeAreImportingControlledTestData(); + assertThat(idsToRemove.size()).isEqualTo(1); // only from partner #99 + } + + @Test + @Order(3005) + void removeEmptyPersons() { + // avoid a error when persisting the deliberately invalid partner entry #99 + final var idsToRemove = new HashSet(); + persons.forEach((id, p) -> { + if (p.getPersonType() == null || + (p.getFamilyName() == null && p.getGivenName() == null && p.getTradeName() == null)) { + idsToRemove.add(id); + } + }); + idsToRemove.forEach(id -> persons.remove(id)); + + assumeThatWeAreImportingControlledTestData(); + assertThat(idsToRemove.size()).isEqualTo(0); + } + + @Test + @Order(9000) + @ContinueOnFailure + void logCollectedErrorsBeforePersist() { + assertNoErrors(); + } + + @Test + @Order(9010) + void persistOfficeEntities() { + + System.out.println("PERSISTING office data to database '" + jdbcUrl + "' as user '" + postgresAdminUser + "'"); + deleteTestDataFromHsOfficeTables(); + resetHsOfficeSequences(); + deleteFromTestTables(); + deleteFromRbacTables(); + + jpaAttempt.transacted(() -> { + context(rbacSuperuser); + contacts.forEach(this::persist); + updateLegacyIds(contacts, "hs_office_contact_legacy_id", "contact_id"); + }).assertSuccessful(); + + jpaAttempt.transacted(() -> { + context(rbacSuperuser); + persons.forEach(this::persist); + relations.forEach((id, rel) -> this.persist(id, rel.getAnchor())); + relations.forEach((id, rel) -> this.persist(id, rel.getHolder())); + }).assertSuccessful(); + + jpaAttempt.transacted(() -> { + context(rbacSuperuser); + relations.forEach(this::persist); + }).assertSuccessful(); + + System.out.println("persisting " + partners.size() + " partners"); + jpaAttempt.transacted(() -> { + context(rbacSuperuser); + partners.forEach((id, partner) -> { + // TODO: this is ugly and I don't know why it's suddenly necessary + partner.getPartnerRel().setAnchor(em.merge(partner.getPartnerRel().getAnchor())); + partner.getPartnerRel().setHolder(em.merge(partner.getPartnerRel().getHolder())); + partner.getPartnerRel().setContact(em.merge(partner.getPartnerRel().getContact())); + partner.setPartnerRel(em.merge(partner.getPartnerRel())); + em.persist(partner); + }); + updateLegacyIds(partners, "hs_office_partner_legacy_id", "bp_id"); + }).assertSuccessful(); + + jpaAttempt.transacted(() -> { + context(rbacSuperuser); + debitors.forEach((id, debitor) -> { + debitor.setDebitorRel(em.merge(debitor.getDebitorRel())); + persist(id, debitor); + }); + }).assertSuccessful(); + + jpaAttempt.transacted(() -> { + context(rbacSuperuser); + memberships.forEach(this::persist); + }).assertSuccessful(); + + jpaAttempt.transacted(() -> { + context(rbacSuperuser); + bankAccounts.forEach(this::persist); + }).assertSuccessful(); + + jpaAttempt.transacted(() -> { + context(rbacSuperuser); + sepaMandates.forEach(this::persist); + updateLegacyIds(sepaMandates, "hs_office_sepamandate_legacy_id", "sepa_mandate_id"); + }).assertSuccessful(); + + jpaAttempt.transacted(() -> { + context(rbacSuperuser); + coopShares.forEach(this::persist); + updateLegacyIds(coopShares, "hs_office_coopsharestransaction_legacy_id", "member_share_id"); + + }).assertSuccessful(); + + jpaAttempt.transacted(() -> { + context(rbacSuperuser); + coopAssets.forEach(this::persist); + updateLegacyIds(coopAssets, "hs_office_coopassetstransaction_legacy_id", "member_asset_id"); + }).assertSuccessful(); + + } + + @Test + @Order(9190) + void verifyMembershipsActuallyPersisted() { + final var biCount = (Integer) em.createNativeQuery("select count(*) from hs_office_membership", Integer.class) + .getSingleResult(); + assertThat(biCount).isGreaterThan(isImportingControlledTestData() ? 5 : 300); + } + + private static boolean isImportingControlledTestData() { + return partners.size() <= MAX_NUMBER_OF_TEST_DATA_PARTNERS; + } + + private static void assumeThatWeAreImportingControlledTestData() { + assumeThat(partners.size()).isLessThanOrEqualTo(MAX_NUMBER_OF_TEST_DATA_PARTNERS); + } + + private void updateLegacyIds( + Map entities, + final String legacyIdTable, + final String legacyIdColumn) { + em.flush(); + entities.forEach((id, entity) -> em.createNativeQuery(""" + UPDATE ${legacyIdTable} + SET ${legacyIdColumn} = :legacyId + WHERE uuid = :uuid + """ + .replace("${legacyIdTable}", legacyIdTable) + .replace("${legacyIdColumn}", legacyIdColumn)) + .setParameter("legacyId", id) + .setParameter("uuid", entity.getUuid()) + .executeUpdate() + ); + } + + @Test + @Order(9999) + @ContinueOnFailure + void logCollectedErrors() { + this.assertNoErrors(); + } + + private void importBusinessPartners(final String[] header, final List records) { + + final var columns = new Columns(header); + + records.stream() + .map(this::trimAll) + .map(row -> new Record(columns, row)) + .forEach(rec -> { + final Integer bpId = rec.getInteger("bp_id"); + if (IGNORE_BUSINESS_PARTNERS.contains(bpId)) { + return; + } + + final var person = HsOfficePersonEntity.builder().build(); + + final var partnerRel = addRelation( + HsOfficeRelationType.PARTNER, + null, // is set after contacts when the person for 'Hostsharing eG' is known + person, + null // is set during contacts import depending on assigned roles + ); + + final var partner = HsOfficePartnerEntity.builder() + .partnerNumber(rec.getInteger("member_id")) + .details(HsOfficePartnerDetailsEntity.builder().build()) + .partnerRel(partnerRel) + .build(); + partners.put(bpId, partner); + + final var debitorRel = addRelation( + HsOfficeRelationType.DEBITOR, partnerRel.getHolder(), // partner person + null, // will be set in contacts import + null // will beset in contacts import + ); + + final var debitor = HsOfficeDebitorEntity.builder() + .debitorNumberSuffix("00") + .partner(partner) + .debitorRel(debitorRel) + .defaultPrefix(rec.getString("member_code").replace("hsh00-", "")) + .billable(rec.isEmpty("free") || rec.getString("free").equals("f")) + .vatReverseCharge(rec.getBoolean("exempt_vat")) + .vatBusiness("GROSS".equals(rec.getString("indicator_vat"))) // TODO: remove + .vatId(rec.getString("uid_vat")) + .build(); + debitors.put(bpId, debitor); + + if (isNotBlank(rec.getString("member_since"))) { + assertThat(rec.getInteger("member_id")).isEqualTo(partner.getPartnerNumber()); + final var membership = HsOfficeMembershipEntity.builder() + .partner(partner) + .memberNumberSuffix("00") + .validity(toPostgresDateRange( + rec.getLocalDate("member_since"), + rec.getLocalDate("member_until"))) + .membershipFeeBillable(rec.isEmpty("member_role")) + .status( + isBlank(rec.getString("member_until")) + ? HsOfficeMembershipStatus.ACTIVE + : HsOfficeMembershipStatus.UNKNOWN) + .build(); + memberships.put(bpId, membership); + } + }); + } + + private void importCoopShares(final String[] header, final List records) { + + final var columns = new Columns(header); + + records.stream() + .map(this::trimAll) + .map(row -> new Record(columns, row)) + .forEach(rec -> { + final var bpId = rec.getInteger("bp_id"); + if (IGNORE_BUSINESS_PARTNERS.contains(bpId)) { + return; + } + + final var member = ofNullable(memberships.get(bpId)) + .orElseGet(() -> createOnDemandMembership(bpId)); + + final var shareTransaction = HsOfficeCoopSharesTransactionEntity.builder() + .membership(member) + .valueDate(rec.getLocalDate("date")) + .transactionType( + "SUBSCRIPTION".equals(rec.getString("action")) + ? HsOfficeCoopSharesTransactionType.SUBSCRIPTION + : "UNSUBSCRIPTION".equals(rec.getString("action")) + ? HsOfficeCoopSharesTransactionType.CANCELLATION + : HsOfficeCoopSharesTransactionType.ADJUSTMENT + ) + .shareCount(rec.getInteger("quantity")) + .comment(rec.getString("comment")) + .reference(member.getMemberNumber().toString()) + .build(); + + if (shareTransaction.getTransactionType() == HsOfficeCoopSharesTransactionType.ADJUSTMENT) { + final var negativeValue = -shareTransaction.getShareCount(); + final var adjustedShareTx = coopShares.values().stream().filter(a -> + a.getTransactionType() != HsOfficeCoopSharesTransactionType.ADJUSTMENT && + a.getMembership() == shareTransaction.getMembership() && + a.getShareCount() == negativeValue) + .findAny() + .orElseThrow(() -> new IllegalStateException( + "cannot determine share reverse entry for adjustment " + shareTransaction)); + shareTransaction.setAdjustedShareTx(adjustedShareTx); + } + coopShares.put(rec.getInteger("member_share_id"), shareTransaction); + }); + } + + private void importCoopAssets(final String[] header, final List records) { + + final var columns = new Columns(header); + + records.stream() + .map(this::trimAll) + .map(row -> new Record(columns, row)) + .forEach(rec -> { + final var bpId = rec.getInteger("bp_id"); + + if (this.IGNORE_BUSINESS_PARTNERS.contains(bpId)) { + return; + } + + final var member = ofNullable(memberships.get(bpId)) + .orElseGet(() -> createOnDemandMembership(bpId)); + + final var assetTypeMapping = new HashMap() { + + { + put("ADJUSTMENT", HsOfficeCoopAssetsTransactionType.ADJUSTMENT); + put("HANDOVER", HsOfficeCoopAssetsTransactionType.TRANSFER); + put("ADOPTION", HsOfficeCoopAssetsTransactionType.ADOPTION); + put("LOSS", HsOfficeCoopAssetsTransactionType.LOSS); + put("CLEARING", HsOfficeCoopAssetsTransactionType.CLEARING); + put("PRESCRIPTION", HsOfficeCoopAssetsTransactionType.LIMITATION); + put("PAYBACK", HsOfficeCoopAssetsTransactionType.DISBURSAL); + put("PAYMENT", HsOfficeCoopAssetsTransactionType.DEPOSIT); + } + + public HsOfficeCoopAssetsTransactionType get(final String key) { + final var value = super.get(key); + if (value != null) { + return value; + } + throw new IllegalStateException("no mapping value found for: " + key); + } + }; + + final var assetTransaction = HsOfficeCoopAssetsTransactionEntity.builder() + .membership(member) + .valueDate(rec.getLocalDate("date")) + .transactionType(assetTypeMapping.get(rec.getString("action"))) + .assetValue(rec.getBigDecimal("amount")) + .comment(rec.getString("comment")) + .reference(member.getMemberNumber().toString()) + .build(); + + if (assetTransaction.getTransactionType() == HsOfficeCoopAssetsTransactionType.ADJUSTMENT) { + final var negativeValue = assetTransaction.getAssetValue().negate(); + final var adjustedAssetTx = coopAssets.values().stream().filter(a -> + a.getTransactionType() != HsOfficeCoopAssetsTransactionType.ADJUSTMENT && + a.getMembership() == assetTransaction.getMembership() && + a.getAssetValue().equals(negativeValue)) + .findAny() + .orElseThrow(() -> new IllegalStateException( + "cannot determine asset reverse entry for adjustment " + assetTransaction)); + assetTransaction.setAdjustedAssetTx(adjustedAssetTx); + } + + coopAssets.put(rec.getInteger("member_asset_id"), assetTransaction); + }); + } + + private static HsOfficeMembershipEntity createOnDemandMembership(final Integer bpId) { + final var onDemandMembership = HsOfficeMembershipEntity.builder() + .memberNumberSuffix("00") + .membershipFeeBillable(false) + .partner(partners.get(bpId)) + .status(HsOfficeMembershipStatus.INVALID) + .build(); + memberships.put(bpId, onDemandMembership); + return onDemandMembership; + } + + private void importSepaMandates(final String[] header, final List records) { + + final var columns = new Columns(header); + + records.stream() + .map(this::trimAll) + .map(row -> new Record(columns, row)) + .forEach(rec -> { + final var debitor = debitors.get(rec.getInteger("bp_id")); + + if (this.IGNORE_BUSINESS_PARTNERS.contains(rec.getInteger("bp_id"))) { + return; + } + + final var sepaMandate = HsOfficeSepaMandateEntity.builder() + .debitor(debitor) + .bankAccount(HsOfficeBankAccountEntity.builder() + .holder(rec.getString("bank_customer")) + // .bankName(rec.get("bank_name")) // not supported + .iban(rec.getString("bank_iban")) + .bic(rec.getString("bank_bic")) + .build()) + .reference(rec.getString("mandat_ref")) + .agreement(LocalDate.parse(rec.getString("mandat_signed"))) + .validity(toPostgresDateRange( + rec.getLocalDate("mandat_since"), + rec.getLocalDate("mandat_until"))) + .build(); + + sepaMandates.put(rec.getInteger("sepa_mandat_id"), sepaMandate); + bankAccounts.put(rec.getInteger("sepa_mandat_id"), sepaMandate.getBankAccount()); + }); + } + + private void importContacts(final String[] header, final List records) { + + final var columns = new Columns(header); + + records.stream() + .map(this::trimAll) + .map(row -> new Record(columns, row)) + .forEach(rec -> { + final var contactId = rec.getInteger("contact_id"); + final var bpId = rec.getInteger("bp_id"); + + if (IGNORE_CONTACTS.contains(contactId)) { + return; + } + if (IGNORE_BUSINESS_PARTNERS.contains(bpId)) { + return; + } + + if (rec.getString("roles").isBlank()) { + fail("empty roles assignment not allowed for contact_id: " + contactId); + } + + final var partner = partners.get(bpId); + final var debitor = debitors.get(bpId); + + final var partnerPerson = partner.getPartnerRel().getHolder(); + if (containsPartnerRel(rec)) { + addPerson(partnerPerson, rec); + } + + HsOfficePersonEntity contactPerson = partnerPerson; + if (!StringUtils.equals(rec.getString("firma"), partnerPerson.getTradeName()) || + !StringUtils.equals(rec.getString("first_name"), partnerPerson.getGivenName()) || + !StringUtils.equals(rec.getString("last_name"), partnerPerson.getFamilyName())) { + contactPerson = addPerson(HsOfficePersonEntity.builder().build(), rec); + } + + final var contact = HsOfficeContactRealEntity.builder().build(); + initContact(contact, rec); + + if (containsPartnerRel(rec)) { + assertThat(partner.getPartnerRel().getContact()).isNull(); + partner.getPartnerRel().setContact(contact); + } + if (containsRole(rec, "billing")) { + assertThat(debitor.getDebitorRel().getContact()).isNull(); + debitor.getDebitorRel().setHolder(contactPerson); + debitor.getDebitorRel().setContact(contact); + } + if (containsRole(rec, "operation")) { + addRelation(HsOfficeRelationType.OPERATIONS, partnerPerson, contactPerson, contact); + } + if (containsRole(rec, "contractual")) { + addRelation(HsOfficeRelationType.REPRESENTATIVE, partnerPerson, contactPerson, contact); + } + if (containsRole(rec, "ex-partner")) { + addRelation(HsOfficeRelationType.EX_PARTNER, partnerPerson, contactPerson, contact); + } + if (containsRole(rec, "vip-contact")) { + addRelation(HsOfficeRelationType.VIP_CONTACT, partnerPerson, contactPerson, contact); + } + for (String subscriberRole : SUBSCRIBER_ROLES) { + if (containsRole(rec, subscriberRole)) { + addRelation(HsOfficeRelationType.SUBSCRIBER, partnerPerson, contactPerson, contact) + .setMark(subscriberRole.split(":")[1]) + ; + } + } + verifyContainsOnlyKnownRoles(rec.getString("roles")); + }); + + assertNoMissingContractualRelations(); + useHostsharingAsPartnerAnchor(); + } + + private static void assertNoMissingContractualRelations() { + final var contractualMissing = new HashSet(); + partners.forEach((id, partner) -> { + final var partnerPerson = partner.getPartnerRel().getHolder(); + if (relations.values().stream() + .filter(rel -> rel.getAnchor() == partnerPerson && rel.getType() == HsOfficeRelationType.REPRESENTATIVE) + .findFirst().isEmpty()) { + contractualMissing.add(partner.getPartnerNumber()); + } + }); + if (isImportingControlledTestData()) { + assertThat(contractualMissing).containsOnly(19999); // deliberately wrong partner entry + } else { + assertThat(contractualMissing).as("partners without contractual contact found").isEmpty(); + } + } + + private static void useHostsharingAsPartnerAnchor() { + final var mandant = persons.values().stream() + .filter(p -> p.getTradeName().startsWith("Hostsharing e")) + .findFirst() + .orElseThrow(); + relations.values().stream() + .filter(r -> r.getType() == HsOfficeRelationType.PARTNER) + .forEach(r -> r.setAnchor(mandant)); + } + + private static boolean containsRole(final Record rec, final String role) { + final var roles = rec.getString("roles"); + return ("," + roles + ",").contains("," + role + ","); + } + + private static boolean containsPartnerRel(final Record rec) { + return containsRole(rec, "partner"); + } + + private static HsOfficeRelationRealEntity addRelation( + final HsOfficeRelationType type, + final HsOfficePersonEntity anchor, + final HsOfficePersonEntity holder, + final HsOfficeContactRealEntity contact) { + final var rel = HsOfficeRelationRealEntity.builder() + .anchor(anchor) + .holder(holder) + .contact(contact) + .type(type) + .build(); + relations.put(relationId++, rel); + return rel; + } + + private HsOfficePersonEntity addPerson(final HsOfficePersonEntity person, final Record contactRecord) { + // TODO: title+salutation: add to person + person.setGivenName(contactRecord.getString("first_name")); + person.setFamilyName(contactRecord.getString("last_name")); + person.setTradeName(contactRecord.getString("firma")); + determinePersonType(person, contactRecord.getString("roles")); + + persons.put(contactRecord.getInteger("contact_id"), person); + return person; + } + + private static void determinePersonType(final HsOfficePersonEntity person, final String roles) { + if (person.getTradeName().isBlank()) { + person.setPersonType(HsOfficePersonType.NATURAL_PERSON); + } else + // contractual && !partner with a firm and a natural person name + // should actually be split up into two persons + // but the legacy database consists such records + if (roles.contains("contractual") && !roles.contains("partner") && + !person.getFamilyName().isBlank() && !person.getGivenName().isBlank()) { + person.setPersonType(HsOfficePersonType.NATURAL_PERSON); + } else if (endsWithWord(person.getTradeName(), "e.K.", "e.G.", "eG", "GmbH", "AG", "KG")) { + person.setPersonType(HsOfficePersonType.LEGAL_PERSON); + } else if (endsWithWord(person.getTradeName(), "OHG")) { + person.setPersonType(HsOfficePersonType.INCORPORATED_FIRM); + } else if (endsWithWord(person.getTradeName(), "GbR")) { + person.setPersonType(HsOfficePersonType.INCORPORATED_FIRM); + } else { + person.setPersonType(HsOfficePersonType.UNKNOWN_PERSON_TYPE); + } + } + + private static boolean endsWithWord(final String value, final String... endings) { + final var lowerCaseValue = value.toLowerCase(); + for (String ending : endings) { + if (lowerCaseValue.endsWith(" " + ending.toLowerCase())) { + return true; + } + } + return false; + } + + private void verifyContainsOnlyKnownRoles(final String roles) { + final var allowedRolesSet = stream(KNOWN_ROLES).collect(Collectors.toSet()); + final var givenRolesSet = stream(roles.replace(" ", "").split(",")).collect(Collectors.toSet()); + final var unexpectedRolesSet = new HashSet<>(givenRolesSet); + unexpectedRolesSet.removeAll(allowedRolesSet); + assertThat(unexpectedRolesSet).isEmpty(); + } + + private HsOfficeContactRealEntity initContact(final HsOfficeContactRealEntity contact, final Record contactRecord) { + + contact.setCaption(toCaption( + contactRecord.getString("salut"), + contactRecord.getString("title"), + contactRecord.getString("first_name"), + contactRecord.getString("last_name"), + contactRecord.getString("firma"))); + contact.putEmailAddresses(Map.of("main", contactRecord.getString("email"))); + contact.setPostalAddress(toAddress(contactRecord)); + contact.putPhoneNumbers(toPhoneNumbers(contactRecord)); + + contacts.put(contactRecord.getInteger("contact_id"), contact); + return contact; + } + + private Map toPhoneNumbers(final Record rec) { + final var phoneNumbers = new LinkedHashMap(); + if (isNotBlank(rec.getString("phone_private"))) + phoneNumbers.put("phone_private", rec.getString("phone_private")); + if (isNotBlank(rec.getString("phone_office"))) + phoneNumbers.put("phone_office", rec.getString("phone_office")); + if (isNotBlank(rec.getString("phone_mobile"))) + phoneNumbers.put("phone_mobile", rec.getString("phone_mobile")); + if (isNotBlank(rec.getString("fax"))) + phoneNumbers.put("fax", rec.getString("fax")); + return phoneNumbers; + } + + private String toAddress(final Record rec) { + final var result = new StringBuilder(); + final var name = toName( + rec.getString("salut"), + rec.getString("title"), + rec.getString("first_name"), + rec.getString("last_name")); + if (isNotBlank(name)) + result.append(name + "\n"); + if (isNotBlank(rec.getString("firma"))) + result.append(rec.getString("firma") + "\n"); + if (isNotBlank(rec.getString("co"))) + result.append("c/o " + rec.getString("co") + "\n"); + if (isNotBlank(rec.getString("street"))) + result.append(rec.getString("street") + "\n"); + final var zipcodeAndCity = toZipcodeAndCity(rec); + if (isNotBlank(zipcodeAndCity)) + result.append(zipcodeAndCity + "\n"); + return result.toString(); + } + + private String toZipcodeAndCity(final Record rec) { + final var result = new StringBuilder(); + if (isNotBlank(rec.getString("country"))) + result.append(rec.getString("country") + " "); + if (isNotBlank(rec.getString("zipcode"))) + result.append(rec.getString("zipcode") + " "); + if (isNotBlank(rec.getString("city"))) + result.append(rec.getString("city")); + return result.toString(); + } + + private String toCaption( + final String salut, + final String title, + final String firstname, + final String lastname, + final String firm) { + final var result = new StringBuilder(); + if (isNotBlank(salut)) + result.append(salut + " "); + if (isNotBlank(title)) + result.append(title + " "); + if (isNotBlank(firstname)) + result.append(firstname + " "); + if (isNotBlank(lastname)) + result.append(lastname + " "); + if (isNotBlank(firm)) { + result.append((isBlank(result) ? "" : ", ") + firm); + } + return result.toString(); + } + + private String toName(final String salut, final String title, final String firstname, final String lastname) { + return toCaption(salut, title, firstname, lastname, null); + } +} diff --git a/src/test/java/net/hostsharing/hsadminng/hs/migration/ImportHostingAssets.java b/src/test/java/net/hostsharing/hsadminng/hs/migration/ImportHostingAssets.java index 83917afe..0f90aa15 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/migration/ImportHostingAssets.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/migration/ImportHostingAssets.java @@ -115,7 +115,8 @@ import static org.assertj.core.api.Assumptions.assumeThat; */ @Tag("importHostingAssets") @DataJpaTest(properties = { - "spring.datasource.url=${HSADMINNG_POSTGRES_JDBC_URL:jdbc:tc:postgresql:15.5-bookworm:///spring_boot_testcontainers}", + "spring.datasource.url=${HSADMINNG_POSTGRES_JDBC_URL:jdbc:tc:postgresql:15.5-bookworm:///importHostingAssetsTC}", +// "spring.datasource.url=jdbc:tc:postgresql:15.5-bookworm:///importHostingAssetsTC", "spring.datasource.username=${HSADMINNG_POSTGRES_ADMIN_USERNAME:ADMIN}", "spring.datasource.password=${HSADMINNG_POSTGRES_ADMIN_PASSWORD:password}", "hsadminng.superuser=${HSADMINNG_SUPERUSER:superuser-alex@hostsharing.net}" @@ -124,7 +125,7 @@ import static org.assertj.core.api.Assumptions.assumeThat; @Import({ Context.class, JpaAttempt.class }) @TestMethodOrder(MethodOrderer.OrderAnnotation.class) @ExtendWith(OrderedDependedTestsExtension.class) -public class ImportHostingAssets extends ImportOfficeData { +public class ImportHostingAssets extends BaseOfficeDataImport { private static final Set NOBODY_SUBSTITUTES = Set.of("nomail", "bounce"); @@ -242,13 +243,13 @@ public class ImportHostingAssets extends ImportOfficeData { HsBookingItemType.MANAGED_SERVER, HsBookingItemType.MANAGED_WEBSPACE)).isEqualToIgnoringWhitespace(""" { - 10630=HsBookingItemEntity(MANAGED_WEBSPACE, BI hsh00, D-1000000:hsh default project, [2001-06-01,)), - 10968=HsBookingItemEntity(MANAGED_SERVER, BI vm1061, D-1015200:rar default project, [2013-04-01,)), - 10978=HsBookingItemEntity(MANAGED_SERVER, BI vm1050, D-1000000:hsh default project, [2013-04-01,)), - 11061=HsBookingItemEntity(MANAGED_SERVER, BI vm1068, D-1000300:mim default project, [2013-08-19,)), - 11094=HsBookingItemEntity(MANAGED_WEBSPACE, BI lug00, D-1000300:mim default project, [2013-09-10,)), - 11111=HsBookingItemEntity(MANAGED_WEBSPACE, BI xyz68, D-1000000:vm1068 Monitor, [2013-08-19,)), - 23611=HsBookingItemEntity(CLOUD_SERVER, BI vm2097, D-1101800:wws default project, [2022-08-10,)) + 10630=HsBookingItem(MANAGED_WEBSPACE, BI hsh00, D-1000000:hsh default project, [2001-06-01,)), + 10968=HsBookingItem(MANAGED_SERVER, BI vm1061, D-1015200:rar default project, [2013-04-01,)), + 10978=HsBookingItem(MANAGED_SERVER, BI vm1050, D-1000000:hsh default project, [2013-04-01,)), + 11061=HsBookingItem(MANAGED_SERVER, BI vm1068, D-1000300:mim default project, [2013-08-19,)), + 11094=HsBookingItem(MANAGED_WEBSPACE, BI lug00, D-1000300:mim default project, [2013-09-10,)), + 11111=HsBookingItem(MANAGED_WEBSPACE, BI xyz68, D-1000000:vm1068 Monitor, [2013-08-19,)), + 23611=HsBookingItem(CLOUD_SERVER, BI vm2097, D-1101800:wws default project, [2022-08-10,)) } """); assertThat(firstOfEach(9, packetAssets)).isEqualToIgnoringWhitespace(""" @@ -301,16 +302,16 @@ public class ImportHostingAssets extends ImportOfficeData { HsBookingItemType.MANAGED_WEBSPACE)) .isEqualToIgnoringWhitespace(""" { - 10630=HsBookingItemEntity(MANAGED_WEBSPACE, BI hsh00, D-1000000:hsh default project, [2001-06-01,), {"HDD": 10, "Multi": 25, "SLA-Platform": "EXT24H", "SSD": 16, "Traffic": 50}), - 10968=HsBookingItemEntity(MANAGED_SERVER, BI vm1061, D-1015200:rar default project, [2013-04-01,), {"CPU": 6, "HDD": 250, "RAM": 14, "SLA-EMail": true, "SLA-Maria": true, "SLA-Office": true, "SLA-PgSQL": true, "SLA-Platform": "EXT4H", "SLA-Web": true, "SSD": 375, "Traffic": 250}), - 10978=HsBookingItemEntity(MANAGED_SERVER, BI vm1050, D-1000000:hsh default project, [2013-04-01,), {"CPU": 4, "HDD": 250, "RAM": 32, "SLA-EMail": true, "SLA-Maria": true, "SLA-Office": true, "SLA-PgSQL": true, "SLA-Platform": "EXT4H", "SLA-Web": true, "SSD": 150, "Traffic": 250}), - 11061=HsBookingItemEntity(MANAGED_SERVER, BI vm1068, D-1000300:mim default project, [2013-08-19,), {"CPU": 2, "HDD": 250, "RAM": 4, "SLA-EMail": true, "SLA-Maria": true, "SLA-Office": true, "SLA-PgSQL": true, "SLA-Platform": "EXT2H", "SLA-Web": true, "Traffic": 250}), - 11094=HsBookingItemEntity(MANAGED_WEBSPACE, BI lug00, D-1000300:mim default project, [2013-09-10,), {"Multi": 5, "SLA-Platform": "EXT24H", "SSD": 1, "Traffic": 10}), - 11111=HsBookingItemEntity(MANAGED_WEBSPACE, BI xyz68, D-1000000:vm1068 Monitor, [2013-08-19,), {"SSD": 3}), - 11112=HsBookingItemEntity(MANAGED_WEBSPACE, BI mim00, D-1000300:mim default project, [2013-09-17,), {"Multi": 5, "SLA-Platform": "EXT24H", "SSD": 3, "Traffic": 20}), - 11447=HsBookingItemEntity(MANAGED_SERVER, BI vm1093, D-1000000:hsh default project, [2014-11-28,), {"CPU": 6, "HDD": 500, "RAM": 16, "SLA-EMail": true, "SLA-Maria": true, "SLA-Office": true, "SLA-PgSQL": true, "SLA-Platform": "EXT4H", "SLA-Web": true, "SSD": 300, "Traffic": 250}), - 19959=HsBookingItemEntity(MANAGED_WEBSPACE, BI dph00, D-1101900:dph default project, [2021-06-02,), {"Multi": 1, "SLA-Platform": "EXT24H", "SSD": 25, "Traffic": 20}), - 23611=HsBookingItemEntity(CLOUD_SERVER, BI vm2097, D-1101800:wws default project, [2022-08-10,), {"CPU": 8, "RAM": 12, "SLA-Infrastructure": "EXT4H", "SSD": 25, "Traffic": 250}) + 10630=HsBookingItem(MANAGED_WEBSPACE, BI hsh00, D-1000000:hsh default project, [2001-06-01,), {"HDD": 10, "Multi": 25, "SLA-Platform": "EXT24H", "SSD": 16, "Traffic": 50}), + 10968=HsBookingItem(MANAGED_SERVER, BI vm1061, D-1015200:rar default project, [2013-04-01,), {"CPU": 6, "HDD": 250, "RAM": 14, "SLA-EMail": true, "SLA-Maria": true, "SLA-Office": true, "SLA-PgSQL": true, "SLA-Platform": "EXT4H", "SLA-Web": true, "SSD": 375, "Traffic": 250}), + 10978=HsBookingItem(MANAGED_SERVER, BI vm1050, D-1000000:hsh default project, [2013-04-01,), {"CPU": 4, "HDD": 250, "RAM": 32, "SLA-EMail": true, "SLA-Maria": true, "SLA-Office": true, "SLA-PgSQL": true, "SLA-Platform": "EXT4H", "SLA-Web": true, "SSD": 150, "Traffic": 250}), + 11061=HsBookingItem(MANAGED_SERVER, BI vm1068, D-1000300:mim default project, [2013-08-19,), {"CPU": 2, "HDD": 250, "RAM": 4, "SLA-EMail": true, "SLA-Maria": true, "SLA-Office": true, "SLA-PgSQL": true, "SLA-Platform": "EXT2H", "SLA-Web": true, "Traffic": 250}), + 11094=HsBookingItem(MANAGED_WEBSPACE, BI lug00, D-1000300:mim default project, [2013-09-10,), {"Multi": 5, "SLA-Platform": "EXT24H", "SSD": 1, "Traffic": 10}), + 11111=HsBookingItem(MANAGED_WEBSPACE, BI xyz68, D-1000000:vm1068 Monitor, [2013-08-19,), {"SSD": 3}), + 11112=HsBookingItem(MANAGED_WEBSPACE, BI mim00, D-1000300:mim default project, [2013-09-17,), {"Multi": 5, "SLA-Platform": "EXT24H", "SSD": 3, "Traffic": 20}), + 11447=HsBookingItem(MANAGED_SERVER, BI vm1093, D-1000000:hsh default project, [2014-11-28,), {"CPU": 6, "HDD": 500, "RAM": 16, "SLA-EMail": true, "SLA-Maria": true, "SLA-Office": true, "SLA-PgSQL": true, "SLA-Platform": "EXT4H", "SLA-Web": true, "SSD": 300, "Traffic": 250}), + 19959=HsBookingItem(MANAGED_WEBSPACE, BI dph00, D-1101900:dph default project, [2021-06-02,), {"Multi": 1, "SLA-Platform": "EXT24H", "SSD": 25, "Traffic": 20}), + 23611=HsBookingItem(CLOUD_SERVER, BI vm2097, D-1101800:wws default project, [2022-08-10,), {"CPU": 8, "RAM": 12, "SLA-Infrastructure": "EXT4H", "SSD": 25, "Traffic": 250}) } """); } diff --git a/src/test/java/net/hostsharing/hsadminng/hs/migration/ImportOfficeData.java b/src/test/java/net/hostsharing/hsadminng/hs/migration/ImportOfficeData.java index a33d6be4..5daf3fad 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/migration/ImportOfficeData.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/migration/ImportOfficeData.java @@ -1,46 +1,14 @@ package net.hostsharing.hsadminng.hs.migration; import net.hostsharing.hsadminng.context.Context; -import net.hostsharing.hsadminng.hs.office.bankaccount.HsOfficeBankAccountEntity; -import net.hostsharing.hsadminng.hs.office.contact.HsOfficeContactRealEntity; -import net.hostsharing.hsadminng.hs.office.coopassets.HsOfficeCoopAssetsTransactionEntity; -import net.hostsharing.hsadminng.hs.office.coopassets.HsOfficeCoopAssetsTransactionType; -import net.hostsharing.hsadminng.hs.office.coopshares.HsOfficeCoopSharesTransactionEntity; -import net.hostsharing.hsadminng.hs.office.coopshares.HsOfficeCoopSharesTransactionType; -import net.hostsharing.hsadminng.hs.office.debitor.HsOfficeDebitorEntity; -import net.hostsharing.hsadminng.hs.office.membership.HsOfficeMembershipEntity; -import net.hostsharing.hsadminng.hs.office.membership.HsOfficeMembershipStatus; -import net.hostsharing.hsadminng.hs.office.partner.HsOfficePartnerDetailsEntity; -import net.hostsharing.hsadminng.hs.office.partner.HsOfficePartnerEntity; -import net.hostsharing.hsadminng.hs.office.person.HsOfficePersonEntity; -import net.hostsharing.hsadminng.hs.office.person.HsOfficePersonType; -import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelation; -import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationRealEntity; -import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationType; -import net.hostsharing.hsadminng.hs.office.sepamandate.HsOfficeSepaMandateEntity; -import net.hostsharing.hsadminng.rbac.rbacobject.BaseEntity; import net.hostsharing.hsadminng.rbac.test.JpaAttempt; -import org.apache.commons.lang3.ArrayUtils; -import org.apache.commons.lang3.StringUtils; import org.junit.jupiter.api.*; import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; import org.springframework.context.annotation.Import; import org.springframework.test.annotation.DirtiesContext; -import java.io.*; -import java.time.LocalDate; -import java.util.*; -import java.util.stream.Collectors; - -import static java.util.Arrays.stream; -import static java.util.Optional.ofNullable; -import static net.hostsharing.hsadminng.mapper.PostgresDateRange.toPostgresDateRange; -import static org.apache.commons.lang3.StringUtils.isBlank; -import static org.apache.commons.lang3.StringUtils.isNotBlank; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assumptions.assumeThat; -import static org.assertj.core.api.Fail.fail; /* * This 'test' includes the complete legacy 'office' data import. @@ -80,7 +48,8 @@ import static org.assertj.core.api.Fail.fail; */ @Tag("importOfficeData") @DataJpaTest(properties = { - "spring.datasource.url=${HSADMINNG_POSTGRES_JDBC_URL:jdbc:tc:postgresql:15.5-bookworm:///spring_boot_testcontainers}", + "spring.datasource.url=${HSADMINNG_POSTGRES_JDBC_URL:jdbc:tc:postgresql:15.5-bookworm:///importOfficeDataTC}", +// "spring.datasource.url=jdbc:tc:postgresql:15.5-bookworm:///importOfficeDataTC", "spring.datasource.username=${HSADMINNG_POSTGRES_ADMIN_USERNAME:ADMIN}", "spring.datasource.password=${HSADMINNG_POSTGRES_ADMIN_PASSWORD:password}", "hsadminng.superuser=${HSADMINNG_SUPERUSER:superuser-alex@hostsharing.net}" @@ -89,1139 +58,10 @@ import static org.assertj.core.api.Fail.fail; @Import({ Context.class, JpaAttempt.class }) @TestMethodOrder(MethodOrderer.OrderAnnotation.class) @ExtendWith(OrderedDependedTestsExtension.class) -public class ImportOfficeData extends CsvDataImport { +public class ImportOfficeData extends BaseOfficeDataImport { - private static final String[] SUBSCRIBER_ROLES = new String[] { - "subscriber:operations-discussion", - "subscriber:operations-announce", - "subscriber:generalversammlung", - "subscriber:members-announce", - "subscriber:members-discussion", - "subscriber:customers-announce" - }; - private static final String[] KNOWN_ROLES = ArrayUtils.addAll( - new String[]{"partner", "vip-contact", "ex-partner", "billing", "contractual", "operation"}, - SUBSCRIBER_ROLES); - - // at least as the number of lines in business_partners.csv from test-data, but less than real data partner count - public static final int MAX_NUMBER_OF_TEST_DATA_PARTNERS = 100; - public static final int DELIBERATELY_BROKEN_BUSINESS_PARTNER_ID = 199; - - static int relationId = 2000000; - - private static final List IGNORE_BUSINESS_PARTNERS = Arrays.asList( - 512167, // 11139, partner without contractual contact - 512170, // 11142, partner without contractual contact - 511725, // 10764, partner without contractual contact - // 512171, // 11143, partner without partner contact -- exc - -1 - ); - - private static final List IGNORE_CONTACTS = Arrays.asList( - 90547, // Kontakt hat keine Rolle - -1 - ); - - static Map contacts = new WriteOnceMap<>(); - static Map persons = new WriteOnceMap<>(); - static Map partners = new WriteOnceMap<>(); - static Map debitors = new WriteOnceMap<>(); - static Map memberships = new WriteOnceMap<>(); - - static Map relations = new WriteOnceMap<>(); - static Map sepaMandates = new WriteOnceMap<>(); - static Map bankAccounts = new WriteOnceMap<>(); - static Map coopShares = new WriteOnceMap<>(); - static Map coopAssets = new WriteOnceMap<>(); - - @Test - @Order(1) - void verifyInitialDatabase() { - // SQL DELETE for thousands of records takes too long, so we make sure, we only start with initial or test data - final var contactCount = (Integer) em.createNativeQuery("select count(*) from hs_office_contact", Integer.class) - .getSingleResult(); - assertThat(contactCount).isLessThan(20); - } - - @Test - @Order(1010) - void importBusinessPartners() { - - try (Reader reader = resourceReader(MIGRATION_DATA_PATH + "/office/business_partners.csv")) { - final var lines = readAllLines(reader); - importBusinessPartners(justHeader(lines), withoutHeader(lines)); - } catch (Exception e) { - throw new RuntimeException(e); - } - } - - @Test - @Order(1019) - void verifyBusinessPartners() { - assumeThatWeAreImportingControlledTestData(); - - // no contacts yet => mostly null values - assertThat(toJsonFormattedString(partners)).isEqualToIgnoringWhitespace(""" - { - 100=partner(P-10003: null null, null), - 120=partner(P-10020: null null, null), - 122=partner(P-11022: null null, null), - 132=partner(P-10152: null null, null), - 190=partner(P-19090: null null, null), - 199=partner(P-19999: null null, null), - 213=partner(P-10000: null null, null), - 541=partner(P-11018: null null, null), - 542=partner(P-11019: null null, null) - } - """); - assertThat(toJsonFormattedString(contacts)).isEqualTo("{}"); - assertThat(toJsonFormattedString(debitors)).isEqualToIgnoringWhitespace(""" - { - 100=debitor(D-1000300: rel(anchor='null null, null', type='DEBITOR'), mim), - 120=debitor(D-1002000: rel(anchor='null null, null', type='DEBITOR'), xyz), - 122=debitor(D-1102200: rel(anchor='null null, null', type='DEBITOR'), xxx), - 132=debitor(D-1015200: rel(anchor='null null, null', type='DEBITOR'), rar), - 190=debitor(D-1909000: rel(anchor='null null, null', type='DEBITOR'), yyy), - 199=debitor(D-1999900: rel(anchor='null null, null', type='DEBITOR'), zzz), - 213=debitor(D-1000000: rel(anchor='null null, null', type='DEBITOR'), hsh), - 541=debitor(D-1101800: rel(anchor='null null, null', type='DEBITOR'), wws), - 542=debitor(D-1101900: rel(anchor='null null, null', type='DEBITOR'), dph) - } - """); - assertThat(toJsonFormattedString(memberships)).isEqualToIgnoringWhitespace(""" - { - 100=Membership(M-1000300, P-10003, [2000-12-06,), ACTIVE), - 120=Membership(M-1002000, P-10020, [2000-12-06,2016-01-01), UNKNOWN), - 122=Membership(M-1102200, P-11022, [2021-04-01,), ACTIVE), - 132=Membership(M-1015200, P-10152, [2003-07-12,), ACTIVE), - 541=Membership(M-1101800, P-11018, [2021-05-17,), ACTIVE), - 542=Membership(M-1101900, P-11019, [2021-05-25,), ACTIVE) - } - """); - } - - @Test - @Order(1020) - void importContacts() { - try (Reader reader = resourceReader(MIGRATION_DATA_PATH + "/office/contacts.csv")) { - final var lines = readAllLines(reader); - importContacts(justHeader(lines), withoutHeader(lines)); - } catch (Exception e) { - throw new RuntimeException(e); - } - } - - @Test - @Order(1029) - void verifyContacts() { - assumeThatWeAreImportingControlledTestData(); - - assertThat(toJsonFormattedString(partners)).isEqualToIgnoringWhitespace(""" - { - 100=partner(P-10003: ?? Michael Mellis, Herr Michael Mellis , Michael Mellis), - 120=partner(P-10020: LP JM GmbH, Herr Philip Meyer-Contract , JM GmbH), - 122=partner(P-11022: ?? Test PS, Petra Schmidt , Test PS), - 132=partner(P-10152: ?? Ragnar IT-Beratung, Herr Ragnar Richter , Ragnar IT-Beratung), - 190=partner(P-19090: NP Camus, Cecilia, Frau Cecilia Camus ), - 199=partner(P-19999: null null, null), - 213=partner(P-10000: LP Hostsharing e.G., Firma Hostmaster Hostsharing , Hostsharing e.G.), - 541=partner(P-11018: ?? Wasserwerk Südholstein, Frau Christiane Milberg , Wasserwerk Südholstein), - 542=partner(P-11019: ?? Das Perfekte Haus, Herr Richard Wiese , Das Perfekte Haus) - } - """); - assertThat(toJsonFormattedString(contacts)).isEqualToIgnoringWhitespace(""" - { - 100=contact(caption='Herr Michael Mellis , Michael Mellis', emailAddresses='{ "main": "michael@Mellis.example.org"}'), - 1200=contact(caption='JM e.K.', emailAddresses='{ "main": "jm-ex-partner@example.org"}'), - 1201=contact(caption='Frau Dr. Jenny Meyer-Billing , JM GmbH', emailAddresses='{ "main": "jm-billing@example.org"}'), - 1202=contact(caption='Herr Andrew Meyer-Operation , JM GmbH', emailAddresses='{ "main": "am-operation@example.org"}'), - 1203=contact(caption='Herr Philip Meyer-Contract , JM GmbH', emailAddresses='{ "main": "pm-partner@example.org"}'), - 1204=contact(caption='Frau Tammy Meyer-VIP , JM GmbH', emailAddresses='{ "main": "tm-vip@example.org"}'), - 1301=contact(caption='Petra Schmidt , Test PS', emailAddresses='{ "main": "ps@example.com"}'), - 132=contact(caption='Herr Ragnar Richter , Ragnar IT-Beratung', emailAddresses='{ "main": "hostsharing@ragnar-richter.de"}'), - 1401=contact(caption='Frau Frauke Fanninga ', emailAddresses='{ "main": "ff@example.org"}'), - 1501=contact(caption='Frau Cecilia Camus ', emailAddresses='{ "main": "cc@example.org"}'), - 212=contact(caption='Firma Hostmaster Hostsharing , Hostsharing e.G.', emailAddresses='{ "main": "hostmaster@hostsharing.net"}'), - 90436=contact(caption='Frau Christiane Milberg , Wasserwerk Südholstein', emailAddresses='{ "main": "rechnung@ww-sholst.example.org"}'), - 90437=contact(caption='Herr Richard Wiese , Das Perfekte Haus', emailAddresses='{ "main": "admin@das-perfekte-haus.example.org"}'), - 90438=contact(caption='Herr Karim Metzger , Wasswerwerk Südholstein', emailAddresses='{ "main": "karim.metzger@ww-sholst.example.org"}'), - 90590=contact(caption='Herr Inhaber R. Wiese , Das Perfekte Haus', emailAddresses='{ "main": "515217@kkemail.example.org"}'), - 90629=contact(caption='Ragnar Richter ', emailAddresses='{ "main": "mail@ragnar-richter..example.org"}'), - 90677=contact(caption='Eike Henning ', emailAddresses='{ "main": "hostsharing@eike-henning..example.org"}'), - 90698=contact(caption='Jan Henning ', emailAddresses='{ "main": "mail@jan-henning.example.org"}') - } - """); - assertThat(toJsonFormattedString(persons)).isEqualToIgnoringWhitespace(""" - { - 100=person(personType='??', tradeName='Michael Mellis', familyName='Mellis', givenName='Michael'), - 1200=person(personType='LP', tradeName='JM e.K.'), - 1201=person(personType='LP', tradeName='JM GmbH', familyName='Meyer-Billing', givenName='Jenny'), - 1202=person(personType='LP', tradeName='JM GmbH', familyName='Meyer-Operation', givenName='Andrew'), - 1203=person(personType='LP', tradeName='JM GmbH', familyName='Meyer-Contract', givenName='Philip'), - 1204=person(personType='LP', tradeName='JM GmbH', familyName='Meyer-VIP', givenName='Tammy'), - 1301=person(personType='??', tradeName='Test PS', familyName='Schmidt', givenName='Petra'), - 132=person(personType='??', tradeName='Ragnar IT-Beratung', familyName='Richter', givenName='Ragnar'), - 1401=person(personType='NP', familyName='Fanninga', givenName='Frauke'), - 1501=person(personType='NP', familyName='Camus', givenName='Cecilia'), - 212=person(personType='LP', tradeName='Hostsharing e.G.', familyName='Hostsharing', givenName='Hostmaster'), - 90436=person(personType='??', tradeName='Wasserwerk Südholstein', familyName='Milberg', givenName='Christiane'), - 90437=person(personType='??', tradeName='Das Perfekte Haus', familyName='Wiese', givenName='Richard'), - 90438=person(personType='??', tradeName='Wasswerwerk Südholstein', familyName='Metzger', givenName='Karim'), - 90590=person(personType='??', tradeName='Das Perfekte Haus', familyName='Wiese', givenName='Inhaber R.'), - 90629=person(personType='NP', familyName='Richter', givenName='Ragnar'), - 90677=person(personType='NP', familyName='Henning', givenName='Eike'), - 90698=person(personType='NP', familyName='Henning', givenName='Jan') - } - """); - assertThat(toJsonFormattedString(debitors)).isEqualToIgnoringWhitespace(""" - { - 100=debitor(D-1000300: rel(anchor='?? Michael Mellis', type='DEBITOR', holder='?? Michael Mellis'), mim), - 120=debitor(D-1002000: rel(anchor='LP JM GmbH', type='DEBITOR', holder='LP JM GmbH'), xyz), - 122=debitor(D-1102200: rel(anchor='?? Test PS', type='DEBITOR', holder='?? Test PS'), xxx), - 132=debitor(D-1015200: rel(anchor='?? Ragnar IT-Beratung', type='DEBITOR', holder='?? Ragnar IT-Beratung'), rar), - 190=debitor(D-1909000: rel(anchor='NP Camus, Cecilia', type='DEBITOR', holder='NP Camus, Cecilia'), yyy), - 199=debitor(D-1999900: rel(anchor='null null, null', type='DEBITOR'), zzz), - 213=debitor(D-1000000: rel(anchor='LP Hostsharing e.G.', type='DEBITOR', holder='LP Hostsharing e.G.'), hsh), - 541=debitor(D-1101800: rel(anchor='?? Wasserwerk Südholstein', type='DEBITOR', holder='?? Wasserwerk Südholstein'), wws), - 542=debitor(D-1101900: rel(anchor='?? Das Perfekte Haus', type='DEBITOR', holder='?? Das Perfekte Haus'), dph) - } - """); - assertThat(toJsonFormattedString(memberships)).isEqualToIgnoringWhitespace(""" - { - 100=Membership(M-1000300, P-10003, [2000-12-06,), ACTIVE), - 120=Membership(M-1002000, P-10020, [2000-12-06,2016-01-01), UNKNOWN), - 122=Membership(M-1102200, P-11022, [2021-04-01,), ACTIVE), - 132=Membership(M-1015200, P-10152, [2003-07-12,), ACTIVE), - 541=Membership(M-1101800, P-11018, [2021-05-17,), ACTIVE), - 542=Membership(M-1101900, P-11019, [2021-05-25,), ACTIVE) - } - """); - assertThat(toJsonFormattedString(relations)).isEqualToIgnoringWhitespace(""" - { - 2000000=rel(anchor='LP Hostsharing e.G.', type='PARTNER', holder='?? Michael Mellis', contact='Herr Michael Mellis , Michael Mellis'), - 2000001=rel(anchor='?? Michael Mellis', type='DEBITOR', holder='?? Michael Mellis', contact='Herr Michael Mellis , Michael Mellis'), - 2000002=rel(anchor='LP Hostsharing e.G.', type='PARTNER', holder='?? Ragnar IT-Beratung', contact='Herr Ragnar Richter , Ragnar IT-Beratung'), - 2000003=rel(anchor='?? Ragnar IT-Beratung', type='DEBITOR', holder='?? Ragnar IT-Beratung', contact='Herr Ragnar Richter , Ragnar IT-Beratung'), - 2000004=rel(anchor='LP Hostsharing e.G.', type='PARTNER', holder='LP Hostsharing e.G.', contact='Firma Hostmaster Hostsharing , Hostsharing e.G.'), - 2000005=rel(anchor='LP Hostsharing e.G.', type='DEBITOR', holder='LP Hostsharing e.G.', contact='Firma Hostmaster Hostsharing , Hostsharing e.G.'), - 2000006=rel(anchor='LP Hostsharing e.G.', type='PARTNER', holder='?? Wasserwerk Südholstein', contact='Frau Christiane Milberg , Wasserwerk Südholstein'), - 2000007=rel(anchor='?? Wasserwerk Südholstein', type='DEBITOR', holder='?? Wasserwerk Südholstein', contact='Frau Christiane Milberg , Wasserwerk Südholstein'), - 2000008=rel(anchor='LP Hostsharing e.G.', type='PARTNER', holder='?? Das Perfekte Haus', contact='Herr Richard Wiese , Das Perfekte Haus'), - 2000009=rel(anchor='?? Das Perfekte Haus', type='DEBITOR', holder='?? Das Perfekte Haus', contact='Herr Inhaber R. Wiese , Das Perfekte Haus'), - 2000010=rel(anchor='LP Hostsharing e.G.', type='PARTNER', holder='LP JM GmbH', contact='Herr Philip Meyer-Contract , JM GmbH'), - 2000011=rel(anchor='LP JM GmbH', type='DEBITOR', holder='LP JM GmbH', contact='Frau Dr. Jenny Meyer-Billing , JM GmbH'), - 2000012=rel(anchor='LP Hostsharing e.G.', type='PARTNER', holder='?? Test PS', contact='Petra Schmidt , Test PS'), - 2000013=rel(anchor='?? Test PS', type='DEBITOR', holder='?? Test PS', contact='Petra Schmidt , Test PS'), - 2000014=rel(anchor='LP Hostsharing e.G.', type='PARTNER', holder='NP Camus, Cecilia', contact='Frau Cecilia Camus '), - 2000015=rel(anchor='NP Camus, Cecilia', type='DEBITOR', holder='NP Camus, Cecilia', contact='Frau Cecilia Camus '), - 2000016=rel(anchor='LP Hostsharing e.G.', type='PARTNER', holder='null null, null'), - 2000017=rel(anchor='null null, null', type='DEBITOR'), - 2000018=rel(anchor='LP Hostsharing e.G.', type='OPERATIONS', holder='LP Hostsharing e.G.', contact='Firma Hostmaster Hostsharing , Hostsharing e.G.'), - 2000019=rel(anchor='LP Hostsharing e.G.', type='REPRESENTATIVE', holder='LP Hostsharing e.G.', contact='Firma Hostmaster Hostsharing , Hostsharing e.G.'), - 2000020=rel(anchor='?? Michael Mellis', type='OPERATIONS', holder='?? Michael Mellis', contact='Herr Michael Mellis , Michael Mellis'), - 2000021=rel(anchor='?? Michael Mellis', type='REPRESENTATIVE', holder='?? Michael Mellis', contact='Herr Michael Mellis , Michael Mellis'), - 2000022=rel(anchor='?? Michael Mellis', type='SUBSCRIBER', mark='operations-discussion', holder='?? Michael Mellis', contact='Herr Michael Mellis , Michael Mellis'), - 2000023=rel(anchor='?? Michael Mellis', type='SUBSCRIBER', mark='operations-announce', holder='?? Michael Mellis', contact='Herr Michael Mellis , Michael Mellis'), - 2000024=rel(anchor='?? Michael Mellis', type='SUBSCRIBER', mark='generalversammlung', holder='?? Michael Mellis', contact='Herr Michael Mellis , Michael Mellis'), - 2000025=rel(anchor='?? Michael Mellis', type='SUBSCRIBER', mark='members-announce', holder='?? Michael Mellis', contact='Herr Michael Mellis , Michael Mellis'), - 2000026=rel(anchor='?? Michael Mellis', type='SUBSCRIBER', mark='members-discussion', holder='?? Michael Mellis', contact='Herr Michael Mellis , Michael Mellis'), - 2000027=rel(anchor='?? Ragnar IT-Beratung', type='OPERATIONS', holder='?? Ragnar IT-Beratung', contact='Herr Ragnar Richter , Ragnar IT-Beratung'), - 2000028=rel(anchor='?? Ragnar IT-Beratung', type='SUBSCRIBER', mark='operations-discussion', holder='?? Ragnar IT-Beratung', contact='Herr Ragnar Richter , Ragnar IT-Beratung'), - 2000029=rel(anchor='?? Ragnar IT-Beratung', type='SUBSCRIBER', mark='operations-announce', holder='?? Ragnar IT-Beratung', contact='Herr Ragnar Richter , Ragnar IT-Beratung'), - 2000030=rel(anchor='LP JM GmbH', type='EX_PARTNER', holder='LP JM e.K.', contact='JM e.K.'), - 2000031=rel(anchor='LP JM GmbH', type='OPERATIONS', holder='LP JM GmbH', contact='Herr Andrew Meyer-Operation , JM GmbH'), - 2000032=rel(anchor='LP JM GmbH', type='VIP_CONTACT', holder='LP JM GmbH', contact='Herr Andrew Meyer-Operation , JM GmbH'), - 2000033=rel(anchor='LP JM GmbH', type='SUBSCRIBER', mark='operations-announce', holder='LP JM GmbH', contact='Herr Andrew Meyer-Operation , JM GmbH'), - 2000034=rel(anchor='LP JM GmbH', type='REPRESENTATIVE', holder='LP JM GmbH', contact='Herr Philip Meyer-Contract , JM GmbH'), - 2000035=rel(anchor='LP JM GmbH', type='SUBSCRIBER', mark='members-announce', holder='LP JM GmbH', contact='Herr Philip Meyer-Contract , JM GmbH'), - 2000036=rel(anchor='LP JM GmbH', type='SUBSCRIBER', mark='customers-announce', holder='LP JM GmbH', contact='Herr Philip Meyer-Contract , JM GmbH'), - 2000037=rel(anchor='LP JM GmbH', type='VIP_CONTACT', holder='LP JM GmbH', contact='Frau Tammy Meyer-VIP , JM GmbH'), - 2000038=rel(anchor='?? Test PS', type='OPERATIONS', holder='?? Test PS', contact='Petra Schmidt , Test PS'), - 2000039=rel(anchor='?? Test PS', type='REPRESENTATIVE', holder='?? Test PS', contact='Petra Schmidt , Test PS'), - 2000040=rel(anchor='LP JM GmbH', type='SUBSCRIBER', mark='operations-announce', holder='NP Fanninga, Frauke', contact='Frau Frauke Fanninga '), - 2000041=rel(anchor='NP Camus, Cecilia', type='OPERATIONS', holder='NP Camus, Cecilia', contact='Frau Cecilia Camus '), - 2000042=rel(anchor='NP Camus, Cecilia', type='REPRESENTATIVE', holder='NP Camus, Cecilia', contact='Frau Cecilia Camus '), - 2000043=rel(anchor='?? Wasserwerk Südholstein', type='REPRESENTATIVE', holder='?? Wasserwerk Südholstein', contact='Frau Christiane Milberg , Wasserwerk Südholstein'), - 2000044=rel(anchor='?? Wasserwerk Südholstein', type='SUBSCRIBER', mark='generalversammlung', holder='?? Wasserwerk Südholstein', contact='Frau Christiane Milberg , Wasserwerk Südholstein'), - 2000045=rel(anchor='?? Wasserwerk Südholstein', type='SUBSCRIBER', mark='members-announce', holder='?? Wasserwerk Südholstein', contact='Frau Christiane Milberg , Wasserwerk Südholstein'), - 2000046=rel(anchor='?? Wasserwerk Südholstein', type='SUBSCRIBER', mark='members-discussion', holder='?? Wasserwerk Südholstein', contact='Frau Christiane Milberg , Wasserwerk Südholstein'), - 2000047=rel(anchor='?? Das Perfekte Haus', type='OPERATIONS', holder='?? Das Perfekte Haus', contact='Herr Richard Wiese , Das Perfekte Haus'), - 2000048=rel(anchor='?? Das Perfekte Haus', type='REPRESENTATIVE', holder='?? Das Perfekte Haus', contact='Herr Richard Wiese , Das Perfekte Haus'), - 2000049=rel(anchor='?? Das Perfekte Haus', type='SUBSCRIBER', mark='operations-discussion', holder='?? Das Perfekte Haus', contact='Herr Richard Wiese , Das Perfekte Haus'), - 2000050=rel(anchor='?? Das Perfekte Haus', type='SUBSCRIBER', mark='operations-announce', holder='?? Das Perfekte Haus', contact='Herr Richard Wiese , Das Perfekte Haus'), - 2000051=rel(anchor='?? Das Perfekte Haus', type='SUBSCRIBER', mark='generalversammlung', holder='?? Das Perfekte Haus', contact='Herr Richard Wiese , Das Perfekte Haus'), - 2000052=rel(anchor='?? Das Perfekte Haus', type='SUBSCRIBER', mark='members-announce', holder='?? Das Perfekte Haus', contact='Herr Richard Wiese , Das Perfekte Haus'), - 2000053=rel(anchor='?? Das Perfekte Haus', type='SUBSCRIBER', mark='members-discussion', holder='?? Das Perfekte Haus', contact='Herr Richard Wiese , Das Perfekte Haus'), - 2000054=rel(anchor='?? Wasserwerk Südholstein', type='OPERATIONS', holder='?? Wasswerwerk Südholstein', contact='Herr Karim Metzger , Wasswerwerk Südholstein'), - 2000055=rel(anchor='?? Wasserwerk Südholstein', type='SUBSCRIBER', mark='operations-discussion', holder='?? Wasswerwerk Südholstein', contact='Herr Karim Metzger , Wasswerwerk Südholstein'), - 2000056=rel(anchor='?? Wasserwerk Südholstein', type='SUBSCRIBER', mark='operations-announce', holder='?? Wasswerwerk Südholstein', contact='Herr Karim Metzger , Wasswerwerk Südholstein'), - 2000057=rel(anchor='?? Ragnar IT-Beratung', type='REPRESENTATIVE', holder='NP Richter, Ragnar', contact='Ragnar Richter '), - 2000058=rel(anchor='?? Ragnar IT-Beratung', type='SUBSCRIBER', mark='generalversammlung', holder='NP Richter, Ragnar', contact='Ragnar Richter '), - 2000059=rel(anchor='?? Ragnar IT-Beratung', type='SUBSCRIBER', mark='members-announce', holder='NP Richter, Ragnar', contact='Ragnar Richter '), - 2000060=rel(anchor='?? Ragnar IT-Beratung', type='SUBSCRIBER', mark='members-discussion', holder='NP Richter, Ragnar', contact='Ragnar Richter '), - 2000061=rel(anchor='?? Ragnar IT-Beratung', type='OPERATIONS', holder='NP Henning, Eike', contact='Eike Henning '), - 2000062=rel(anchor='?? Ragnar IT-Beratung', type='SUBSCRIBER', mark='operations-discussion', holder='NP Henning, Eike', contact='Eike Henning '), - 2000063=rel(anchor='?? Ragnar IT-Beratung', type='SUBSCRIBER', mark='operations-announce', holder='NP Henning, Eike', contact='Eike Henning '), - 2000064=rel(anchor='?? Ragnar IT-Beratung', type='OPERATIONS', holder='NP Henning, Jan', contact='Jan Henning ') - } - """); - } - - @Test - @Order(1030) - void importSepaMandates() { - try (Reader reader = resourceReader(MIGRATION_DATA_PATH + "/office/sepa_mandates.csv")) { - final var lines = readAllLines(reader); - importSepaMandates(justHeader(lines), withoutHeader(lines)); - } catch (Exception e) { - throw new RuntimeException(e); - } - } - - @Test - @Order(1039) - void verifySepaMandates() { - assumeThatWeAreImportingControlledTestData(); - - assertThat(toJsonFormattedString(bankAccounts)).isEqualToIgnoringWhitespace(""" - { - 132=bankAccount(DE37500105177419788228: holder='Michael Mellis', bic='GENODEF1HH2'), - 234234=bankAccount(DE37500105177419788228: holder='Michael Mellis', bic='INGDDEFFXXX'), - 235600=bankAccount(DE02300209000106531065: holder='JM e.K.', bic='CMCIDEDD'), - 235662=bankAccount(DE49500105174516484892: holder='JM GmbH', bic='INGDDEFFXXX'), - 30=bankAccount(DE02300209000106531065: holder='Ragnar Richter', bic='GENODEM1GLS'), - 386=bankAccount(DE49500105174516484892: holder='Wasserwerk Suedholstein', bic='NOLADE21WHO'), - 387=bankAccount(DE89370400440532013000: holder='Richard Wiese Das Perfekte Haus', bic='COBADEFFXXX') - } - """); - assertThat(toJsonFormattedString(sepaMandates)).isEqualToIgnoringWhitespace(""" - { - 132=SEPA-Mandate(DE37500105177419788228, HS-10003-20140801, 2013-12-01, [2013-12-01,)), - 234234=SEPA-Mandate(DE37500105177419788228, MH12345, 2004-06-12, [2004-06-15,)), - 235600=SEPA-Mandate(DE02300209000106531065, JM33344, 2004-01-15, [2004-01-20,2005-06-28)), - 235662=SEPA-Mandate(DE49500105174516484892, JM33344, 2005-06-28, [2005-07-01,)), - 30=SEPA-Mandate(DE02300209000106531065, HS-10152-20140801, 2013-12-01, [2013-12-01,2016-02-16)), - 386=SEPA-Mandate(DE49500105174516484892, HS-11018-20210512, 2021-05-12, [2021-05-17,)), - 387=SEPA-Mandate(DE89370400440532013000, HS-11019-20210519, 2021-05-19, [2021-05-25,)) - } - """); - } - - @Test - @Order(1040) - void importCoopShares() { - try (Reader reader = resourceReader(MIGRATION_DATA_PATH + "/office/share_transactions.csv")) { - final var lines = readAllLines(reader); - importCoopShares(justHeader(lines), withoutHeader(lines)); - } catch (Exception e) { - throw new RuntimeException(e); - } - } - - @Test - @Order(1041) - void verifyCoopShares() { - assumeThatWeAreImportingControlledTestData(); - - assertThat(toJsonFormattedString(coopShares)).isEqualToIgnoringWhitespace(""" - { - 241=CoopShareTransaction(M-1000300: 2011-12-05, SUBSCRIPTION, 16, 1000300), - 279=CoopShareTransaction(M-1015200: 2013-10-21, SUBSCRIPTION, 1, 1015200), - 33451=CoopShareTransaction(M-1002000: 2000-12-06, SUBSCRIPTION, 2, 1002000, initial share subscription), - 33701=CoopShareTransaction(M-1000300: 2005-01-10, SUBSCRIPTION, 40, 1000300, increase), - 33810=CoopShareTransaction(M-1002000: 2016-12-31, CANCELLATION, 22, 1002000, membership ended), - 3=CoopShareTransaction(M-1000300: 2000-12-06, SUBSCRIPTION, 80, 1000300, initial share subscription), - 523=CoopShareTransaction(M-1000300: 2020-12-08, SUBSCRIPTION, 96, 1000300, Kapitalerhoehung), - 562=CoopShareTransaction(M-1101800: 2021-05-17, SUBSCRIPTION, 4, 1101800, Beitritt), - 563=CoopShareTransaction(M-1101900: 2021-05-25, SUBSCRIPTION, 1, 1101900, Beitritt), - 721=CoopShareTransaction(M-1000300: 2023-10-10, SUBSCRIPTION, 96, 1000300, Kapitalerhoehung), - 90=CoopShareTransaction(M-1015200: 2003-07-12, SUBSCRIPTION, 1, 1015200) - } - """); - } - - @Test - @Order(1050) - void importCoopAssets() { - try (Reader reader = resourceReader(MIGRATION_DATA_PATH + "/office/asset_transactions.csv")) { - final var lines = readAllLines(reader); - importCoopAssets(justHeader(lines), withoutHeader(lines)); - } catch (Exception e) { - throw new RuntimeException(e); - } - } - - @Test - @Order(1059) - void verifyCoopAssets() { - assumeThatWeAreImportingControlledTestData(); - - assertThat(toJsonFormattedString(coopAssets)).isEqualToIgnoringWhitespace(""" - { - 1093=CoopAssetsTransaction(M-1000300: 2023-10-05, DEPOSIT, 3072, 1000300, Kapitalerhoehung - Ueberweisung), - 1094=CoopAssetsTransaction(M-1000300: 2023-10-06, DEPOSIT, 3072, 1000300, Kapitalerhoehung - Ueberweisung), - 31000=CoopAssetsTransaction(M-1002000: 2000-12-06, DEPOSIT, 128.00, 1002000, for subscription B), - 32000=CoopAssetsTransaction(M-1000300: 2005-01-10, DEPOSIT, 2560.00, 1000300, for subscription C), - 33001=CoopAssetsTransaction(M-1000300: 2005-01-10, TRANSFER, -512.00, 1000300, for transfer to 10), - 33002=CoopAssetsTransaction(M-1002000: 2005-01-10, ADOPTION, 512.00, 1002000, for transfer from 7), - 34001=CoopAssetsTransaction(M-1002000: 2016-12-31, CLEARING, -8.00, 1002000, for cancellation D), - 34002=CoopAssetsTransaction(M-1002000: 2016-12-31, DISBURSAL, -100.00, 1002000, for cancellation D), - 34003=CoopAssetsTransaction(M-1002000: 2016-12-31, LOSS, -20.00, 1002000, for cancellation D), - 35001=CoopAssetsTransaction(M-1909000: 2024-01-15, DEPOSIT, 128.00, 1909000, for subscription E), - 35002=CoopAssetsTransaction(M-1909000: 2024-01-20, ADJUSTMENT, -128.00, 1909000, chargeback for subscription E, M-1909000:DEP:+128.00), - 358=CoopAssetsTransaction(M-1000300: 2000-12-06, DEPOSIT, 5120, 1000300, for subscription A), - 442=CoopAssetsTransaction(M-1015200: 2003-07-07, DEPOSIT, 64, 1015200), - 577=CoopAssetsTransaction(M-1000300: 2011-12-12, DEPOSIT, 1024, 1000300), - 632=CoopAssetsTransaction(M-1015200: 2013-10-21, DEPOSIT, 64, 1015200), - 885=CoopAssetsTransaction(M-1000300: 2020-12-15, DEPOSIT, 6144, 1000300, Einzahlung), - 924=CoopAssetsTransaction(M-1101800: 2021-05-21, DEPOSIT, 256, 1101800, Beitritt - Lastschrift), - 925=CoopAssetsTransaction(M-1101900: 2021-05-31, DEPOSIT, 64, 1101900, Beitritt - Lastschrift) - } - """); - } - - @Test - @Order(1099) - void verifyMemberships() { - assumeThatWeAreImportingControlledTestData(); - - assertThat(toJsonFormattedString(memberships)).isEqualToIgnoringWhitespace(""" - { - 100=Membership(M-1000300, P-10003, [2000-12-06,), ACTIVE), - 120=Membership(M-1002000, P-10020, [2000-12-06,2016-01-01), UNKNOWN), - 122=Membership(M-1102200, P-11022, [2021-04-01,), ACTIVE), - 132=Membership(M-1015200, P-10152, [2003-07-12,), ACTIVE), - 190=Membership(M-1909000, P-19090, empty, INVALID), - 541=Membership(M-1101800, P-11018, [2021-05-17,), ACTIVE), - 542=Membership(M-1101900, P-11019, [2021-05-25,), ACTIVE) - } - """); - } - - @Test - @Order(2000) - void verifyAllPartnersHavePersons() { - partners.forEach((id, p) -> { - final var partnerRel = p.getPartnerRel(); - assertThat(partnerRel).describedAs("partner " + id + " without partnerRel").isNotNull(); - if (id != DELIBERATELY_BROKEN_BUSINESS_PARTNER_ID) { - logError( () -> { - assertThat(partnerRel.getContact()).describedAs("partner " + id + " without partnerRel.contact").isNotNull(); - assertThat(partnerRel.getContact().getCaption()).describedAs("partner " + id + " without valid partnerRel.contact").isNotNull(); - }); - logError( () -> { - assertThat(partnerRel.getHolder()).describedAs("partner " + id + " without partnerRel.relHolder").isNotNull(); - assertThat(partnerRel.getHolder().getPersonType()).describedAs("partner " + id + " without valid partnerRel.relHolder").isNotNull(); - }); - } - }); - } - - @Test - @Order(3001) - void removeSelfRepresentativeRelations() { - - // this happens if a natural person is marked as 'contractual' for itself - final var idsToRemove = new HashSet(); - relations.forEach( (id, r) -> { - if (r.getHolder() == r.getAnchor() ) { - idsToRemove.add(id); - } - }); - - // remove self-representatives - idsToRemove.forEach(id -> { - System.out.println("removing self representative relation: " + relations.get(id).toString()); - relations.remove(id); - }); - } - - @Test - @Order(3002) - void removeEmptyRelations() { - - // avoid a error when persisting the deliberately invalid partner entry #99 - final var idsToRemove = new HashSet(); - relations.forEach( (id, r) -> { - if (r.getContact() == null || r.getContact().getCaption() == null || - r.getHolder() == null || r.getHolder().getPersonType() == null ) { - idsToRemove.add(id); - } - }); - - // expected relations created from partner #99 + Hostsharing eG itself - idsToRemove.forEach(id -> { - System.out.println("removing unused relation: " + relations.get(id).toString()); - relations.remove(id); - }); - } - - @Test - @Order(3003) - void removeEmptyPartners() { - - // avoid a error when persisting the deliberately invalid partner entry #99 - final var idsToRemove = new HashSet(); - partners.forEach( (id, r) -> { - final var partnerRole = r.getPartnerRel(); - - // such a record is in test data to test error messages - if (partnerRole.getContact() == null || partnerRole.getContact().getCaption() == null || - partnerRole.getHolder() == null | partnerRole.getHolder().getPersonType() == null ) { - idsToRemove.add(id); - } - }); - - // expected partners created from partner #99 + Hostsharing eG itself - idsToRemove.forEach(id -> { - System.out.println("removing unused partner: " + partners.get(id).toString()); - partners.remove(id); - }); - } - - @Test - @Order(3004) - void removeEmptyDebitors() { - - // avoid a error when persisting the deliberately invalid partner entry #99 - final var idsToRemove = new HashSet(); - debitors.forEach( (id, d) -> { - final var debitorRel = d.getDebitorRel(); - if (debitorRel.getContact() == null || debitorRel.getContact().getCaption() == null || - debitorRel.getAnchor() == null || debitorRel.getAnchor().getPersonType() == null || - debitorRel.getHolder() == null || debitorRel.getHolder().getPersonType() == null ) { - idsToRemove.add(id); - } - }); - idsToRemove.forEach(id -> debitors.remove(id)); - - assumeThatWeAreImportingControlledTestData(); - assertThat(idsToRemove.size()).isEqualTo(1); // only from partner #99 - } - - @Test - @Order(3005) - void removeEmptyPersons() { - // avoid a error when persisting the deliberately invalid partner entry #99 - final var idsToRemove = new HashSet(); - persons.forEach( (id, p) -> { - if ( p.getPersonType() == null || - (p.getFamilyName() == null && p.getGivenName() == null && p.getTradeName() == null) ) { - idsToRemove.add(id); - } - }); - idsToRemove.forEach(id -> persons.remove(id)); - - assumeThatWeAreImportingControlledTestData(); - assertThat(idsToRemove.size()).isEqualTo(0); - } - - @Test - @Order(9000) - @ContinueOnFailure - void logCollectedErrorsBeforePersist() { - assertNoErrors(); - } - - @Test - @Order(9010) - void persistOfficeEntities() { - - System.out.println("PERSISTING office data to database '" + jdbcUrl + "' as user '" + postgresAdminUser + "'"); - deleteTestDataFromHsOfficeTables(); - resetHsOfficeSequences(); - deleteFromTestTables(); - deleteFromRbacTables(); - - jpaAttempt.transacted(() -> { - context(rbacSuperuser); - contacts.forEach(this::persist); - updateLegacyIds(contacts, "hs_office_contact_legacy_id", "contact_id"); - }).assertSuccessful(); - - jpaAttempt.transacted(() -> { - context(rbacSuperuser); - persons.forEach(this::persist); - relations.forEach( (id, rel) -> this.persist(id, rel.getAnchor()) ); - relations.forEach( (id, rel) -> this.persist(id, rel.getHolder()) ); - }).assertSuccessful(); - - jpaAttempt.transacted(() -> { - context(rbacSuperuser); - relations.forEach(this::persist); - }).assertSuccessful(); - - System.out.println("persisting " + partners.size() + " partners"); - jpaAttempt.transacted(() -> { - context(rbacSuperuser); - partners.forEach((id, partner) -> { - // TODO: this is ugly and I don't know why it's suddenly necessary - partner.getPartnerRel().setAnchor(em.merge(partner.getPartnerRel().getAnchor())); - partner.getPartnerRel().setHolder(em.merge(partner.getPartnerRel().getHolder())); - partner.getPartnerRel().setContact(em.merge(partner.getPartnerRel().getContact())); - partner.setPartnerRel(em.merge(partner.getPartnerRel())); - em.persist(partner); - }); - updateLegacyIds(partners, "hs_office_partner_legacy_id", "bp_id"); - }).assertSuccessful(); - - jpaAttempt.transacted(() -> { - context(rbacSuperuser); - debitors.forEach((id, debitor) -> { - debitor.setDebitorRel(em.merge(debitor.getDebitorRel())); - persist(id, debitor); - }); - }).assertSuccessful(); - - jpaAttempt.transacted(() -> { - context(rbacSuperuser); - memberships.forEach(this::persist); - }).assertSuccessful(); - - jpaAttempt.transacted(() -> { - context(rbacSuperuser); - bankAccounts.forEach(this::persist); - }).assertSuccessful(); - - jpaAttempt.transacted(() -> { - context(rbacSuperuser); - sepaMandates.forEach(this::persist); - updateLegacyIds(sepaMandates, "hs_office_sepamandate_legacy_id", "sepa_mandate_id"); - }).assertSuccessful(); - - jpaAttempt.transacted(() -> { - context(rbacSuperuser); - coopShares.forEach(this::persist); - updateLegacyIds(coopShares, "hs_office_coopsharestransaction_legacy_id", "member_share_id"); - - }).assertSuccessful(); - - jpaAttempt.transacted(() -> { - context(rbacSuperuser); - coopAssets.forEach(this::persist); - updateLegacyIds(coopAssets, "hs_office_coopassetstransaction_legacy_id", "member_asset_id"); - }).assertSuccessful(); - - } - - @Test - @Order(9190) - void verifyMembershipsActuallyPersisted() { - final var biCount = (Integer) em.createNativeQuery("SELECT count(*) FROM hs_office_membership", Integer.class).getSingleResult(); - assertThat(biCount).isGreaterThan(isImportingControlledTestData() ? 5 : 300); - } - - private static boolean isImportingControlledTestData() { - return partners.size() <= MAX_NUMBER_OF_TEST_DATA_PARTNERS; - } - - private static void assumeThatWeAreImportingControlledTestData() { - assumeThat(partners.size()).isLessThanOrEqualTo(MAX_NUMBER_OF_TEST_DATA_PARTNERS); - } - - private void updateLegacyIds( - Map entities, - final String legacyIdTable, - final String legacyIdColumn) { - em.flush(); - entities.forEach((id, entity) -> em.createNativeQuery(""" - UPDATE ${legacyIdTable} - SET ${legacyIdColumn} = :legacyId - WHERE uuid = :uuid - """ - .replace("${legacyIdTable}", legacyIdTable) - .replace("${legacyIdColumn}", legacyIdColumn)) - .setParameter("legacyId", id) - .setParameter("uuid", entity.getUuid()) - .executeUpdate() - ); - } - - @Test - @Order(9999) - @ContinueOnFailure - void logCollectedErrors() { - this.assertNoErrors(); - } - - private void importBusinessPartners(final String[] header, final List records) { - - final var columns = new Columns(header); - - records.stream() - .map(this::trimAll) - .map(row -> new Record(columns, row)) - .forEach(rec -> { - final Integer bpId = rec.getInteger("bp_id"); - if (IGNORE_BUSINESS_PARTNERS.contains(bpId)) { - return; - } - - final var person = HsOfficePersonEntity.builder().build(); - - final var partnerRel = addRelation( - HsOfficeRelationType.PARTNER, - null, // is set after contacts when the person for 'Hostsharing eG' is known - person, - null // is set during contacts import depending on assigned roles - ); - - final var partner = HsOfficePartnerEntity.builder() - .partnerNumber(rec.getInteger("member_id")) - .details(HsOfficePartnerDetailsEntity.builder().build()) - .partnerRel(partnerRel) - .build(); - partners.put(bpId, partner); - - final var debitorRel = addRelation( - HsOfficeRelationType.DEBITOR, partnerRel.getHolder(), // partner person - null, // will be set in contacts import - null // will beset in contacts import - ); - - final var debitor = HsOfficeDebitorEntity.builder() - .debitorNumberSuffix("00") - .partner(partner) - .debitorRel(debitorRel) - .defaultPrefix(rec.getString("member_code").replace("hsh00-", "")) - .billable(rec.isEmpty("free") || rec.getString("free").equals("f")) - .vatReverseCharge(rec.getBoolean("exempt_vat")) - .vatBusiness("GROSS".equals(rec.getString("indicator_vat"))) // TODO: remove - .vatId(rec.getString("uid_vat")) - .build(); - debitors.put(bpId, debitor); - - if (isNotBlank(rec.getString("member_since"))) { - assertThat(rec.getInteger("member_id")).isEqualTo(partner.getPartnerNumber()); - final var membership = HsOfficeMembershipEntity.builder() - .partner(partner) - .memberNumberSuffix("00") - .validity(toPostgresDateRange( - rec.getLocalDate("member_since"), - rec.getLocalDate("member_until"))) - .membershipFeeBillable(rec.isEmpty("member_role")) - .status( - isBlank(rec.getString("member_until")) - ? HsOfficeMembershipStatus.ACTIVE - : HsOfficeMembershipStatus.UNKNOWN) - .build(); - memberships.put(bpId, membership); - } - }); - } - - private void importCoopShares(final String[] header, final List records) { - - final var columns = new Columns(header); - - records.stream() - .map(this::trimAll) - .map(row -> new Record(columns, row)) - .forEach(rec -> { - final var bpId = rec.getInteger("bp_id"); - if (IGNORE_BUSINESS_PARTNERS.contains(bpId)) { - return; - } - - final var member = ofNullable(memberships.get(bpId)) - .orElseGet(() -> createOnDemandMembership(bpId)); - - final var shareTransaction = HsOfficeCoopSharesTransactionEntity.builder() - .membership(member) - .valueDate(rec.getLocalDate("date")) - .transactionType( - "SUBSCRIPTION".equals(rec.getString("action")) - ? HsOfficeCoopSharesTransactionType.SUBSCRIPTION - : "UNSUBSCRIPTION".equals(rec.getString("action")) - ? HsOfficeCoopSharesTransactionType.CANCELLATION - : HsOfficeCoopSharesTransactionType.ADJUSTMENT - ) - .shareCount(rec.getInteger("quantity")) - .comment( rec.getString("comment")) - .reference(member.getMemberNumber().toString()) - .build(); - - if (shareTransaction.getTransactionType() == HsOfficeCoopSharesTransactionType.ADJUSTMENT) { - final var negativeValue = -shareTransaction.getShareCount(); - final var adjustedShareTx = coopShares.values().stream().filter(a -> - a.getTransactionType() != HsOfficeCoopSharesTransactionType.ADJUSTMENT && - a.getMembership() == shareTransaction.getMembership() && - a.getShareCount() == negativeValue) - .findAny() - .orElseThrow(() -> new IllegalStateException("cannot determine share reverse entry for adjustment " + shareTransaction)); - shareTransaction.setAdjustedShareTx(adjustedShareTx); - } - coopShares.put(rec.getInteger("member_share_id"), shareTransaction); - }); - } - - private void importCoopAssets(final String[] header, final List records) { - - final var columns = new Columns(header); - - records.stream() - .map(this::trimAll) - .map(row -> new Record(columns, row)) - .forEach(rec -> { - final var bpId = rec.getInteger("bp_id"); - - if (this.IGNORE_BUSINESS_PARTNERS.contains(bpId)) { - return; - } - - final var member = ofNullable(memberships.get(bpId)) - .orElseGet(() -> createOnDemandMembership(bpId)); - - final var assetTypeMapping = new HashMap() { - - { - put("ADJUSTMENT", HsOfficeCoopAssetsTransactionType.ADJUSTMENT); - put("HANDOVER", HsOfficeCoopAssetsTransactionType.TRANSFER); - put("ADOPTION", HsOfficeCoopAssetsTransactionType.ADOPTION); - put("LOSS", HsOfficeCoopAssetsTransactionType.LOSS); - put("CLEARING", HsOfficeCoopAssetsTransactionType.CLEARING); - put("PRESCRIPTION", HsOfficeCoopAssetsTransactionType.LIMITATION); - put("PAYBACK", HsOfficeCoopAssetsTransactionType.DISBURSAL); - put("PAYMENT", HsOfficeCoopAssetsTransactionType.DEPOSIT); - } - - public HsOfficeCoopAssetsTransactionType get(final String key) { - final var value = super.get(key); - if (value != null) { - return value; - } - throw new IllegalStateException("no mapping value found for: " + key); - } - }; - - final var assetTransaction = HsOfficeCoopAssetsTransactionEntity.builder() - .membership(member) - .valueDate(rec.getLocalDate("date")) - .transactionType(assetTypeMapping.get(rec.getString("action"))) - .assetValue(rec.getBigDecimal("amount")) - .comment(rec.getString("comment")) - .reference(member.getMemberNumber().toString()) - .build(); - - if (assetTransaction.getTransactionType() == HsOfficeCoopAssetsTransactionType.ADJUSTMENT) { - final var negativeValue = assetTransaction.getAssetValue().negate(); - final var adjustedAssetTx = coopAssets.values().stream().filter(a -> - a.getTransactionType() != HsOfficeCoopAssetsTransactionType.ADJUSTMENT && - a.getMembership() == assetTransaction.getMembership() && - a.getAssetValue().equals(negativeValue)) - .findAny() - .orElseThrow(() -> new IllegalStateException("cannot determine asset reverse entry for adjustment " + assetTransaction)); - assetTransaction.setAdjustedAssetTx(adjustedAssetTx); - } - - coopAssets.put(rec.getInteger("member_asset_id"), assetTransaction); - }); - } - - private static HsOfficeMembershipEntity createOnDemandMembership(final Integer bpId) { - final var onDemandMembership = HsOfficeMembershipEntity.builder() - .memberNumberSuffix("00") - .membershipFeeBillable(false) - .partner(partners.get(bpId)) - .status(HsOfficeMembershipStatus.INVALID) - .build(); - memberships.put(bpId, onDemandMembership); - return onDemandMembership; - } - - private void importSepaMandates(final String[] header, final List records) { - - final var columns = new Columns(header); - - records.stream() - .map(this::trimAll) - .map(row -> new Record(columns, row)) - .forEach(rec -> { - final var debitor = debitors.get(rec.getInteger("bp_id")); - - if (this.IGNORE_BUSINESS_PARTNERS.contains(rec.getInteger("bp_id"))) { - return; - } - - final var sepaMandate = HsOfficeSepaMandateEntity.builder() - .debitor(debitor) - .bankAccount(HsOfficeBankAccountEntity.builder() - .holder(rec.getString("bank_customer")) - // .bankName(rec.get("bank_name")) // not supported - .iban(rec.getString("bank_iban")) - .bic(rec.getString("bank_bic")) - .build()) - .reference(rec.getString("mandat_ref")) - .agreement(LocalDate.parse(rec.getString("mandat_signed"))) - .validity(toPostgresDateRange( - rec.getLocalDate("mandat_since"), - rec.getLocalDate("mandat_until"))) - .build(); - - sepaMandates.put(rec.getInteger("sepa_mandat_id"), sepaMandate); - bankAccounts.put(rec.getInteger("sepa_mandat_id"), sepaMandate.getBankAccount()); - }); - } - - private void importContacts(final String[] header, final List records) { - - final var columns = new Columns(header); - - records.stream() - .map(this::trimAll) - .map(row -> new Record(columns, row)) - .forEach(rec -> { - final var contactId = rec.getInteger("contact_id"); - final var bpId = rec.getInteger("bp_id"); - - if (IGNORE_CONTACTS.contains(contactId)) { - return; - } - if (IGNORE_BUSINESS_PARTNERS.contains(bpId)) { - return; - } - - if (rec.getString("roles").isBlank()) { - fail("empty roles assignment not allowed for contact_id: " + contactId); - } - - final var partner = partners.get(bpId); - final var debitor = debitors.get(bpId); - - final var partnerPerson = partner.getPartnerRel().getHolder(); - if (containsPartnerRel(rec)) { - addPerson(partnerPerson, rec); - } - - HsOfficePersonEntity contactPerson = partnerPerson; - if (!StringUtils.equals(rec.getString("firma"), partnerPerson.getTradeName()) || - !StringUtils.equals(rec.getString("first_name"), partnerPerson.getGivenName()) || - !StringUtils.equals(rec.getString("last_name"), partnerPerson.getFamilyName())) { - contactPerson = addPerson(HsOfficePersonEntity.builder().build(), rec); - } - - final var contact = HsOfficeContactRealEntity.builder().build(); - initContact(contact, rec); - - if (containsPartnerRel(rec)) { - assertThat(partner.getPartnerRel().getContact()).isNull(); - partner.getPartnerRel().setContact(contact); - } - if (containsRole(rec, "billing")) { - assertThat(debitor.getDebitorRel().getContact()).isNull(); - debitor.getDebitorRel().setHolder(contactPerson); - debitor.getDebitorRel().setContact(contact); - } - if (containsRole(rec, "operation")) { - addRelation(HsOfficeRelationType.OPERATIONS, partnerPerson, contactPerson, contact); - } - if (containsRole(rec, "contractual")) { - addRelation(HsOfficeRelationType.REPRESENTATIVE, partnerPerson, contactPerson, contact); - } - if (containsRole(rec, "ex-partner")) { - addRelation(HsOfficeRelationType.EX_PARTNER, partnerPerson, contactPerson, contact); - } - if (containsRole(rec, "vip-contact")) { - addRelation(HsOfficeRelationType.VIP_CONTACT, partnerPerson, contactPerson, contact); - } - for (String subscriberRole: SUBSCRIBER_ROLES) { - if (containsRole(rec, subscriberRole)) { - addRelation(HsOfficeRelationType.SUBSCRIBER, partnerPerson, contactPerson, contact) - .setMark(subscriberRole.split(":")[1]) - ; - } - } - verifyContainsOnlyKnownRoles(rec.getString("roles")); - }); - - assertNoMissingContractualRelations(); - useHostsharingAsPartnerAnchor(); - } - - private static void assertNoMissingContractualRelations() { - final var contractualMissing = new HashSet(); - partners.forEach( (id, partner) -> { - final var partnerPerson = partner.getPartnerRel().getHolder(); - if (relations.values().stream() - .filter(rel -> rel.getAnchor() == partnerPerson && rel.getType() == HsOfficeRelationType.REPRESENTATIVE) - .findFirst().isEmpty()) { - contractualMissing.add(partner.getPartnerNumber()); - } - }); - if (isImportingControlledTestData()) { - assertThat(contractualMissing).containsOnly(19999); // deliberately wrong partner entry - } else { - assertThat(contractualMissing).as("partners without contractual contact found").isEmpty(); - } - } - - private static void useHostsharingAsPartnerAnchor() { - final var mandant = persons.values().stream() - .filter(p -> p.getTradeName().startsWith("Hostsharing e")) - .findFirst() - .orElseThrow(); - relations.values().stream() - .filter(r -> r.getType() == HsOfficeRelationType.PARTNER) - .forEach(r -> r.setAnchor(mandant)); - } - - private static boolean containsRole(final Record rec, final String role) { - final var roles = rec.getString("roles"); - return ("," + roles + ",").contains("," + role + ","); - } - - private static boolean containsPartnerRel(final Record rec) { - return containsRole(rec, "partner"); - } - - private static HsOfficeRelationRealEntity addRelation( - final HsOfficeRelationType type, - final HsOfficePersonEntity anchor, - final HsOfficePersonEntity holder, - final HsOfficeContactRealEntity contact) { - final var rel = HsOfficeRelationRealEntity.builder() - .anchor(anchor) - .holder(holder) - .contact(contact) - .type(type) - .build(); - relations.put(relationId++, rel); - return rel; - } - - private HsOfficePersonEntity addPerson(final HsOfficePersonEntity person, final Record contactRecord) { - // TODO: title+salutation: add to person - person.setGivenName(contactRecord.getString("first_name")); - person.setFamilyName(contactRecord.getString("last_name")); - person.setTradeName(contactRecord.getString("firma")); - determinePersonType(person, contactRecord.getString("roles")); - - persons.put(contactRecord.getInteger("contact_id"), person); - return person; - } - - private static void determinePersonType(final HsOfficePersonEntity person, final String roles) { - if (person.getTradeName().isBlank()) { - person.setPersonType(HsOfficePersonType.NATURAL_PERSON); - } else - // contractual && !partner with a firm and a natural person name - // should actually be split up into two persons - // but the legacy database consists such records - if (roles.contains("contractual") && !roles.contains("partner") && - !person.getFamilyName().isBlank() && !person.getGivenName().isBlank()) { - person.setPersonType(HsOfficePersonType.NATURAL_PERSON); - } else if ( endsWithWord(person.getTradeName(), "e.K.", "e.G.", "eG", "GmbH", "AG", "KG") ) { - person.setPersonType(HsOfficePersonType.LEGAL_PERSON); - } else if ( endsWithWord(person.getTradeName(), "OHG") ) { - person.setPersonType(HsOfficePersonType.INCORPORATED_FIRM); - } else if ( endsWithWord(person.getTradeName(), "GbR") ) { - person.setPersonType(HsOfficePersonType.INCORPORATED_FIRM); - } else { - person.setPersonType(HsOfficePersonType.UNKNOWN_PERSON_TYPE); - } - } - - private static boolean endsWithWord(final String value, final String... endings) { - final var lowerCaseValue = value.toLowerCase(); - for( String ending: endings ) { - if (lowerCaseValue.endsWith(" " + ending.toLowerCase())) { - return true; - } - } - return false; - } - - private void verifyContainsOnlyKnownRoles(final String roles) { - final var allowedRolesSet = stream(KNOWN_ROLES).collect(Collectors.toSet()); - final var givenRolesSet = stream(roles.replace(" ", "").split(",")).collect(Collectors.toSet()); - final var unexpectedRolesSet = new HashSet<>(givenRolesSet); - unexpectedRolesSet.removeAll(allowedRolesSet); - assertThat(unexpectedRolesSet).isEmpty(); - } - - private HsOfficeContactRealEntity initContact(final HsOfficeContactRealEntity contact, final Record contactRecord) { - - contact.setCaption(toCaption( - contactRecord.getString("salut"), - contactRecord.getString("title"), - contactRecord.getString("first_name"), - contactRecord.getString("last_name"), - contactRecord.getString("firma"))); - contact.putEmailAddresses( Map.of("main", contactRecord.getString("email"))); - contact.setPostalAddress(toAddress(contactRecord)); - contact.putPhoneNumbers(toPhoneNumbers(contactRecord)); - - contacts.put(contactRecord.getInteger("contact_id"), contact); - return contact; - } - - private Map toPhoneNumbers(final Record rec) { - final var phoneNumbers = new LinkedHashMap(); - if (isNotBlank(rec.getString("phone_private"))) - phoneNumbers.put("phone_private", rec.getString("phone_private")); - if (isNotBlank(rec.getString("phone_office"))) - phoneNumbers.put("phone_office", rec.getString("phone_office")); - if (isNotBlank(rec.getString("phone_mobile"))) - phoneNumbers.put("phone_mobile", rec.getString("phone_mobile")); - if (isNotBlank(rec.getString("fax"))) - phoneNumbers.put("fax", rec.getString("fax")); - return phoneNumbers; - } - - private String toAddress(final Record rec) { - final var result = new StringBuilder(); - final var name = toName( - rec.getString("salut"), - rec.getString("title"), - rec.getString("first_name"), - rec.getString("last_name")); - if (isNotBlank(name)) - result.append(name + "\n"); - if (isNotBlank(rec.getString("firma"))) - result.append(rec.getString("firma") + "\n"); - if (isNotBlank(rec.getString("co"))) - result.append("c/o " + rec.getString("co") + "\n"); - if (isNotBlank(rec.getString("street"))) - result.append(rec.getString("street") + "\n"); - final var zipcodeAndCity = toZipcodeAndCity(rec); - if (isNotBlank(zipcodeAndCity)) - result.append(zipcodeAndCity + "\n"); - return result.toString(); - } - - private String toZipcodeAndCity(final Record rec) { - final var result = new StringBuilder(); - if (isNotBlank(rec.getString("country"))) - result.append(rec.getString("country") + " "); - if (isNotBlank(rec.getString("zipcode"))) - result.append(rec.getString("zipcode") + " "); - if (isNotBlank(rec.getString("city"))) - result.append(rec.getString("city")); - return result.toString(); - } - - private String toCaption( - final String salut, - final String title, - final String firstname, - final String lastname, - final String firm) { - final var result = new StringBuilder(); - if (isNotBlank(salut)) - result.append(salut + " "); - if (isNotBlank(title)) - result.append(title + " "); - if (isNotBlank(firstname)) - result.append(firstname + " "); - if (isNotBlank(lastname)) - result.append(lastname + " "); - if (isNotBlank(firm)) { - result.append( (isBlank(result) ? "" : ", ") + firm); - } - return result.toString(); - } - - private String toName(final String salut, final String title, final String firstname, final String lastname) { - return toCaption(salut, title, firstname, lastname, null); + @BeforeEach + void check() { + assertThat(jdbcUrl).isEqualTo("jdbc:tc:postgresql:15.5-bookworm:///importOfficeDataTC"); } } diff --git a/src/test/java/net/hostsharing/hsadminng/rbac/test/ContextBasedTestWithCleanup.java b/src/test/java/net/hostsharing/hsadminng/rbac/test/ContextBasedTestWithCleanup.java index 8e5f9683..366e79d7 100644 --- a/src/test/java/net/hostsharing/hsadminng/rbac/test/ContextBasedTestWithCleanup.java +++ b/src/test/java/net/hostsharing/hsadminng/rbac/test/ContextBasedTestWithCleanup.java @@ -32,7 +32,7 @@ import static org.assertj.core.api.Assertions.assertThat; // TODO.impl: cleanup the whole class public abstract class ContextBasedTestWithCleanup extends ContextBasedTest { - private static final boolean DETAILED_BUT_SLOW_CHECK = true; + private static final boolean DETAILED_BUT_SLOW_CHECK = false; @PersistenceContext protected EntityManager em;