initial data import for hs-office tables (db-migration #10)

Co-authored-by: Michael Hoennig <michael@hoennig.de>
Co-authored-by: Timotheus Pokorra <timotheus.pokorra@hostsharing.net>
Reviewed-on: #10
Reviewed-by: Timotheus Pokorra <timotheus.pokorra@hostsharing.net>
This commit is contained in:
Michael Hoennig 2024-01-23 15:11:23 +01:00
parent e427bb1784
commit fd1bd897b1
98 changed files with 2278 additions and 471 deletions

View File

@ -1,3 +1,10 @@
# For using the alias import-office-tables, # copy these exports to .environment (ignored by git)
# and amend them according to your external DB:
export HSADMINNG_POSTGRES_JDBC_URL=jdbc:tc:postgresql:15.5-bookworm:///spring_boot_testcontainers
export HSADMINNG_POSTGRES_ADMIN_USERNAME=admin
export HSADMINNG_POSTGRES_ADMIN_PASSWORD=
export HSADMINNG_POSTGRES_RESTRICTED_USERNAME=restricted
gradleWrapper () {
if [ ! -f gradlew ]; then
echo "No 'gradlew' found. Maybe you are not in the root dir of a gradle project?"
@ -36,9 +43,26 @@ postgresAutodoc () {
dot -Tsvg build/postgres-autodoc.neato >build/postgres-autodoc-rbac.svg && \
echo "generated $PWD/build/postgres-autodoc-rbac.svg"
}
alias postgres-autodoc=postgresAutodoc
function importOfficeData() {
export HSADMINNG_POSTGRES_JDBC=jdbc:tc:postgresql:15.5-bookworm:///spring_boot_testcontainers
export HSADMINNG_POSTGRES_ADMIN_USERNAME=admin
export HSADMINNG_POSTGRES_ADMIN_PASSWORD=password
export HSADMINNG_POSTGRES_RESTRICTED_USERNAME=restricted
export HSADMINNG_SUPERUSER=superuser-alex@hostsharing.net
if [ -f .environment ]; then
source .environment
fi
echo "using environment (with ending ';' for use in IntelliJ IDEA):"
set | grep ^HSADMINNG_ | sed 's/$/;/'
./gradlew importOfficeData --rerun
}
alias gw-importOfficeData=importOfficeData
alias podman-start='systemctl --user enable --now podman.socket && systemctl --user status podman.socket && ls -la /run/user/$UID/podman/podman.sock'
alias podman-stop='systemctl --user disable --now podman.socket && systemctl --user status podman.socket && ls -la /run/user/$UID/podman/podman.sock'
alias podman-use='export DOCKER_HOST="unix:///run/user/$UID/podman/podman.sock"; export TESTCONTAINERS_RYUK_DISABLED=true'
@ -53,3 +77,6 @@ alias pg-sql-backup='docker exec -i hsadmin-ng-postgres /usr/bin/pg_dump --clean
alias pg-sql-restore='gunzip --stdout | docker exec -i hsadmin-ng-postgres psql -U postgres -d postgres'
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'

1
.gitignore vendored
View File

@ -137,3 +137,4 @@ Desktop.ini
# ESLint
######################
.eslintcache
/.environment*

View File

@ -100,6 +100,7 @@ dependencies {
testImplementation 'io.rest-assured:spring-mock-mvc'
testImplementation 'org.hamcrest:hamcrest-core:2.2'
testImplementation 'org.pitest:pitest-junit5-plugin:1.2.1'
testImplementation 'org.junit.jupiter:junit-jupiter-api'
}
dependencyManagement {
@ -129,7 +130,7 @@ openapiProcessor {
processor 'io.openapiprocessor:openapi-processor-spring:2022.5'
apiPath "$projectDir/src/main/resources/api-definition.yaml"
mapping "$projectDir/src/main/resources/api-mappings.yaml"
targetDir "$projectDir/build/generated/sources/openapi-javax"
targetDir "$buildDir/generated/sources/openapi-javax"
showWarnings true
openApiNullable true
}
@ -138,7 +139,7 @@ openapiProcessor {
processor 'io.openapiprocessor:openapi-processor-spring:2022.5'
apiPath "$projectDir/src/main/resources/api-definition/rbac/rbac.yaml"
mapping "$projectDir/src/main/resources/api-definition/rbac/api-mappings.yaml"
targetDir "$projectDir/build/generated/sources/openapi-javax"
targetDir "$buildDir/generated/sources/openapi-javax"
showWarnings true
openApiNullable true
}
@ -147,7 +148,7 @@ openapiProcessor {
processor 'io.openapiprocessor:openapi-processor-spring:2022.5'
apiPath "$projectDir/src/main/resources/api-definition/test/test.yaml"
mapping "$projectDir/src/main/resources/api-definition/test/api-mappings.yaml"
targetDir "$projectDir/build/generated/sources/openapi-javax"
targetDir "$buildDir/generated/sources/openapi-javax"
showWarnings true
openApiNullable true
}
@ -156,7 +157,7 @@ openapiProcessor {
processor 'io.openapiprocessor:openapi-processor-spring:2022.5'
apiPath "$projectDir/src/main/resources/api-definition/hs-office/hs-office.yaml"
mapping "$projectDir/src/main/resources/api-definition/hs-office/api-mappings.yaml"
targetDir "$projectDir/build/generated/sources/openapi-javax"
targetDir "$buildDir/generated/sources/openapi-javax"
showWarnings true
openApiNullable true
}
@ -237,6 +238,9 @@ test {
excludes = [
'net.hostsharing.hsadminng.**.generated.**',
]
useJUnitPlatform {
excludeTags 'import'
}
}
jacocoTestReport {
dependsOn test
@ -297,6 +301,16 @@ jacocoTestCoverageVerification {
}
}
tasks.register('importOfficeData', Test) {
useJUnitPlatform {
includeTags 'import'
}
group 'verification'
description 'run the import jobs as tests'
}
// pitest mutation testing
pitest {
targetClasses = ['net.hostsharing.hsadminng.**']

View File

@ -0,0 +1,186 @@
# Beispiel: juristische Person (GmbH)
```mermaid
classDiagram
direction TD
namespace Hostsharing {
class person-HostsharingEG
}
namespace Partner {
class partner-MeierGmbH
class role-MeierGmbH
class personDetails-MeierGmbH
class contactData-MeierGmbH
class person-MeierGmbH
}
namespace Representatives {
class person-FrankMeier
class contactData-FrankMeier
class role-MeierGmbH-FrankMeier
}
namespace Debitors {
class debitor-MeierGmbH
class contactData-MeierGmbH-Buha
class role-MeierGmbH-Buha
}
namespace Operations {
class person-SabineMeier
class contactData-SabineMeier
class role-MeierGmbH-SabineMeier
}
namespace Enums {
class RoleType {
<<enumeration>>
UNKNOWN
REPRESENTATIVE
ACCOUNTING
OPERATIONS
}
class PersonType {
<<enumeration>>
UNKNOWN: nur für Import
NATURAL_PERSON: natürliche Person
LEGAL_PERSON: z.B. GmbH, e.K., eG, e.V.
INCORORATED_FIRM: z.B. OHG, Partnerschaftsgesellschaft
UNINCORPORATED_FIRM: z.B. GbR, ARGE, Erbengemeinschaft
PUBLIC_INSTITUTION: KdöR, AöR [ohne Registergericht/Registernummer]
}
}
class person-HostsharingEG {
+personType: LEGAL
+tradeName: Hostsahring eG
+familyName
+givenName
}
class partner-MeierGmbH {
+Numeric partnerNumber: 12345
+Role partnerRole
}
partner-MeierGmbH *-- role-MeierGmbH
class person-MeierGmbH {
+personType: LEGAL
+tradeName: Meier GmbH
+familyName
+givenName
}
person-MeierGmbH *-- personDetails-MeierGmbH
class personDetails-MeierGmbH {
+registrationOffice: AG Hamburg
+registrationNumber: ABC123434
+birthName
+birthPlace
+dateOfDeath
}
class contactData-MeierGmbH {
+postalAddress: Hauptstraße 5, 22345 Hamburg
+phoneNumbers: +49 40 12345-00
+emailAddresses: office@meier-gmbh.de
}
class role-MeierGmbH {
+RoleType RoleType PARTNER
+Person anchor
+Person holder
+Contact roleContact
}
role-MeierGmbH o-- person-HostsharingEG : anchor
role-MeierGmbH o-- person-MeierGmbH : holder
role-MeierGmbH o-- contactData-MeierGmbH
%% --- Debitors ---
class debitor-MeierGmbH {
+Partner partner
+Numeric[2] debitorNumberSuffix: 00
+Role billingRole
+boolean billable: true
+String vatId: ID123456789
+String vatCountryCode: DE
+boolean vatBusiness: true
+boolean vatReverseCharge: false
+BankAccount refundBankAccount
+String defaultPrefix: mei
}
debitor-MeierGmbH o-- partner-MeierGmbH
debitor-MeierGmbH *-- role-MeierGmbH-Buha
class contactData-MeierGmbH-Buha {
+postalAddress: Hauptstraße 5, 22345 Hamburg
+phoneNumbers: +49 40 12345-05
+emailAddresses: buha@meier-gmbh.de
}
class role-MeierGmbH-Buha {
+RoleType RoleType ACCOUNTING
+Person anchor
+Person holder
+Contact roleContact
}
role-MeierGmbH-Buha o-- person-MeierGmbH : anchor
role-MeierGmbH-Buha o-- person-MeierGmbH : holder
role-MeierGmbH-Buha o-- contactData-MeierGmbH-Buha
%% --- Representatives ---
class person-FrankMeier {
+ personType: NATURAL
+ tradeName
+ familyName: Meier
+ givenName: Frank
}
class contactData-FrankMeier {
+postalAddress
+phoneNumbers: +49 40 12345-22
+emailAddresses: frank.meier@meier-gmbh.de
}
class role-MeierGmbH-FrankMeier {
+RoleType RoleType REPRESENTATIVE
+Person anchor
+Person holder
+Contact roleContact
}
role-MeierGmbH-FrankMeier o-- person-MeierGmbH : anchor
role-MeierGmbH-FrankMeier o-- person-FrankMeier : holder
role-MeierGmbH-FrankMeier o-- contactData-FrankMeier
%% --- Operations ---
class person-SabineMeier {
+personType: NATURAL
+tradeName
+familyName: Meier
+givenName: Sabine
}
class contactData-SabineMeier {
+postalAddress
+phoneNumbers: +49 40 12345-22
+emailAddresses: sabine.meier@meier-gmbh.de
}
class role-MeierGmbH-SabineMeier {
+RoleType RoleType OPERATIONAL
+Person anchor
+Person holder
+Contact roleContact
}
role-MeierGmbH-SabineMeier o-- person-MeierGmbH : anchor
role-MeierGmbH-SabineMeier o-- person-SabineMeier : holder
role-MeierGmbH-SabineMeier o-- contactData-SabineMeier
```

View File

@ -3,6 +3,7 @@ package net.hostsharing.hsadminng.hs.office.bankaccount;
import lombok.*;
import lombok.experimental.FieldNameConstants;
import net.hostsharing.hsadminng.errors.DisplayName;
import net.hostsharing.hsadminng.hs.office.migration.HasUuid;
import net.hostsharing.hsadminng.stringify.Stringify;
import net.hostsharing.hsadminng.stringify.Stringifyable;
@ -23,7 +24,7 @@ import static net.hostsharing.hsadminng.stringify.Stringify.stringify;
@AllArgsConstructor
@FieldNameConstants
@DisplayName("BankAccount")
public class HsOfficeBankAccountEntity implements Stringifyable {
public class HsOfficeBankAccountEntity implements HasUuid, Stringifyable {
private static Stringify<HsOfficeBankAccountEntity> toString = stringify(HsOfficeBankAccountEntity.class, "bankAccount")
.withProp(Fields.holder, HsOfficeBankAccountEntity::getHolder)

View File

@ -3,6 +3,7 @@ package net.hostsharing.hsadminng.hs.office.contact;
import lombok.*;
import lombok.experimental.FieldNameConstants;
import net.hostsharing.hsadminng.errors.DisplayName;
import net.hostsharing.hsadminng.hs.office.migration.HasUuid;
import net.hostsharing.hsadminng.stringify.Stringify;
import net.hostsharing.hsadminng.stringify.Stringifyable;
import org.hibernate.annotations.GenericGenerator;
@ -21,7 +22,7 @@ import static net.hostsharing.hsadminng.stringify.Stringify.stringify;
@AllArgsConstructor
@FieldNameConstants
@DisplayName("Contact")
public class HsOfficeContactEntity implements Stringifyable {
public class HsOfficeContactEntity implements Stringifyable, HasUuid {
private static Stringify<HsOfficeContactEntity> toString = stringify(HsOfficeContactEntity.class, "contact")
.withProp(Fields.label, HsOfficeContactEntity::getLabel)
@ -38,10 +39,10 @@ public class HsOfficeContactEntity implements Stringifyable {
private String postalAddress;
@Column(name = "emailaddresses", columnDefinition = "json")
private String emailAddresses;
private String emailAddresses; // TODO: check if we can really add multiple. format: ["eins@...", "zwei@..."]
@Column(name = "phonenumbers", columnDefinition = "json")
private String phoneNumbers;
private String phoneNumbers; // TODO: check if we can really add multiple. format: { "office": "+49 40 12345-10", "fax": "+49 40 12345-05" }
@Override
public String toString() {

View File

@ -1,8 +1,10 @@
package net.hostsharing.hsadminng.hs.office.coopassets;
import lombok.*;
import net.hostsharing.hsadminng.errors.DisplayName;
import net.hostsharing.hsadminng.hs.office.membership.HsOfficeMembershipEntity;
import net.hostsharing.hsadminng.hs.office.migration.HasUuid;
import net.hostsharing.hsadminng.stringify.Stringify;
import net.hostsharing.hsadminng.stringify.Stringifyable;
import org.hibernate.annotations.GenericGenerator;
@ -13,6 +15,7 @@ import java.text.DecimalFormat;
import java.time.LocalDate;
import java.util.UUID;
import static java.util.Optional.ofNullable;
import static net.hostsharing.hsadminng.stringify.Stringify.stringify;
@Entity
@ -23,14 +26,15 @@ import static net.hostsharing.hsadminng.stringify.Stringify.stringify;
@NoArgsConstructor
@AllArgsConstructor
@DisplayName("CoopAssetsTransaction")
public class HsOfficeCoopAssetsTransactionEntity implements Stringifyable {
public class HsOfficeCoopAssetsTransactionEntity implements Stringifyable, HasUuid {
private static Stringify<HsOfficeCoopAssetsTransactionEntity> stringify = stringify(HsOfficeCoopAssetsTransactionEntity.class)
.withProp(e -> e.getMembership().getMemberNumber())
.withProp(HsOfficeCoopAssetsTransactionEntity::getMemberNumber)
.withProp(HsOfficeCoopAssetsTransactionEntity::getValueDate)
.withProp(HsOfficeCoopAssetsTransactionEntity::getTransactionType)
.withProp(HsOfficeCoopAssetsTransactionEntity::getAssetValue)
.withProp(HsOfficeCoopAssetsTransactionEntity::getReference)
.withProp(HsOfficeCoopAssetsTransactionEntity::getComment)
.withSeparator(", ")
.quotedValues(false);
@ -54,11 +58,16 @@ public class HsOfficeCoopAssetsTransactionEntity implements Stringifyable {
private BigDecimal assetValue;
@Column(name = "reference")
private String reference;
private String reference; // TODO: what is this for?
@Column(name = "comment")
private String comment;
public Integer getMemberNumber() {
return ofNullable(membership).map(HsOfficeMembershipEntity::getMemberNumber).orElse(null);
}
@Override
public String toString() {
return stringify.apply(this);
@ -66,6 +75,6 @@ public class HsOfficeCoopAssetsTransactionEntity implements Stringifyable {
@Override
public String toShortString() {
return membership.getMemberNumber() + new DecimalFormat("+0.00").format(assetValue);
return "%s%+1.2f".formatted(getMemberNumber(), assetValue);
}
}

View File

@ -1,5 +1,12 @@
package net.hostsharing.hsadminng.hs.office.coopassets;
public enum HsOfficeCoopAssetsTransactionType {
ADJUSTMENT, DEPOSIT, DISBURSAL, TRANSFER, ADOPTION, CLEARING, LOSS
ADJUSTMENT, // correction of wrong bookings
DEPOSIT, // payment received from member after signing shares, >0
DISBURSAL, // payment send to member after cancellation of shares, <0
TRANSFER, // transferring shares to another member, <0
ADOPTION, // receiving shares from another member, >0
CLEARING, // settlement with members dept, <0
LOSS, // assignment of balance sheet loss in case of cancellation of shares, <0
LIMITATION // limitation period was reached after impossible disbursal, <0
}

View File

@ -3,6 +3,7 @@ package net.hostsharing.hsadminng.hs.office.coopshares;
import lombok.*;
import net.hostsharing.hsadminng.errors.DisplayName;
import net.hostsharing.hsadminng.hs.office.membership.HsOfficeMembershipEntity;
import net.hostsharing.hsadminng.hs.office.migration.HasUuid;
import net.hostsharing.hsadminng.stringify.Stringify;
import net.hostsharing.hsadminng.stringify.Stringifyable;
@ -10,6 +11,7 @@ import jakarta.persistence.*;
import java.time.LocalDate;
import java.util.UUID;
import static java.util.Optional.ofNullable;
import static net.hostsharing.hsadminng.stringify.Stringify.stringify;
@Entity
@ -20,14 +22,15 @@ import static net.hostsharing.hsadminng.stringify.Stringify.stringify;
@NoArgsConstructor
@AllArgsConstructor
@DisplayName("CoopShareTransaction")
public class HsOfficeCoopSharesTransactionEntity implements Stringifyable {
public class HsOfficeCoopSharesTransactionEntity implements Stringifyable, HasUuid {
private static Stringify<HsOfficeCoopSharesTransactionEntity> stringify = stringify(HsOfficeCoopSharesTransactionEntity.class)
.withProp(e -> e.getMembership().getMemberNumber())
.withProp(HsOfficeCoopSharesTransactionEntity::getMemberNumber)
.withProp(HsOfficeCoopSharesTransactionEntity::getValueDate)
.withProp(HsOfficeCoopSharesTransactionEntity::getTransactionType)
.withProp(HsOfficeCoopSharesTransactionEntity::getShareCount)
.withProp(HsOfficeCoopSharesTransactionEntity::getReference)
.withProp(HsOfficeCoopSharesTransactionEntity::getComment)
.withSeparator(", ")
.quotedValues(false);
@ -50,7 +53,7 @@ public class HsOfficeCoopSharesTransactionEntity implements Stringifyable {
private int shareCount;
@Column(name = "reference")
private String reference;
private String reference; // TODO: what is this for?
@Column(name = "comment")
private String comment;
@ -60,8 +63,12 @@ public class HsOfficeCoopSharesTransactionEntity implements Stringifyable {
return stringify.apply(this);
}
public Integer getMemberNumber() {
return ofNullable(membership).map(HsOfficeMembershipEntity::getMemberNumber).orElse(null);
}
@Override
public String toShortString() {
return "%s%+d".formatted(membership.getMemberNumber(), shareCount);
return "%s%+d".formatted(getMemberNumber(), shareCount);
}
}

View File

@ -1,5 +1,7 @@
package net.hostsharing.hsadminng.hs.office.coopshares;
public enum HsOfficeCoopSharesTransactionType {
ADJUSTMENT, SUBSCRIPTION, CANCELLATION;
ADJUSTMENT, // correction of wrong bookings
SUBSCRIPTION, // shares signed, e.g. with the declaration of accession, >0
CANCELLATION; // shares terminated, e.g. when a membership is resigned, <0
}

View File

@ -4,12 +4,14 @@ import lombok.*;
import net.hostsharing.hsadminng.errors.DisplayName;
import net.hostsharing.hsadminng.hs.office.bankaccount.HsOfficeBankAccountEntity;
import net.hostsharing.hsadminng.hs.office.contact.HsOfficeContactEntity;
import net.hostsharing.hsadminng.hs.office.migration.HasUuid;
import net.hostsharing.hsadminng.hs.office.partner.HsOfficePartnerEntity;
import net.hostsharing.hsadminng.stringify.Stringify;
import net.hostsharing.hsadminng.stringify.Stringifyable;
import org.hibernate.annotations.GenericGenerator;
import jakarta.persistence.*;
import java.util.Optional;
import java.util.UUID;
import static net.hostsharing.hsadminng.stringify.Stringify.stringify;
@ -22,12 +24,16 @@ import static net.hostsharing.hsadminng.stringify.Stringify.stringify;
@NoArgsConstructor
@AllArgsConstructor
@DisplayName("Debitor")
public class HsOfficeDebitorEntity implements Stringifyable {
public class HsOfficeDebitorEntity implements HasUuid, Stringifyable {
// TODO: I would rather like to generate something matching this example:
// debitor(1234500: Test AG, tes)
// maybe remove withSepararator (always use ', ') and add withBusinessIdProp (with ': ' afterwards)?
private static Stringify<HsOfficeDebitorEntity> stringify =
stringify(HsOfficeDebitorEntity.class, "debitor")
.withProp(HsOfficeDebitorEntity::getDebitorNumber)
.withProp(HsOfficeDebitorEntity::getPartner)
.withProp(HsOfficeDebitorEntity::getDefaultPrefix)
.withSeparator(": ")
.quotedValues(false);
@ -40,12 +46,15 @@ public class HsOfficeDebitorEntity implements Stringifyable {
@JoinColumn(name = "partneruuid")
private HsOfficePartnerEntity partner;
@Column(name = "debitornumber")
private Integer debitorNumber;
@Column(name = "debitornumbersuffix", columnDefinition = "numeric(2)")
private Byte debitorNumberSuffix; // TODO maybe rather as a formatted String?
@ManyToOne
@JoinColumn(name = "billingcontactuuid")
private HsOfficeContactEntity billingContact;
private HsOfficeContactEntity billingContact; // TODO: migrate to billingPerson
@Column(name = "billable", nullable = false)
private Boolean billable; // not a primitive because otherwise the default would be false
@Column(name = "vatid")
private String vatId;
@ -56,10 +65,34 @@ public class HsOfficeDebitorEntity implements Stringifyable {
@Column(name = "vatbusiness")
private boolean vatBusiness;
@Column(name = "vatreversecharge")
private boolean vatReverseCharge;
@ManyToOne
@JoinColumn(name = "refundbankaccountuuid")
private HsOfficeBankAccountEntity refundBankAccount;
@Column(name = "defaultprefix", columnDefinition = "char(3) not null")
private String defaultPrefix;
public String getDebitorNumberString() {
// TODO: refactor
if (partner.getDebitorNumberPrefix() == null ) {
if (debitorNumberSuffix == null) {
return null;
}
return String.format("%02d", debitorNumberSuffix);
}
if (debitorNumberSuffix == null) {
return partner.getDebitorNumberPrefix() + "??";
}
return partner.getDebitorNumberPrefix() + String.format("%02d", debitorNumberSuffix);
}
public Integer getDebitorNumber() {
return Optional.ofNullable(getDebitorNumberString()).map(Integer::parseInt).orElse(null);
}
@Override
public String toString() {
return stringify.apply(this);
@ -67,6 +100,6 @@ public class HsOfficeDebitorEntity implements Stringifyable {
@Override
public String toShortString() {
return debitorNumber.toString();
return getDebitorNumberString();
}
}

View File

@ -1,11 +1,13 @@
package net.hostsharing.hsadminng.hs.office.debitor;
import net.hostsharing.hsadminng.hs.office.bankaccount.HsOfficeBankAccountEntity;
import net.hostsharing.hsadminng.hs.office.contact.HsOfficeContactEntity;
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeDebitorPatchResource;
import net.hostsharing.hsadminng.mapper.EntityPatcher;
import net.hostsharing.hsadminng.mapper.OptionalFromJson;
import jakarta.persistence.EntityManager;
import java.util.Optional;
class HsOfficeDebitorEntityPatcher implements EntityPatcher<HsOfficeDebitorPatchResource> {
@ -25,11 +27,18 @@ class HsOfficeDebitorEntityPatcher implements EntityPatcher<HsOfficeDebitorPatch
verifyNotNull(newValue, "billingContact");
entity.setBillingContact(em.getReference(HsOfficeContactEntity.class, newValue));
});
Optional.ofNullable(resource.getBillable()).ifPresent(entity::setBillable);
OptionalFromJson.of(resource.getVatId()).ifPresent(entity::setVatId);
OptionalFromJson.of(resource.getVatCountryCode()).ifPresent(entity::setVatCountryCode);
OptionalFromJson.of(resource.getVatBusiness()).ifPresent(newValue -> {
verifyNotNull(newValue, "vatBusiness");
entity.setVatBusiness(newValue);
Optional.ofNullable(resource.getVatBusiness()).ifPresent(entity::setVatBusiness);
Optional.ofNullable(resource.getVatReverseCharge()).ifPresent(entity::setVatReverseCharge);
OptionalFromJson.of(resource.getDefaultPrefix()).ifPresent(newValue -> {
verifyNotNull(newValue, "defaultPrefix");
entity.setDefaultPrefix(newValue);
});
OptionalFromJson.of(resource.getRefundBankAccountUuid()).ifPresent(newValue -> {
verifyNotNull(newValue, "refundBankAccount");
entity.setRefundBankAccount(em.getReference(HsOfficeBankAccountEntity.class, newValue));
});
}

View File

@ -13,9 +13,14 @@ public interface HsOfficeDebitorRepository extends Repository<HsOfficeDebitorEnt
@Query("""
SELECT debitor FROM HsOfficeDebitorEntity debitor
WHERE debitor.debitorNumber = :debitorNumber
WHERE cast(debitor.partner.debitorNumberPrefix as integer) = :debitorNumberPrefix
AND cast(debitor.debitorNumberSuffix as integer) = :debitorNumberSuffix
""")
List<HsOfficeDebitorEntity> findDebitorByDebitorNumber(int debitorNumber);
List<HsOfficeDebitorEntity> findDebitorByDebitorNumber(int debitorNumberPrefix, byte debitorNumberSuffix);
default List<HsOfficeDebitorEntity> findDebitorByDebitorNumber(int debitorNumber) {
return findDebitorByDebitorNumber( debitorNumber/100, (byte) (debitorNumber%100));
}
@Query("""
SELECT debitor FROM HsOfficeDebitorEntity debitor

View File

@ -5,6 +5,7 @@ import com.vladmihalcea.hibernate.type.range.Range;
import lombok.*;
import net.hostsharing.hsadminng.errors.DisplayName;
import net.hostsharing.hsadminng.hs.office.debitor.HsOfficeDebitorEntity;
import net.hostsharing.hsadminng.hs.office.migration.HasUuid;
import net.hostsharing.hsadminng.hs.office.partner.HsOfficePartnerEntity;
import net.hostsharing.hsadminng.stringify.Stringify;
import net.hostsharing.hsadminng.stringify.Stringifyable;
@ -27,7 +28,7 @@ import static net.hostsharing.hsadminng.stringify.Stringify.stringify;
@NoArgsConstructor
@AllArgsConstructor
@DisplayName("Membership")
public class HsOfficeMembershipEntity implements Stringifyable {
public class HsOfficeMembershipEntity implements HasUuid, Stringifyable {
private static Stringify<HsOfficeMembershipEntity> stringify = stringify(HsOfficeMembershipEntity.class)
.withProp(HsOfficeMembershipEntity::getMemberNumber)
@ -52,12 +53,15 @@ public class HsOfficeMembershipEntity implements Stringifyable {
private HsOfficeDebitorEntity mainDebitor;
@Column(name = "membernumber")
private int memberNumber;
private int memberNumber; // TODO: migrate to suffix, like debitorNumberSuffix
@Column(name = "validity", columnDefinition = "daterange")
@Type(PostgreSQLRangeType.class)
private Range<LocalDate> validity;
@Column(name = "membershipfeebillable", nullable = false)
private Boolean membershipFeeBillable; // not primitive to force setting the value
@Column(name = "reasonfortermination")
@Enumerated(EnumType.STRING)
private HsOfficeReasonForTermination reasonForTermination;

View File

@ -37,6 +37,8 @@ public class HsOfficeMembershipEntityPatcher implements EntityPatcher<HsOfficeMe
Optional.ofNullable(resource.getReasonForTermination())
.map(v -> mapper.map(v, HsOfficeReasonForTermination.class))
.ifPresent(entity::setReasonForTermination);
OptionalFromJson.of(resource.getMembershipFeeBillable()).ifPresent(
entity::setMembershipFeeBillable);
}
private void verifyNotNull(final UUID newValue, final String propertyName) {

View File

@ -1,5 +1,5 @@
package net.hostsharing.hsadminng.hs.office.membership;
public enum HsOfficeReasonForTermination {
NONE, CANCELLATION, TRANSFER, DEATH, LIQUIDATION, EXPULSION;
NONE, CANCELLATION, TRANSFER, DEATH, LIQUIDATION, EXPULSION, UNKNOWN;
}

View File

@ -0,0 +1,7 @@
package net.hostsharing.hsadminng.hs.office.migration;
import java.util.UUID;
public interface HasUuid {
UUID getUuid();
}

View File

@ -2,6 +2,7 @@ package net.hostsharing.hsadminng.hs.office.partner;
import lombok.*;
import net.hostsharing.hsadminng.errors.DisplayName;
import net.hostsharing.hsadminng.hs.office.migration.HasUuid;
import net.hostsharing.hsadminng.stringify.Stringify;
import net.hostsharing.hsadminng.stringify.Stringifyable;
@ -19,15 +20,16 @@ import static net.hostsharing.hsadminng.stringify.Stringify.stringify;
@NoArgsConstructor
@AllArgsConstructor
@DisplayName("PartnerDetails")
public class HsOfficePartnerDetailsEntity implements Stringifyable {
public class HsOfficePartnerDetailsEntity implements HasUuid, Stringifyable {
private static Stringify<HsOfficePartnerDetailsEntity> stringify = stringify(
HsOfficePartnerDetailsEntity.class,
"partnerDetails")
.withProp(HsOfficePartnerDetailsEntity::getRegistrationOffice)
.withProp(HsOfficePartnerDetailsEntity::getRegistrationNumber)
.withProp(HsOfficePartnerDetailsEntity::getBirthPlace)
.withProp(HsOfficePartnerDetailsEntity::getBirthday)
.withProp(HsOfficePartnerDetailsEntity::getBirthday)
.withProp(HsOfficePartnerDetailsEntity::getBirthName)
.withProp(HsOfficePartnerDetailsEntity::getDateOfDeath)
.withSeparator(", ")
.quotedValues(false);
@ -39,6 +41,7 @@ public class HsOfficePartnerDetailsEntity implements Stringifyable {
private @Column(name = "registrationoffice") String registrationOffice;
private @Column(name = "registrationnumber") String registrationNumber;
private @Column(name = "birthname") String birthName;
private @Column(name = "birthplace") String birthPlace;
private @Column(name = "birthday") LocalDate birthday;
private @Column(name = "dateofdeath") LocalDate dateOfDeath;

View File

@ -22,6 +22,7 @@ class HsOfficePartnerDetailsEntityPatcher implements EntityPatcher<HsOfficePartn
OptionalFromJson.of(resource.getRegistrationOffice()).ifPresent(entity::setRegistrationOffice);
OptionalFromJson.of(resource.getRegistrationNumber()).ifPresent(entity::setRegistrationNumber);
OptionalFromJson.of(resource.getBirthday()).ifPresent(entity::setBirthday);
OptionalFromJson.of(resource.getBirthPlace()).ifPresent(entity::setBirthPlace);
OptionalFromJson.of(resource.getBirthName()).ifPresent(entity::setBirthName);
OptionalFromJson.of(resource.getDateOfDeath()).ifPresent(entity::setDateOfDeath);
}

View File

@ -3,6 +3,7 @@ package net.hostsharing.hsadminng.hs.office.partner;
import lombok.*;
import net.hostsharing.hsadminng.errors.DisplayName;
import net.hostsharing.hsadminng.hs.office.contact.HsOfficeContactEntity;
import net.hostsharing.hsadminng.hs.office.migration.HasUuid;
import net.hostsharing.hsadminng.hs.office.person.HsOfficePersonEntity;
import net.hostsharing.hsadminng.stringify.Stringify;
import net.hostsharing.hsadminng.stringify.Stringifyable;
@ -10,6 +11,7 @@ import org.hibernate.annotations.NotFound;
import org.hibernate.annotations.NotFoundAction;
import jakarta.persistence.*;
import java.util.Optional;
import java.util.UUID;
import static net.hostsharing.hsadminng.stringify.Stringify.stringify;
@ -22,7 +24,7 @@ import static net.hostsharing.hsadminng.stringify.Stringify.stringify;
@NoArgsConstructor
@AllArgsConstructor
@DisplayName("Partner")
public class HsOfficePartnerEntity implements Stringifyable {
public class HsOfficePartnerEntity implements Stringifyable, HasUuid {
private static Stringify<HsOfficePartnerEntity> stringify = stringify(HsOfficePartnerEntity.class, "partner")
.withProp(HsOfficePartnerEntity::getPerson)
@ -34,6 +36,9 @@ public class HsOfficePartnerEntity implements Stringifyable {
@GeneratedValue
private UUID uuid;
@Column(name = "debitornumberprefix", columnDefinition = "numeric(5) not null")
private Integer debitorNumberPrefix;
@ManyToOne
@JoinColumn(name = "personuuid", nullable = false)
private HsOfficePersonEntity person;
@ -43,7 +48,7 @@ public class HsOfficePartnerEntity implements Stringifyable {
private HsOfficeContactEntity contact;
@ManyToOne(cascade = { CascadeType.PERSIST, CascadeType.MERGE, CascadeType.DETACH }, optional = true)
@JoinColumn(name = "detailsuuid", nullable = true)
@JoinColumn(name = "detailsuuid")
@NotFound(action = NotFoundAction.IGNORE)
private HsOfficePartnerDetailsEntity details;
@ -54,6 +59,6 @@ public class HsOfficePartnerEntity implements Stringifyable {
@Override
public String toShortString() {
return person.toShortString();
return Optional.ofNullable(person).map(HsOfficePersonEntity::toShortString).orElse("<person=null>");
}
}

View File

@ -3,6 +3,7 @@ package net.hostsharing.hsadminng.hs.office.person;
import lombok.*;
import lombok.experimental.FieldNameConstants;
import net.hostsharing.hsadminng.errors.DisplayName;
import net.hostsharing.hsadminng.hs.office.migration.HasUuid;
import net.hostsharing.hsadminng.stringify.Stringify;
import net.hostsharing.hsadminng.stringify.Stringifyable;
import org.apache.commons.lang3.StringUtils;
@ -21,7 +22,7 @@ import static net.hostsharing.hsadminng.stringify.Stringify.stringify;
@AllArgsConstructor
@FieldNameConstants
@DisplayName("Person")
public class HsOfficePersonEntity implements Stringifyable {
public class HsOfficePersonEntity implements HasUuid, Stringifyable {
private static Stringify<HsOfficePersonEntity> toString = stringify(HsOfficePersonEntity.class, "person")
.withProp(Fields.personType, HsOfficePersonEntity::getPersonType)
@ -53,6 +54,7 @@ public class HsOfficePersonEntity implements Stringifyable {
@Override
public String toShortString() {
return !StringUtils.isEmpty(tradeName) ? tradeName : (familyName + ", " + givenName);
return personType + " " +
(!StringUtils.isEmpty(tradeName) ? tradeName : (familyName + ", " + givenName));
}
}

View File

@ -1,6 +1,7 @@
package net.hostsharing.hsadminng.hs.office.person;
public enum HsOfficePersonType {
UNKNOWN,
NATURAL,
LEGAL,
SOLE_REPRESENTATION,

View File

@ -3,8 +3,10 @@ package net.hostsharing.hsadminng.hs.office.relationship;
import lombok.*;
import lombok.experimental.FieldNameConstants;
import net.hostsharing.hsadminng.hs.office.contact.HsOfficeContactEntity;
import net.hostsharing.hsadminng.hs.office.migration.HasUuid;
import net.hostsharing.hsadminng.hs.office.person.HsOfficePersonEntity;
import net.hostsharing.hsadminng.stringify.Stringify;
import net.hostsharing.hsadminng.stringify.Stringifyable;
import jakarta.persistence.*;
import java.util.UUID;
@ -19,7 +21,7 @@ import static net.hostsharing.hsadminng.stringify.Stringify.stringify;
@NoArgsConstructor
@AllArgsConstructor
@FieldNameConstants
public class HsOfficeRelationshipEntity {
public class HsOfficeRelationshipEntity implements HasUuid, Stringifyable {
private static Stringify<HsOfficeRelationshipEntity> toString = stringify(HsOfficeRelationshipEntity.class, "rel")
.withProp(Fields.relAnchor, HsOfficeRelationshipEntity::getRelAnchor)
@ -27,6 +29,11 @@ public class HsOfficeRelationshipEntity {
.withProp(Fields.relHolder, HsOfficeRelationshipEntity::getRelHolder)
.withProp(Fields.contact, HsOfficeRelationshipEntity::getContact);
private static Stringify<HsOfficeRelationshipEntity> toShortString = stringify(HsOfficeRelationshipEntity.class, "rel")
.withProp(Fields.relAnchor, HsOfficeRelationshipEntity::getRelAnchor)
.withProp(Fields.relType, HsOfficeRelationshipEntity::getRelType)
.withProp(Fields.relHolder, HsOfficeRelationshipEntity::getRelHolder);
@Id
@GeneratedValue
private UUID uuid;
@ -51,4 +58,9 @@ public class HsOfficeRelationshipEntity {
public String toString() {
return toString.apply(this);
}
@Override
public String toShortString() {
return toShortString.apply(this);
}
}

View File

@ -1,8 +1,10 @@
package net.hostsharing.hsadminng.hs.office.relationship;
public enum HsOfficeRelationshipType {
SOLE_AGENT,
JOINT_AGENT,
ACCOUNTING_CONTACT,
TECHNICAL_CONTACT
UNKNOWN,
EX_PARTNER,
REPRESENTATIVE,
VIP_CONTACT,
ACCOUNTING,
OPERATIONS
}

View File

@ -6,6 +6,7 @@ import lombok.*;
import net.hostsharing.hsadminng.errors.DisplayName;
import net.hostsharing.hsadminng.hs.office.bankaccount.HsOfficeBankAccountEntity;
import net.hostsharing.hsadminng.hs.office.debitor.HsOfficeDebitorEntity;
import net.hostsharing.hsadminng.hs.office.migration.HasUuid;
import net.hostsharing.hsadminng.stringify.Stringify;
import net.hostsharing.hsadminng.stringify.Stringifyable;
import org.hibernate.annotations.Type;
@ -25,7 +26,7 @@ import static net.hostsharing.hsadminng.stringify.Stringify.stringify;
@NoArgsConstructor
@AllArgsConstructor
@DisplayName("SEPA-Mandate")
public class HsOfficeSepaMandateEntity implements Stringifyable {
public class HsOfficeSepaMandateEntity implements Stringifyable, HasUuid {
private static Stringify<HsOfficeSepaMandateEntity> stringify = stringify(HsOfficeSepaMandateEntity.class)
.withProp(e -> e.getBankAccount().getIban())

View File

@ -12,12 +12,19 @@ components:
debitorNumber:
type: integer
format: int32
minimum: 10000
maximum: 99999
minimum: 1000000
maximum: 9999999
debitorNumberSuffix:
type: integer
format: int8
minimum: 00
maximum: 99
partner:
$ref: './hs-office-partner-schemas.yaml#/components/schemas/HsOfficePartner'
billingContact:
$ref: './hs-office-contact-schemas.yaml#/components/schemas/HsOfficeContact'
billable:
type: boolean
vatId:
type: string
vatCountryCode:
@ -25,8 +32,13 @@ components:
pattern: '^[A-Z][A-Z]$'
vatBusiness:
type: boolean
vatReverseCharge:
type: boolean
refundBankAccount:
$ref: './hs-office-bankaccount-schemas.yaml#/components/schemas/HsOfficeBankAccount'
defaultPrefix:
type: string
pattern: '^[a-z0-9]{3}$'
HsOfficeDebitorPatch:
type: object
@ -35,6 +47,9 @@ components:
type: string
format: uuid
nullable: true
billable:
type: boolean
nullable: false
vatId:
type: string
nullable: true
@ -44,11 +59,18 @@ components:
nullable: true
vatBusiness:
type: boolean
nullable: true
nullable: false
vatReverseCharge:
type: boolean
nullable: false
refundBankAccountUuid:
type: string
format: uuid
nullable: true
defaultPrefix:
type: string
pattern: '^[a-z0-9]{3}$'
nullable: true
HsOfficeDebitorInsert:
type: object
@ -61,11 +83,13 @@ components:
type: string
format: uuid
nullable: false
debitorNumber:
debitorNumberSuffix:
type: integer
format: int32
minimum: 10000
maximum: 99999
format: int8
minimum: 00
maximum: 99
billable:
type: boolean
vatId:
type: string
vatCountryCode:
@ -73,9 +97,17 @@ components:
pattern: '^[A-Z][A-Z]$'
vatBusiness:
type: boolean
vatReverseCharge:
type: boolean
refundBankAccountUuid:
type: string
format: uuid
defaultPrefix:
type: string
pattern: '^[a-z]{3}$'
required:
- partnerUuid
- billingContactUuid
- defaultPrefix
- billable

View File

@ -33,6 +33,8 @@ components:
format: date
reasonForTermination:
$ref: '#/components/schemas/HsOfficeReasonForTermination'
membershipFeeBillable:
type: boolean
HsOfficeMembershipPatch:
type: object
@ -48,6 +50,9 @@ components:
reasonForTermination:
nullable: true
$ref: '#/components/schemas/HsOfficeReasonForTermination'
membershipFeeBillable:
nullable: true
type: boolean
additionalProperties: false
HsOfficeMembershipInsert:
@ -74,8 +79,12 @@ components:
nullable: true
reasonForTermination:
$ref: '#/components/schemas/HsOfficeReasonForTermination'
membershipFeeBillable:
nullable: false
type: boolean
required:
- partnerUuid
- mainDebitorUuid
- validFrom
- membershipFeeBillable
additionalProperties: false

View File

@ -9,6 +9,11 @@ components:
uuid:
type: string
format: uuid
debitorNumberPrefix:
type: integer
format: int8
minimum: 10000
maximum: 99999
person:
$ref: './hs-office-person-schemas.yaml#/components/schemas/HsOfficePerson'
contact:
@ -32,6 +37,9 @@ components:
birthName:
type: string
nullable: true
birthPlace:
type: string
nullable: true
birthday:
type: string
format: date
@ -68,6 +76,9 @@ components:
birthName:
type: string
nullable: true
birthPlace:
type: string
nullable: true
birthday:
type: string
format: date
@ -80,6 +91,11 @@ components:
HsOfficePartnerInsert:
type: object
properties:
debitorNumberPrefix:
type: integer
format: int8
minimum: 10000
maximum: 99999
personUuid:
type: string
format: uuid
@ -89,6 +105,7 @@ components:
details:
$ref: '#/components/schemas/HsOfficePartnerDetailsInsert'
required:
- debitorNumberPrefix
- personUuid
- contactUuid
- details
@ -106,6 +123,9 @@ components:
birthName:
type: string
nullable: true
birthPlace:
type: string
nullable: true
birthday:
type: string
format: date

View File

@ -6,10 +6,12 @@ components:
HsOfficeRelationshipType:
type: string
enum:
- SOLE_AGENT # e.g. CEO
- JOINT_AGENT # e.g. heir
- ACCOUNTING_CONTACT
- TECHNICAL_CONTACT
- UNKNOWN
- EX_PARTNER
- REPRESENTATIVE,
- VIP_CONTACT
- ACCOUNTING,
- OPERATIONS
HsOfficeRelationship:
type: object

View File

@ -20,3 +20,7 @@ spring:
liquibase:
contexts: dev
hsadminng:
postgres:
leakproof:

View File

@ -0,0 +1,18 @@
--liquibase formatted sql
-- ============================================================================
-- NUMERIC-HASH-FUNCTIONS
--changeset hash:1 endDelimiter:--//
-- ----------------------------------------------------------------------------
do $$
begin
if starts_with ('${HSADMINNG_POSTGRES_ADMIN_USERNAME}', '$') then
RAISE EXCEPTION 'environment variable HSADMINNG_POSTGRES_ADMIN_USERNAME not set';
end if;
if starts_with ('${HSADMINNG_POSTGRES_RESTRICTED_USERNAME}', '$') then
RAISE EXCEPTION 'environment variable HSADMINNG_POSTGRES_RESTRICTED_USERNAME not set';
end if;
end $$
--//

View File

@ -55,7 +55,7 @@ end; $$;
*/
create or replace function currentTask()
returns varchar(96)
stable leakproof
stable -- leakproof
language plpgsql as $$
declare