add refundBankAccount to hs_office_debitor

This commit is contained in:
Michael Hoennig 2022-10-05 17:22:33 +02:00
parent d98b8feaad
commit 6b6f8127bb
16 changed files with 231 additions and 95 deletions

View File

@ -2,8 +2,8 @@ package net.hostsharing.hsadminng.hs.office.debitor;
import net.hostsharing.hsadminng.Mapper; import net.hostsharing.hsadminng.Mapper;
import net.hostsharing.hsadminng.context.Context; import net.hostsharing.hsadminng.context.Context;
import net.hostsharing.hsadminng.hs.office.bankaccount.HsOfficeBankAccountEntity;
import net.hostsharing.hsadminng.hs.office.contact.HsOfficeContactEntity; import net.hostsharing.hsadminng.hs.office.contact.HsOfficeContactEntity;
import net.hostsharing.hsadminng.hs.office.contact.HsOfficeContactRepository;
import net.hostsharing.hsadminng.hs.office.generated.api.v1.api.HsOfficeDebitorsApi; import net.hostsharing.hsadminng.hs.office.generated.api.v1.api.HsOfficeDebitorsApi;
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.*; import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.*;
import net.hostsharing.hsadminng.hs.office.debitor.HsOfficeDebitorEntity; import net.hostsharing.hsadminng.hs.office.debitor.HsOfficeDebitorEntity;
@ -35,12 +35,6 @@ public class HsOfficeDebitorController implements HsOfficeDebitorsApi {
@Autowired @Autowired
private HsOfficeDebitorRepository debitorRepo; private HsOfficeDebitorRepository debitorRepo;
@Autowired
private HsOfficePartnerRepository partnerRepo;
@Autowired
private HsOfficeContactRepository contactRepo;
@Autowired @Autowired
private EntityManager em; private EntityManager em;
@ -141,10 +135,16 @@ public class HsOfficeDebitorController implements HsOfficeDebitorsApi {
final BiConsumer<HsOfficeDebitorEntity, HsOfficeDebitorResource> DEBITOR_ENTITY_TO_RESOURCE_POSTMAPPER = (entity, resource) -> { final BiConsumer<HsOfficeDebitorEntity, HsOfficeDebitorResource> DEBITOR_ENTITY_TO_RESOURCE_POSTMAPPER = (entity, resource) -> {
resource.setPartner(map(entity.getPartner(), HsOfficePartnerResource.class)); resource.setPartner(map(entity.getPartner(), HsOfficePartnerResource.class));
resource.setBillingContact(map(entity.getBillingContact(), HsOfficeContactResource.class)); resource.setBillingContact(map(entity.getBillingContact(), HsOfficeContactResource.class));
if ( entity.getRefundBankAccount() != null ) {
resource.setRefundBankAccount(map(entity.getRefundBankAccount(), HsOfficeBankAccountResource.class));
}
}; };
final BiConsumer<HsOfficeDebitorInsertResource, HsOfficeDebitorEntity> DEBITOR_RESOURCE_TO_ENTITY_POSTMAPPER = (resource, entity) -> { final BiConsumer<HsOfficeDebitorInsertResource, HsOfficeDebitorEntity> DEBITOR_RESOURCE_TO_ENTITY_POSTMAPPER = (resource, entity) -> {
entity.setPartner(em.getReference(HsOfficePartnerEntity.class, resource.getPartnerUuid())); entity.setPartner(em.getReference(HsOfficePartnerEntity.class, resource.getPartnerUuid()));
entity.setBillingContact(em.getReference(HsOfficeContactEntity.class, resource.getBillingContactUuid())); entity.setBillingContact(em.getReference(HsOfficeContactEntity.class, resource.getBillingContactUuid()));
if ( resource.getRefundBankAccountUuid() != null ) {
entity.setRefundBankAccount(em.getReference(HsOfficeBankAccountEntity.class, resource.getRefundBankAccountUuid()));
}
}; };
} }

View File

