HsOfficeDebitorEntity.java
package net.hostsharing.hsadminng.hs.office.debitor;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import net.hostsharing.hsadminng.errors.DisplayAs;
import net.hostsharing.hsadminng.hs.office.bankaccount.HsOfficeBankAccountEntity;
import net.hostsharing.hsadminng.hs.office.partner.HsOfficePartner;
import net.hostsharing.hsadminng.hs.office.partner.HsOfficePartnerRealEntity;
import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelation;
import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationRealEntity;
import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationRbacEntity;
import net.hostsharing.hsadminng.persistence.BaseEntity;
import net.hostsharing.hsadminng.rbac.generator.RbacSpec;
import net.hostsharing.hsadminng.rbac.generator.RbacSpec.SQL;
import net.hostsharing.hsadminng.repr.Stringify;
import net.hostsharing.hsadminng.repr.Stringifyable;
import org.hibernate.annotations.JoinFormula;
import org.hibernate.annotations.NotFound;
import org.hibernate.annotations.NotFoundAction;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
import jakarta.persistence.Version;
import jakarta.validation.constraints.Pattern;
import java.io.IOException;
import java.util.UUID;
import static jakarta.persistence.CascadeType.DETACH;
import static jakarta.persistence.CascadeType.MERGE;
import static jakarta.persistence.CascadeType.PERSIST;
import static jakarta.persistence.CascadeType.REFRESH;
import static java.util.Optional.ofNullable;
import static net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationType.DEBITOR;
import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.Column.dependsOnColumn;
import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.ColumnValue.usingCase;
import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.ColumnValue.usingDefaultCase;
import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.GLOBAL;
import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.Nullable.NOT_NULL;
import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.Nullable.NULLABLE;
import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.Permission.*;
import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.Role.*;
import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.SQL.directlyFetchedByDependsOnColumn;
import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.SQL.fetchedBySql;
import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.rbacViewFor;
import static net.hostsharing.hsadminng.repr.Stringify.stringify;
@Entity
@Table(schema = "hs_office", name = "debitor_rv")
@Getter
@Setter
@Builder(toBuilder = true)
@NoArgsConstructor
@AllArgsConstructor
@DisplayAs("Debitor")
public class HsOfficeDebitorEntity implements BaseEntity<HsOfficeDebitorEntity>, Stringifyable {
public static final String DEBITOR_NUMBER_TAG = "D-";
public static final String TWO_DECIMAL_DIGITS = "^([0-9]{2})$";
private static Stringify<HsOfficeDebitorEntity> stringify =
stringify(HsOfficeDebitorEntity.class, "debitor")
.withIdProp(HsOfficeDebitorEntity::toShortString)
.withProp(e -> ofNullable(e.getDebitorRel()).map(HsOfficeRelation::toShortString).orElse(null))
.withProp(HsOfficeDebitorEntity::getDefaultPrefix)
.quotedValues(false);
@Id
@GeneratedValue
private UUID uuid;
@Version
private int version;
@ManyToOne(fetch = FetchType.LAZY)
@JoinFormula(
referencedColumnName = "uuid",
value = """
(
SELECT DISTINCT partner.uuid
FROM hs_office.partner partner
JOIN hs_office.relation dRel
ON dRel.uuid = debitorRelUuid AND dRel.type = 'DEBITOR'
JOIN hs_office.relation pRel
ON pRel.uuid = partner.partnerRelUuid AND pRel.type = 'PARTNER'
WHERE pRel.holderUuid = dRel.anchorUuid
)
""")
@NotFound(action = NotFoundAction.EXCEPTION) // TODO.impl: map a simplified raw-PartnerEntity, just for the partner-number
private HsOfficePartnerRealEntity partner;
@Column(name = "debitornumbersuffix", length = 2)
@Pattern(regexp = TWO_DECIMAL_DIGITS)
private String debitorNumberSuffix;
@ManyToOne(cascade = { PERSIST, MERGE, REFRESH, DETACH }, optional = false, fetch = FetchType.LAZY)
@JoinColumn(name = "debitorreluuid", nullable = false)
private HsOfficeRelationRealEntity debitorRel;
@Column(name = "billable", nullable = false)
private Boolean billable; // not a primitive because otherwise the default would be false
@Column(name = "vatid")
private String vatId;
@Column(name = "vatcountrycode")
private String vatCountryCode;
@Column(name = "vatbusiness")
private boolean vatBusiness;
@Column(name = "vatreversecharge")
private boolean vatReverseCharge;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "refundbankaccountuuid")
@NotFound(action = NotFoundAction.IGNORE)
private HsOfficeBankAccountEntity refundBankAccount;
@Column(name = "defaultprefix", columnDefinition = "char(3) not null")
private String defaultPrefix;
@Override
public HsOfficeDebitorEntity load() {
BaseEntity.super.load();
partner.load();
debitorRel.load();
if (refundBankAccount != null) {
refundBankAccount.load();
}
return this;
}
public String getTaggedDebitorNumber() {
return ofNullable(partner)
.filter(partner -> debitorNumberSuffix != null)
.map(HsOfficePartner::getPartnerNumber)
.map(partnerNumber -> DEBITOR_NUMBER_TAG + partnerNumber + debitorNumberSuffix)
.orElse(null);
}
@Override
public String toString() {
return stringify.apply(this);
}
@Override
public String toShortString() {
return getTaggedDebitorNumber();
}
public static RbacSpec rbac() {
return rbacViewFor("debitor", HsOfficeDebitorEntity.class)
.withIdentityView(SQL.query("""
SELECT debitor.uuid AS uuid,
'D-' || (SELECT partner.partnerNumber
FROM hs_office.partner partner
JOIN hs_office.relation partnerRel
ON partnerRel.uuid = partner.partnerRelUUid AND partnerRel.type = 'PARTNER'
JOIN hs_office.relation debitorRel
ON debitorRel.anchorUuid = partnerRel.holderUuid AND debitorRel.type = 'DEBITOR'
WHERE debitorRel.uuid = debitor.debitorRelUuid)
|| debitorNumberSuffix as idName
FROM hs_office.debitor AS debitor
"""))
.withRestrictedViewOrderBy(SQL.projection("defaultPrefix"))
.withUpdatableColumns(
"debitorRelUuid",
"billable",
"refundBankAccountUuid",
"vatId",
"vatCountryCode",
"vatBusiness",
"vatReverseCharge",
"defaultPrefix")
.toRole(GLOBAL, ADMIN).grantPermission(INSERT)
.importRootEntityAliasProxy("debitorRel", HsOfficeRelationRbacEntity.class, usingCase(DEBITOR),
directlyFetchedByDependsOnColumn(),
dependsOnColumn("debitorRelUuid"))
.createPermission(DELETE).grantedTo("debitorRel", OWNER)
.createPermission(UPDATE).grantedTo("debitorRel", ADMIN)
.createPermission(SELECT).grantedTo("debitorRel", TENANT)
.importEntityAlias("refundBankAccount", HsOfficeBankAccountEntity.class, usingDefaultCase(),
dependsOnColumn("refundBankAccountUuid"),
directlyFetchedByDependsOnColumn(),
NULLABLE)
.toRole("refundBankAccount", ADMIN).grantRole("debitorRel", AGENT)
.toRole("debitorRel", AGENT).grantRole("refundBankAccount", REFERRER)
.importEntityAlias("partnerRel", HsOfficeRelationRbacEntity.class, usingDefaultCase(),
dependsOnColumn("debitorRelUuid"),
fetchedBySql("""
SELECT ${columns}
FROM hs_office.relation AS partnerRel
JOIN hs_office.relation AS debitorRel
ON debitorRel.type = 'DEBITOR' AND debitorRel.anchorUuid = partnerRel.holderUuid
WHERE partnerRel.type = 'PARTNER'
AND ${REF}.debitorRelUuid = debitorRel.uuid
"""),
NOT_NULL)
.toRole("partnerRel", ADMIN).grantRole("debitorRel", ADMIN)
.toRole("partnerRel", AGENT).grantRole("debitorRel", AGENT)
.toRole("debitorRel", AGENT).grantRole("partnerRel", TENANT)
.declarePlaceholderEntityAliases("partnerPerson", "operationalPerson")
.forExampleRole("partnerPerson", ADMIN).wouldBeGrantedTo("partnerRel", ADMIN)
.forExampleRole("operationalPerson", ADMIN).wouldBeGrantedTo("partnerRel", ADMIN)
.forExampleRole("partnerRel", TENANT).wouldBeGrantedTo("partnerPerson", REFERRER);
}
public static void main(String[] args) throws IOException {
rbac().generateWithBaseFileName("5-hs-office/506-debitor/5063-hs-office-debitor-rbac");
}
}