@ -4,6 +4,7 @@ import lombok.*;
import net.hostsharing.hsadminng.errors.DisplayName; import net.hostsharing.hsadminng.errors.DisplayName;
import net.hostsharing.hsadminng.Stringify; import net.hostsharing.hsadminng.Stringify;
import net.hostsharing.hsadminng.Stringifyable; import net.hostsharing.hsadminng.Stringifyable;
import net.hostsharing.hsadminng.hs.office.bankaccount.HsOfficeBankAccountEntity;
import net.hostsharing.hsadminng.hs.office.contact.HsOfficeContactEntity; import net.hostsharing.hsadminng.hs.office.contact.HsOfficeContactEntity;
import net.hostsharing.hsadminng.hs.office.partner.HsOfficePartnerEntity; import net.hostsharing.hsadminng.hs.office.partner.HsOfficePartnerEntity;
@ -45,6 +46,12 @@ public class HsOfficeDebitorEntity implements Stringifyable {
private @Column(name = "vatcountrycode") String vatCountryCode; private @Column(name = "vatcountrycode") String vatCountryCode;
private @Column(name = "vatbusiness") boolean vatBusiness; private @Column(name = "vatbusiness") boolean vatBusiness;
@ManyToOne
@JoinColumn(name = "refundbankaccountuuid")
private HsOfficeBankAccountEntity refundBankAccount;
@Override @Override
public String toString() { public String toString() {
return stringify.apply(this); return stringify.apply(this);

View File

@ -25,6 +25,8 @@ components:
pattern: '^[A_Z][A-Z]$' pattern: '^[A_Z][A-Z]$'
vatBusiness: vatBusiness:
type: boolean type: boolean
refundBankAccount:
$ref: './hs-office-bankaccount-schemas.yaml#/components/schemas/HsOfficeBankAccount'
HsOfficeDebitorPatch: HsOfficeDebitorPatch:
type: object type: object
@ -43,6 +45,10 @@ components:
vatBusiness: vatBusiness:
type: boolean type: boolean
nullable: true nullable: true
refundBankAccountUuid:
type: string
format: uuid
nullable: true
HsOfficeDebitorInsert: HsOfficeDebitorInsert:
type: object type: object
@ -65,6 +71,9 @@ components:
pattern: '^[A_Z][A-Z]$' pattern: '^[A_Z][A-Z]$'
vatBusiness: vatBusiness:
type: boolean type: boolean
refundBankAccountUuid:
type: string
format: uuid
required: required:
- partnerUuid - partnerUuid
- billingContactUuid - billingContactUuid

View File

@ -22,7 +22,7 @@ declare
contact hs_office_contact; contact hs_office_contact;
begin begin
idName := cleanIdentifier( anchorPersonTradeName|| '-' || holderPersonFamilyName); idName := cleanIdentifier( anchorPersonTradeName || '-' || holderPersonFamilyName);
currentTask := 'creating RBAC test relationship ' || idName; currentTask := 'creating RBAC test relationship ' || idName;
call defineContext(currentTask, null, 'superuser-alex@hostsharing.net', 'global#global.admin'); call defineContext(currentTask, null, 'superuser-alex@hostsharing.net', 'global#global.admin');
execute format('set local hsadminng.currentTask to %L', currentTask); execute format('set local hsadminng.currentTask to %L', currentTask);

View File

@ -0,0 +1,43 @@
### hs_office_bankaccount RBAC Roles
```mermaid
flowchart TB
%% ---------- generated start: ----------
subgraph global
role:global.admin[global.admin]
end
subgraph context
user:current([current])
end
subgraph bankaccount
subgraph roles[ ]
role:bankaccount.owner[[bankaccount.owner]]
role:bankaccount.admin[[bankaccount.admin]]
role:bankaccount.tenant[[bankaccount.tenant]]
end
subgraph perms[ ]
perm:bankaccount.delete{{bankaccount.delete}}
perm:bankaccount.view{{bankaccount.view}}
end
end
%% ---------- generated end. ----------
role:bankaccount.owner --> perm:bankaccount.delete
role:global.admin --> role:bankaccount.owner
user:current --> role:bankaccount.owner
role:bankaccount.owner --> role:bankaccount.admin
role:bankaccount.admin --> role:bankaccount.tenant
role:bankaccount.tenant --> perm:bankaccount.view
```

View File

@ -28,6 +28,7 @@ create or replace function createRbacRolesForHsOfficeBankAccount()
strict as $$ strict as $$
declare declare
ownerRole uuid; ownerRole uuid;
adminRole uuid;
begin begin
if TG_OP <> 'INSERT' then if TG_OP <> 'INSERT' then
raise exception 'invalid usage of TRIGGER AFTER INSERT'; raise exception 'invalid usage of TRIGGER AFTER INSERT';
@ -43,15 +44,22 @@ begin
grantedByRole(globalAdmin()) grantedByRole(globalAdmin())
); );
-- the admin role for those related users who can view the data and related records
adminRole := createRole(
hsOfficeBankAccountAdmin(NEW),
-- Where bankaccounts can be created, assigned, re-assigned and deleted, they cannot be updated.
-- Thus SQL UPDATE and 'edit' permission are being implemented.
withoutPermissions(),
beneathRole(ownerRole)
);
-- TODO.spec: assumption can not be updated -- TODO.spec: assumption can not be updated
-- Where bankaccounts can be created, assigned, re-assigned and deleted, they cannot be updated.
-- Thus SQL UPDATE and 'edit' permission are being implemented.
-- the tenant role for those related users who can view the data -- the tenant role for those related users who can view the data
perform createRole( perform createRole(
hsOfficeBankAccountTenant(NEW), hsOfficeBankAccountTenant(NEW),
grantingPermissions(forObjectUuid => NEW.uuid, permitOps => array ['view']), grantingPermissions(forObjectUuid => NEW.uuid, permitOps => array ['view']),
beneathRole(ownerRole) beneathRole(adminRole)
); );
return NEW; return NEW;

View File

@ -6,14 +6,14 @@
create table hs_office_debitor create table hs_office_debitor
( (
uuid uuid unique references RbacObject (uuid) initially deferred, uuid uuid unique references RbacObject (uuid) initially deferred,
partnerUuid uuid not null references hs_office_partner(uuid), partnerUuid uuid not null references hs_office_partner(uuid),
debitorNumber numeric(5) not null, debitorNumber numeric(5) not null,
billingContactUuid uuid not null references hs_office_contact(uuid), billingContactUuid uuid not null references hs_office_contact(uuid),
vatId varchar(24), -- TODO.spec: here or in person? vatId varchar(24), -- TODO.spec: here or in person?
vatCountryCode varchar(2), vatCountryCode varchar(2),
vatBusiness boolean not null, -- TODO.spec: more of such? vatBusiness boolean not null, -- TODO.spec: more of such?
bankAccountUuid uuid references hs_office_bankaccount(uuid) refundBankAccountUuid uuid references hs_office_bankaccount(uuid)
-- TODO.impl: SEPA-mandate -- TODO.impl: SEPA-mandate
); );
--// --//

View File

@ -4,7 +4,6 @@
flowchart TB; flowchart TB;
subgraph bankaccount; subgraph bankaccount;
direction TB;
%% oversimplified version for now %% oversimplified version for now
%% %%
@ -13,36 +12,36 @@ direction TB;
%% e.g. package admins could see the debitors bank account, %% e.g. package admins could see the debitors bank account,
%% except if we do NOT use the debitor in the hosting super module. %% except if we do NOT use the debitor in the hosting super module.
%% role:bankaccount.owner role:bankaccount.tenant --> perm:bankaccount.view{{bankaccount.view}};
role:bankaccount.owner --> perm:bankaccount.*;
end; end;
subgraph debitor[" "]; subgraph debitor[" "];
direction TB; direction TB;
%% role:debitor.owner role:debitor.owner[[debitor.owner]]
role:debitor.owner --> perm:debitor.*; role:debitor.owner --> perm:debitor.*{{debitor.*}};
role:debitor.owner --> role:bankaccount.owner;
%% role:debitor.admin role:debitor.admin[[debitor.admin]]
role:debitor.admin --> perm:debitor.edit; %% super-roles
role:debitor.owner --> role:debitor.admin; role:debitor.owner --> role:debitor.admin;
role:partner.admin --> role:debitor.admin;
role:person.admin --> role:debitor.admin;
role:contact.admin --> role:debitor.admin;
%% sub-roles
role:debitor.admin --> role:partner.tenant;
role:debitor.admin --> role:person.tenant;
role:debitor.admin --> role:contact.tenant;
role:debitor.admin --> role:bankaccount.tenant;
%% role:debitor.tenant role:debitor.tenant[[debitor.tenant]]
role:debitor.tenant --> perm:debitor.view; role:debitor.tenant --> perm:debitor.view{{debitor.view}};
%% super-roles %% super-roles
role:debitor.admin --> role:debitor.tenant; role:debitor.admin --> role:debitor.tenant;
role:partner.admin --> role:debitor.tenant;
role:person.admin --> role:debitor.tenant;
role:contact.admin --> role:debitor.tenant;
%% sub-roles %% sub-roles
role:debitor.tenant --> role:partner.tenant;
role:debitor.tenant --> role:person.tenant;
role:debitor.tenant --> role:contact.tenant;
end; end;
subgraph global; subgraph global;
direction TB;
role:global.admin --> role:debitor.owner; role:global.admin --> role:debitor.owner;
end; end;

View File

@ -35,6 +35,7 @@ declare
newPerson hs_office_person; newPerson hs_office_person;
oldContact hs_office_contact; oldContact hs_office_contact;
newContact hs_office_contact; newContact hs_office_contact;
newBankAccount hs_office_bankaccount;
begin begin
hsOfficeDebitorTenant := hsOfficeDebitorTenant(NEW); hsOfficeDebitorTenant := hsOfficeDebitorTenant(NEW);
@ -42,7 +43,7 @@ begin
select * from hs_office_partner as p where p.uuid = NEW.partnerUuid into newPartner; select * from hs_office_partner as p where p.uuid = NEW.partnerUuid into newPartner;
select * from hs_office_person as p where p.uuid = newPartner.personUuid into newPerson; select * from hs_office_person as p where p.uuid = newPartner.personUuid into newPerson;
select * from hs_office_contact as c where c.uuid = NEW.billingContactUuid into newContact; select * from hs_office_contact as c where c.uuid = NEW.billingContactUuid into newContact;
select * from hs_office_bankaccount as b where b.uuid = NEW.refundBankAccountUuid into newBankAccount;
if TG_OP = 'INSERT' then if TG_OP = 'INSERT' then
-- the owner role with full access for the global admins -- the owner role with full access for the global admins
@ -55,23 +56,25 @@ begin
-- the admin role with full access for owner -- the admin role with full access for owner
adminRole = createRole( adminRole = createRole(
hsOfficeDebitorAdmin(NEW), hsOfficeDebitorAdmin(NEW),
grantingPermissions(forObjectUuid => NEW.uuid, permitOps => array ['edit']), withoutPermissions(),
beneathRole(ownerRole) beneathRoles(array [
hsOfficeDebitorOwner(NEW),
hsOfficePartnerAdmin(newPartner),
hsOfficePersonAdmin(newPerson),
hsOfficeContactAdmin(newContact),
hsOfficeBankAccountAdmin(newBankAccount)]),
withSubRoles(array [
hsOfficePartnerTenant(newPartner),
hsOfficePersonTenant(newPerson),
hsOfficeContactTenant(newContact),
hsOfficeBankAccountTenant(newBankAccount)])
); );
-- the tenant role for those related users who can view the data -- the tenant role for those related users who can view the data
perform createRole( perform createRole(
hsOfficeDebitorTenant, hsOfficeDebitorTenant,
grantingPermissions(forObjectUuid => NEW.uuid, permitOps => array ['view']), grantingPermissions(forObjectUuid => NEW.uuid, permitOps => array ['view']),
beneathRoles(array[ beneathRole(hsOfficeDebitorAdmin(NEW))
hsOfficeDebitorAdmin(NEW),
hsOfficePartnerAdmin(newPartner),
hsOfficePersonAdmin(newPerson),
hsOfficeContactAdmin(newContact)]),
withSubRoles(array[
hsOfficePartnerTenant(newPartner),
hsOfficePersonTenant(newPerson),
hsOfficeContactTenant(newContact)])
); );
elsif TG_OP = 'UPDATE' then elsif TG_OP = 'UPDATE' then
@ -79,21 +82,23 @@ begin
if OLD.partnerUuid <> NEW.partnerUuid then if OLD.partnerUuid <> NEW.partnerUuid then
select * from hs_office_partner as p where p.uuid = OLD.partnerUuid into oldPartner; select * from hs_office_partner as p where p.uuid = OLD.partnerUuid into oldPartner;
call revokeRoleFromRole( hsOfficeDebitorTenant, hsOfficePartnerAdmin(oldPartner) ); call revokeRoleFromRole(hsOfficeDebitorAdmin(OLD), hsOfficePartnerAdmin(oldPartner));
call grantRoleToRole( hsOfficeDebitorTenant, hsOfficePartnerAdmin(newPartner) ); call grantRoleToRole(hsOfficeDebitorAdmin(NEW), hsOfficePartnerAdmin(newPartner));
call revokeRoleFromRole( hsOfficePartnerTenant(oldPartner), hsOfficeDebitorTenant ); call revokeRoleFromRole(hsOfficePartnerTenant(oldPartner), hsOfficeDebitorAdmin(OLD));
call grantRoleToRole( hsOfficePartnerTenant(newPartner), hsOfficeDebitorTenant ); call grantRoleToRole(hsOfficePartnerTenant(newPartner), hsOfficeDebitorAdmin(NEW));
-- TODO: What about the person of the partner? And what if the person of the partner changes?
end if; end if;
if OLD.billingContactUuid <> NEW.billingContactUuid then if OLD.billingContactUuid <> NEW.billingContactUuid then
select * from hs_office_contact as c where c.uuid = OLD.billingContactUuid into oldContact; select * from hs_office_contact as c where c.uuid = OLD.billingContactUuid into oldContact;
call revokeRoleFromRole( hsOfficeDebitorTenant, hsOfficeContactAdmin(oldContact) ); call revokeRoleFromRole(hsOfficeDebitorAdmin(OLD), hsOfficeContactAdmin(oldContact));
call grantRoleToRole( hsOfficeDebitorTenant, hsOfficeContactAdmin(newContact) ); call grantRoleToRole(hsOfficeDebitorAdmin(NEW), hsOfficeContactAdmin(newContact));
call revokeRoleFromRole( hsOfficeContactTenant(oldContact), hsOfficeDebitorTenant ); call revokeRoleFromRole(hsOfficeContactTenant(oldContact), hsOfficeDebitorAdmin(OLD));
call grantRoleToRole( hsOfficeContactTenant(newContact), hsOfficeDebitorTenant ); call grantRoleToRole(hsOfficeContactTenant(newContact), hsOfficeDebitorAdmin(NEW));
end if; end if;
else else
raise exception 'invalid usage of TRIGGER'; raise exception 'invalid usage of TRIGGER';
@ -136,8 +141,8 @@ call generateRbacIdentityView('hs_office_debitor', $idName$
--changeset hs-office-debitor-rbac-RESTRICTED-VIEW:1 endDelimiter:--// --changeset hs-office-debitor-rbac-RESTRICTED-VIEW:1 endDelimiter:--//
-- ---------------------------------------------------------------------------- -- ----------------------------------------------------------------------------
call generateRbacRestrictedView('hs_office_debitor', call generateRbacRestrictedView('hs_office_debitor',
'target.debitorNumber', 'target.debitorNumber',
$updates$ $updates$
billingContactUuid = new.billingContactUuid, billingContactUuid = new.billingContactUuid,
vatId = new.vatId, vatId = new.vatId,
vatCountryCode = new.vatCountryCode, vatCountryCode = new.vatCountryCode,
@ -153,9 +158,9 @@ call generateRbacRestrictedView('hs_office_debitor',
*/ */
do language plpgsql $$ do language plpgsql $$
declare declare
addDebitorPermissions uuid[]; addDebitorPermissions uuid[];
globalObjectUuid uuid; globalObjectUuid uuid;
globalAdminRoleUuid uuid ; globalAdminRoleUuid uuid ;
begin begin
call defineContext('granting global new-debitor permission to global admin role', null, null, null); call defineContext('granting global new-debitor permission to global admin role', null, null, null);

View File

@ -11,11 +11,12 @@
create or replace procedure createHsOfficeDebitorTestData( partnerTradeName varchar, billingContactLabel varchar ) create or replace procedure createHsOfficeDebitorTestData( partnerTradeName varchar, billingContactLabel varchar )
language plpgsql as $$ language plpgsql as $$
declare declare
currentTask varchar; currentTask varchar;
idName varchar; idName varchar;
relatedPartner hs_office_partner; relatedPartner hs_office_partner;
relatedContact hs_office_contact; relatedContact hs_office_contact;
newDebitorNumber numeric(6); relatedBankAccountUuid uuid;
newDebitorNumber numeric(6);
begin begin
idName := cleanIdentifier( partnerTradeName|| '-' || billingContactLabel); idName := cleanIdentifier( partnerTradeName|| '-' || billingContactLabel);
currentTask := 'creating RBAC test debitor ' || idName; currentTask := 'creating RBAC test debitor ' || idName;
@ -26,14 +27,15 @@ begin
join hs_office_person person on person.uuid = partner.personUuid join hs_office_person person on person.uuid = partner.personUuid
where person.tradeName = partnerTradeName into relatedPartner; where person.tradeName = partnerTradeName into relatedPartner;
select c.* from hs_office_contact c where c.label = billingContactLabel into relatedContact; select c.* from hs_office_contact c where c.label = billingContactLabel into relatedContact;
select b.uuid from hs_office_bankaccount b where b.holder = partnerTradeName into relatedBankAccountUuid;
select coalesce(max(debitorNumber)+1, 10001) from hs_office_debitor into newDebitorNumber; select coalesce(max(debitorNumber)+1, 10001) from hs_office_debitor into newDebitorNumber;
raise notice 'creating test debitor: % (#%)', idName, newDebitorNumber; raise notice 'creating test debitor: % (#%)', idName, newDebitorNumber;
raise notice '- using partner (%): %', relatedPartner.uuid, relatedPartner; raise notice '- using partner (%): %', relatedPartner.uuid, relatedPartner;
raise notice '- using billingContact (%): %', relatedContact.uuid, relatedContact; raise notice '- using billingContact (%): %', relatedContact.uuid, relatedContact;
insert insert
into hs_office_debitor (uuid, partneruuid, debitornumber, billingcontactuuid, vatbusiness) into hs_office_debitor (uuid, partneruuid, debitornumber, billingcontactuuid, vatbusiness, refundbankaccountuuid)
values (uuid_generate_v4(), relatedPartner.uuid, newDebitorNumber, relatedContact.uuid, true); values (uuid_generate_v4(), relatedPartner.uuid, newDebitorNumber, relatedContact.uuid, true, relatedBankAccountUuid);
end; $$; end; $$;
--// --//

View File

@ -343,6 +343,12 @@ class HsOfficeBankAccountControllerAcceptanceTest {
@BeforeEach @BeforeEach
@AfterEach @AfterEach
void cleanup() { void cleanup() {
jpaAttempt.transacted(() -> {
context.define("superuser-alex@hostsharing.net", null);
tempBankAccountUuids.addAll(
bankAccountRepo.findByOptionalHolderLike("some temp acc").stream().map(HsOfficeBankAccountEntity::getUuid).toList()
);
});
tempBankAccountUuids.forEach(uuid -> { tempBankAccountUuids.forEach(uuid -> {
jpaAttempt.transacted(() -> { jpaAttempt.transacted(() -> {
context.define("superuser-alex@hostsharing.net", null); context.define("superuser-alex@hostsharing.net", null);

View File

@ -109,13 +109,15 @@ class HsOfficeBankAccountRepositoryIntegrationTest extends ContextBasedTest {
assertThat(roleNamesOf(roles)).containsExactlyInAnyOrder(Array.from( assertThat(roleNamesOf(roles)).containsExactlyInAnyOrder(Array.from(
initialRoleNames, initialRoleNames,
"hs_office_bankaccount#sometempaccC.owner", "hs_office_bankaccount#sometempaccC.owner",
"hs_office_bankaccount#sometempaccC.admin",
"hs_office_bankaccount#sometempaccC.tenant" "hs_office_bankaccount#sometempaccC.tenant"
)); ));
assertThat(grantDisplaysOf(rawGrantRepo.findAll())).containsExactlyInAnyOrder(Array.from( assertThat(grantDisplaysOf(rawGrantRepo.findAll())).containsExactlyInAnyOrder(Array.from(
initialGrantNames, initialGrantNames,
"{ grant role hs_office_bankaccount#sometempaccC.owner to role global#global.admin by system and assume }", "{ grant role hs_office_bankaccount#sometempaccC.owner to role global#global.admin by system and assume }",
"{ grant perm delete on hs_office_bankaccount#sometempaccC to role hs_office_bankaccount#sometempaccC.owner by system and assume }", "{ grant perm delete on hs_office_bankaccount#sometempaccC to role hs_office_bankaccount#sometempaccC.owner by system and assume }",
"{ grant role hs_office_bankaccount#sometempaccC.tenant to role hs_office_bankaccount#sometempaccC.owner by system and assume }", "{ grant role hs_office_bankaccount#sometempaccC.tenant to role hs_office_bankaccount#sometempaccC.admin by system and assume }",
"{ grant role hs_office_bankaccount#sometempaccC.admin to role hs_office_bankaccount#sometempaccC.owner by system and assume }",
"{ grant perm view on hs_office_bankaccount#sometempaccC to role hs_office_bankaccount#sometempaccC.tenant by system and assume }", "{ grant perm view on hs_office_bankaccount#sometempaccC to role hs_office_bankaccount#sometempaccC.tenant by system and assume }",
"{ grant role hs_office_bankaccount#sometempaccC.owner to user selfregistered-user-drew@hostsharing.org by global#global.admin and assume }" "{ grant role hs_office_bankaccount#sometempaccC.owner to user selfregistered-user-drew@hostsharing.org by global#global.admin and assume }"
)); ));
@ -253,9 +255,9 @@ class HsOfficeBankAccountRepositoryIntegrationTest extends ContextBasedTest {
final var initialGrantNames = grantDisplaysOf(rawGrantRepo.findAll()); final var initialGrantNames = grantDisplaysOf(rawGrantRepo.findAll());
final var givenBankAccount = givenSomeTemporaryBankAccount("selfregistered-user-drew@hostsharing.org"); final var givenBankAccount = givenSomeTemporaryBankAccount("selfregistered-user-drew@hostsharing.org");
assertThat(rawRoleRepo.findAll().size()).as("unexpected number of roles created") assertThat(rawRoleRepo.findAll().size()).as("unexpected number of roles created")
.isEqualTo(initialRoleNames.size() + 2); .isEqualTo(initialRoleNames.size() + 3);
assertThat(rawGrantRepo.findAll().size()).as("unexpected number of grants created") assertThat(rawGrantRepo.findAll().size()).as("unexpected number of grants created")
.isEqualTo(initialGrantNames.size() + 5); .isEqualTo(initialGrantNames.size() + 6);
// when // when
final var result = jpaAttempt.transacted(() -> { final var result = jpaAttempt.transacted(() -> {

View File

@ -8,7 +8,9 @@ import net.hostsharing.hsadminng.context.Context;
import net.hostsharing.test.JpaAttempt; import net.hostsharing.test.JpaAttempt;
import org.apache.commons.lang3.RandomStringUtils; import org.apache.commons.lang3.RandomStringUtils;
import org.json.JSONException; import org.json.JSONException;
import org.junit.Before;
import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
@ -363,6 +365,7 @@ class HsOfficeContactControllerAcceptanceTest {
return tempContactUuid; return tempContactUuid;
} }
@BeforeEach
@AfterEach @AfterEach
void cleanup() { void cleanup() {
tempContactUuids.forEach(uuid -> { tempContactUuids.forEach(uuid -> {

View File

@ -5,6 +5,7 @@ import io.restassured.http.ContentType;
import net.hostsharing.hsadminng.Accepts; import net.hostsharing.hsadminng.Accepts;
import net.hostsharing.hsadminng.HsadminNgApplication; import net.hostsharing.hsadminng.HsadminNgApplication;
import net.hostsharing.hsadminng.context.Context; import net.hostsharing.hsadminng.context.Context;
import net.hostsharing.hsadminng.hs.office.bankaccount.HsOfficeBankAccountRepository;
import net.hostsharing.hsadminng.hs.office.contact.HsOfficeContactRepository; import net.hostsharing.hsadminng.hs.office.contact.HsOfficeContactRepository;
import net.hostsharing.hsadminng.hs.office.partner.HsOfficePartnerRepository; import net.hostsharing.hsadminng.hs.office.partner.HsOfficePartnerRepository;
import net.hostsharing.test.JpaAttempt; import net.hostsharing.test.JpaAttempt;
@ -55,6 +56,9 @@ class HsOfficeDebitorControllerAcceptanceTest {
@Autowired @Autowired
HsOfficeContactRepository contactRepo; HsOfficeContactRepository contactRepo;
@Autowired
HsOfficeBankAccountRepository bankAccountRepo;
@Autowired @Autowired
JpaAttempt jpaAttempt; JpaAttempt jpaAttempt;
@ -84,7 +88,8 @@ class HsOfficeDebitorControllerAcceptanceTest {
"billingContact": { "label": "first contact" }, "billingContact": { "label": "first contact" },
"vatId": null, "vatId": null,
"vatCountryCode": null, "vatCountryCode": null,
"vatBusiness": true "vatBusiness": true,
"refundBankAccount": { "holder": "First GmbH" }
}, },
{ {
"debitorNumber": 10002, "debitorNumber": 10002,
@ -92,7 +97,8 @@ class HsOfficeDebitorControllerAcceptanceTest {
"billingContact": { "label": "second contact" }, "billingContact": { "label": "second contact" },
"vatId": null, "vatId": null,
"vatCountryCode": null, "vatCountryCode": null,
"vatBusiness": true "vatBusiness": true,
"refundBankAccount": { "holder": "Second e.K." }
}, },
{ {
"debitorNumber": 10003, "debitorNumber": 10003,
@ -100,7 +106,8 @@ class HsOfficeDebitorControllerAcceptanceTest {
"billingContact": { "label": "third contact" }, "billingContact": { "label": "third contact" },
"vatId": null, "vatId": null,
"vatCountryCode": null, "vatCountryCode": null,
"vatBusiness": true "vatBusiness": true,
"refundBankAccount": { "holder": "Third OHG" }
} }
] ]
""")); """));
@ -137,14 +144,15 @@ class HsOfficeDebitorControllerAcceptanceTest {
@Nested @Nested
@Accepts({ "Debitor:C(Create)" }) @Accepts({ "Debitor:C(Create)" })
class AddDebitor { class CreateDebitor {
@Test @Test
void globalAdmin_withoutAssumedRole_canAddDebitor() { void globalAdmin_withoutAssumedRole_canAddDebitorWithBankAccount() {
context.define("superuser-alex@hostsharing.net"); context.define("superuser-alex@hostsharing.net");
final var givenPartner = partnerRepo.findPartnerByOptionalNameLike("Third").get(0); final var givenPartner = partnerRepo.findPartnerByOptionalNameLike("Third").get(0);
final var givenContact = contactRepo.findContactByOptionalLabelLike("forth").get(0); final var givenContact = contactRepo.findContactByOptionalLabelLike("forth").get(0);
final var givenBankAccount = bankAccountRepo.findByOptionalHolderLike("Fourth").get(0);
final var location = RestAssured // @formatter:off final var location = RestAssured // @formatter:off
.given() .given()
@ -157,9 +165,10 @@ class HsOfficeDebitorControllerAcceptanceTest {
"debitorNumber": "%s", "debitorNumber": "%s",
"vatId": "VAT123456", "vatId": "VAT123456",
"vatCountryCode": "DE", "vatCountryCode": "DE",
"vatBusiness": true "vatBusiness": true,
"refundBankAccountUuid": "%s"
} }
""".formatted( givenPartner.getUuid(), givenContact.getUuid(), nextDebitorNumber++)) """.formatted( givenPartner.getUuid(), givenContact.getUuid(), nextDebitorNumber++, givenBankAccount.getUuid()))
.port(port) .port(port)
.when() .when()
.post("http://localhost/api/hs/office/debitors") .post("http://localhost/api/hs/office/debitors")
@ -170,6 +179,7 @@ class HsOfficeDebitorControllerAcceptanceTest {
.body("vatId", is("VAT123456")) .body("vatId", is("VAT123456"))
.body("billingContact.label", is(givenContact.getLabel())) .body("billingContact.label", is(givenContact.getLabel()))
.body("partner.person.tradeName", is(givenPartner.getPerson().getTradeName())) .body("partner.person.tradeName", is(givenPartner.getPerson().getTradeName()))
.body("refundBankAccount.holder", is(givenBankAccount.getHolder()))
.header("Location", startsWith("http://localhost")) .header("Location", startsWith("http://localhost"))
.extract().header("Location"); // @formatter:on .extract().header("Location"); // @formatter:on
@ -179,6 +189,46 @@ class HsOfficeDebitorControllerAcceptanceTest {
assertThat(newUserUuid).isNotNull(); assertThat(newUserUuid).isNotNull();
} }
@Test
void globalAdmin_withoutAssumedRole_canAddDebitorWithoutBankAccount() {
context.define("superuser-alex@hostsharing.net");
final var givenPartner = partnerRepo.findPartnerByOptionalNameLike("Third").get(0);
final var givenContact = contactRepo.findContactByOptionalLabelLike("forth").get(0);
final var location = RestAssured // @formatter:off
.given()
.header("current-user", "superuser-alex@hostsharing.net")
.contentType(ContentType.JSON)
.body("""
{
"partnerUuid": "%s",
"billingContactUuid": "%s",
"debitorNumber": "%s",
"vatId": "VAT123456",
"vatCountryCode": "DE",
"vatBusiness": true
}
""".formatted( givenPartner.getUuid(), givenContact.getUuid(), nextDebitorNumber++))
.port(port)
.when()
.post("http://localhost/api/hs/office/debitors")
.then().log().all().assertThat()
.statusCode(201)
.contentType(ContentType.JSON)
.body("uuid", isUuidValid())
.body("vatId", is("VAT123456"))
.body("billingContact.label", is(givenContact.getLabel()))
.body("partner.person.tradeName", is(givenPartner.getPerson().getTradeName()))
.header("Location", startsWith("http://localhost"))
.extract().header("Location"); // @formatter:on
// finally, the new debitor can be accessed under the generated UUID
final var newUserUuid = toCleanup(UUID.fromString(
location.substring(location.lastIndexOf('/') + 1)));
assertThat(newUserUuid).isNotNull();
}
@Test @Test
void globalAdmin_canNotAddDebitor_ifContactDoesNotExist() { void globalAdmin_canNotAddDebitor_ifContactDoesNotExist() {
@ -300,7 +350,8 @@ class HsOfficeDebitorControllerAcceptanceTest {
.body("", lenientlyEquals(""" .body("", lenientlyEquals("""
{ {
"partner": { person: { "tradeName": "First GmbH" } }, "partner": { person: { "tradeName": "First GmbH" } },
"billingContact": { "label": "first contact" } "billingContact": { "label": "first contact" },
"refundBankAccount": { "holder": "First GmbH" }
} }
""")); // @formatter:on """)); // @formatter:on
} }

View File

@ -122,21 +122,19 @@ class HsOfficeDebitorRepositoryIntegrationTest extends ContextBasedTest {
"hs_office_debitor#20002Fourthe.G.-forthcontact.tenant")); "hs_office_debitor#20002Fourthe.G.-forthcontact.tenant"));
assertThat(grantDisplaysOf(rawGrantRepo.findAll())).containsExactlyInAnyOrder(Array.fromSkippingNull( assertThat(grantDisplaysOf(rawGrantRepo.findAll())).containsExactlyInAnyOrder(Array.fromSkippingNull(
initialGrantNames, initialGrantNames,
"{ grant perm * on hs_office_debitor#20002Fourthe.G.-forthcontact to role hs_office_debitor#20002Fourthe.G.-forthcontact.owner by system and assume }",
"{ grant role hs_office_debitor#20002Fourthe.G.-forthcontact.owner to role global#global.admin by system and assume }",
"{ grant perm * on hs_office_debitor#20002Fourthe.G.-forthcontact to role hs_office_debitor#20002Fourthe.G.-forthcontact.owner by system and assume }", "{ grant role hs_office_debitor#20002Fourthe.G.-forthcontact.admin to role hs_office_debitor#20002Fourthe.G.-forthcontact.owner by system and assume }",
"{ grant role hs_office_debitor#20002Fourthe.G.-forthcontact.owner to role global#global.admin by system and assume }", "{ grant role hs_office_debitor#20002Fourthe.G.-forthcontact.admin to role hs_office_partner#Fourthe.G.-forthcontact.admin by system and assume }",
"{ grant role hs_office_debitor#20002Fourthe.G.-forthcontact.admin to role hs_office_person#Fourthe.G..admin by system and assume }",
"{ grant role hs_office_debitor#20002Fourthe.G.-forthcontact.admin to role hs_office_contact#forthcontact.admin by system and assume }",
"{ grant role hs_office_contact#forthcontact.tenant to role hs_office_debitor#20002Fourthe.G.-forthcontact.admin by system and assume }",
"{ grant role hs_office_partner#Fourthe.G.-forthcontact.tenant to role hs_office_debitor#20002Fourthe.G.-forthcontact.admin by system and assume }",
"{ grant role hs_office_person#Fourthe.G..tenant to role hs_office_debitor#20002Fourthe.G.-forthcontact.admin by system and assume }",
"{ grant role hs_office_debitor#20002Fourthe.G.-forthcontact.tenant to role hs_office_debitor#20002Fourthe.G.-forthcontact.admin by system and assume }",
"{ grant perm edit on hs_office_debitor#20002Fourthe.G.-forthcontact to role hs_office_debitor#20002Fourthe.G.-forthcontact.admin by system and assume }", "{ grant perm view on hs_office_debitor#20002Fourthe.G.-forthcontact to role hs_office_debitor#20002Fourthe.G.-forthcontact.tenant by system and assume }",
"{ grant role hs_office_debitor#20002Fourthe.G.-forthcontact.admin to role hs_office_debitor#20002Fourthe.G.-forthcontact.owner by system and assume }",
"{ grant perm view on hs_office_debitor#20002Fourthe.G.-forthcontact to role hs_office_debitor#20002Fourthe.G.-forthcontact.tenant by system and assume }",
"{ grant role hs_office_debitor#20002Fourthe.G.-forthcontact.tenant to role hs_office_contact#forthcontact.admin by system and assume }",
"{ grant role hs_office_debitor#20002Fourthe.G.-forthcontact.tenant to role hs_office_debitor#20002Fourthe.G.-forthcontact.admin by system and assume }",
"{ grant role hs_office_debitor#20002Fourthe.G.-forthcontact.tenant to role hs_office_partner#Fourthe.G.-forthcontact.admin by system and assume }",
"{ grant role hs_office_debitor#20002Fourthe.G.-forthcontact.tenant to role hs_office_person#Fourthe.G..admin by system and assume }",
"{ grant role hs_office_partner#Fourthe.G.-forthcontact.tenant to role hs_office_debitor#20002Fourthe.G.-forthcontact.tenant by system and assume }",
"{ grant role hs_office_contact#forthcontact.tenant to role hs_office_debitor#20002Fourthe.G.-forthcontact.tenant by system and assume }",
"{ grant role hs_office_person#Fourthe.G..tenant to role hs_office_debitor#20002Fourthe.G.-forthcontact.tenant by system and assume }",
null)); null));
} }
@ -394,7 +392,7 @@ class HsOfficeDebitorRepositoryIntegrationTest extends ContextBasedTest {
assertThat(rawRoleRepo.findAll().size()).as("precondition failed: unexpected number of roles created") assertThat(rawRoleRepo.findAll().size()).as("precondition failed: unexpected number of roles created")
.isEqualTo(initialRoleNames.length + 3); .isEqualTo(initialRoleNames.length + 3);
assertThat(rawGrantRepo.findAll().size()).as("precondition failed: unexpected number of grants created") assertThat(rawGrantRepo.findAll().size()).as("precondition failed: unexpected number of grants created")
.isEqualTo(initialGrantNames.length + 12); .isEqualTo(initialGrantNames.length + 11);
// when // when
final var result = jpaAttempt.transacted(() -> { final var result = jpaAttempt.transacted(() -> {

View File

@ -24,7 +24,10 @@ public class Array {
public static String[] fromSkippingNull(final List<String> initialList, final String... additionalStrings) { public static String[] fromSkippingNull(final List<String> initialList, final String... additionalStrings) {
final var resultList = new ArrayList<>(initialList); final var resultList = new ArrayList<>(initialList);
resultList.addAll(Arrays.stream(additionalStrings).filter(Objects::nonNull).toList()); resultList.addAll(Arrays.stream(additionalStrings)
.filter(Objects::nonNull)
.map(s -> s.replaceAll(" *", " "))
.toList());
return resultList.toArray(String[]::new); return resultList.toArray(String[]::new);
} }