better error handling + requiresExactlyOneOf implemented

This commit is contained in:
Michael Hoennig 2024-07-20 13:17:46 +02:00
parent 7daf57513e
commit b012225c8b
20 changed files with 182 additions and 71 deletions

View File

@ -43,7 +43,10 @@ postgresAutodoc () {
alias postgres-autodoc=postgresAutodoc alias postgres-autodoc=postgresAutodoc
function importLegacyData() { function importLegacyData() {
set target=$1 export target=$1
if [ -z "$target" ]; then
echo "importLegacyData needs target argument, but none was given" >&2
else
source .tc-environment source .tc-environment
if [ -f .environment ]; then if [ -f .environment ]; then
@ -51,9 +54,13 @@ function importLegacyData() {
fi fi
echo "using environment (with ending ';' for use in IntelliJ IDEA):" echo "using environment (with ending ';' for use in IntelliJ IDEA):"
echo "--- BEGIN: ---"
set | grep ^HSADMINNG_ | sed 's/$/;/' set | grep ^HSADMINNG_ | sed 's/$/;/'
echo "---- END. ----\n"
echo ./gradlew $target --rerun
./gradlew $target --rerun ./gradlew $target --rerun
fi
} }
alias gw-importOfficeData='importLegacyData importOfficeData' alias gw-importOfficeData='importLegacyData importOfficeData'
alias gw-importHostingAssets='importLegacyData importHostingAssets' alias gw-importHostingAssets='importLegacyData importHostingAssets'

View File

@ -318,7 +318,7 @@ jacocoTestCoverageVerification {
tasks.register('importOfficeData', Test) { tasks.register('importOfficeData', Test) {
useJUnitPlatform { useJUnitPlatform {
includeTags 'import' includeTags 'importOfficeData'
} }
group 'verification' group 'verification'
@ -327,6 +327,16 @@ tasks.register('importOfficeData', Test) {
mustRunAfter spotlessJava mustRunAfter spotlessJava
} }
tasks.register('importHostingAssets', Test) {
useJUnitPlatform {
includeTags 'importHostingAssets'
}
group 'verification'
description 'run the import jobs as tests'
mustRunAfter spotlessJava
}
// pitest mutation testing // pitest mutation testing
pitest { pitest {

View File

@ -22,6 +22,10 @@ public class HsBookingItemEntityValidator extends HsEntityValidator<HsBookingIte
@Override @Override
public List<String> validateEntity(final HsBookingItemEntity bookingItem) { public List<String> validateEntity(final HsBookingItemEntity bookingItem) {
// TODO.impl: HsBookingItemType could do this similar to HsHostingAssetType
if ( bookingItem.getParentItem() == null && bookingItem.getProject() == null) {
return List.of(bookingItem + ".'parentItem' or .'project' expected to be set, but both are null");
}
return enrich(prefix(bookingItem.toShortString(), "resources"), super.validateProperties(bookingItem)); return enrich(prefix(bookingItem.toShortString(), "resources"), super.validateProperties(bookingItem));
} }

View File

@ -13,10 +13,10 @@ class HsCloudServerBookingItemValidator extends HsBookingItemEntityValidator {
integerProperty("CPU") .min( 1) .max( 32) .required(), integerProperty("CPU") .min( 1) .max( 32) .required(),
integerProperty("RAM").unit("GB") .min( 1) .max( 8192) .required(), integerProperty("RAM").unit("GB") .min( 1) .max( 8192) .required(),
integerProperty("SSD").unit("GB") .min( 0) .max( 1000) .step(25).required(), integerProperty("SSD").unit("GB") .min( 0) .max( 1000) .step(25).requiresExactlyOneOf("SDD", "HDD").withDefault(0),
integerProperty("HDD").unit("GB") .min( 0) .max( 4000) .step(250).withDefault(0), integerProperty("HDD").unit("GB") .min( 0) .max( 4000) .step(250).requiresExactlyOneOf("SSD", "HDD").withDefault(0),
integerProperty("Traffic").unit("GB") .min(250) .max(10000) .step(250).required(), integerProperty("Traffic").unit("GB") .min(250) .max(10000) .step(250).requiresAtMaxOneOf("Bandwidth", "Traffic"),
integerProperty("Bandwidth").unit("GB") .min(250) .max(10000) .step(250).optional(), // TODO.spec integerProperty("Bandwidth").unit("GB") .min(250) .max(10000) .step(250).requiresAtMaxOneOf("Bandwidth", "Traffic"), // TODO.spec
enumerationProperty("SLA-Infrastructure").values("BASIC", "EXT8H", "EXT4H", "EXT2H").optional() enumerationProperty("SLA-Infrastructure").values("BASIC", "EXT8H", "EXT4H", "EXT2H").optional()
// @formatter:on // @formatter:on

View File

@ -12,10 +12,10 @@ class HsManagedServerBookingItemValidator extends HsBookingItemEntityValidator {
super( super(
integerProperty("CPU").min(1).max(32).required(), integerProperty("CPU").min(1).max(32).required(),
integerProperty("RAM").unit("GB").min(1).max(128).required(), integerProperty("RAM").unit("GB").min(1).max(128).required(),
integerProperty("SSD").unit("GB").min(25).max(2000).step(25).required().asTotalLimit().withThreshold(200), integerProperty("SSD").unit("GB").min(25).max(2000).step(25).requiresExactlyOneOf("SSD", "HDD").withDefault(0).asTotalLimit().withThreshold(200),
integerProperty("HDD").unit("GB").min(0).max(10000).step(250).withDefault(0).asTotalLimit().withThreshold(200), integerProperty("HDD").unit("GB").min(0).max(10000).step(250).requiresExactlyOneOf("SSD", "HDD").withDefault(0).asTotalLimit().withThreshold(200),
integerProperty("Traffic").unit("GB").min(250).max(64000).step(250).required().asTotalLimit().withThreshold(200), integerProperty("Traffic").unit("GB").min(250).max(64000).step(250).requiresAtMaxOneOf("Bandwidth", "Traffic").asTotalLimit().withThreshold(200),
integerProperty("Bandwidth").unit("GB").min(250).max(64000).step(250).optional().asTotalLimit().withThreshold(200), // TODO.spec integerProperty("Bandwidth").unit("GB").min(250).max(64000).step(250).requiresAtMaxOneOf("Bandwidth", "Traffic").asTotalLimit().withThreshold(200), // TODO.spec
enumerationProperty("SLA-Platform").values("BASIC", "EXT8H", "EXT4H", "EXT2H").withDefault("BASIC"), enumerationProperty("SLA-Platform").values("BASIC", "EXT8H", "EXT4H", "EXT2H").withDefault("BASIC"),
booleanProperty("SLA-EMail").falseIf("SLA-Platform", "BASIC").withDefault(false), booleanProperty("SLA-EMail").falseIf("SLA-Platform", "BASIC").withDefault(false),
booleanProperty("SLA-Maria").falseIf("SLA-Platform", "BASIC").optional(), booleanProperty("SLA-Maria").falseIf("SLA-Platform", "BASIC").optional(),

View File

@ -25,8 +25,8 @@ class HsManagedWebspaceBookingItemValidator extends HsBookingItemEntityValidator
super( super(
integerProperty("SSD").unit("GB").min(1).max(2000).step(1).required(), integerProperty("SSD").unit("GB").min(1).max(2000).step(1).required(),
integerProperty("HDD").unit("GB").min(0).max(10000).step(10).optional(), integerProperty("HDD").unit("GB").min(0).max(10000).step(10).optional(),
integerProperty("Traffic").unit("GB").min(10).max(64000).step(10).required(), integerProperty("Traffic").unit("GB").min(10).max(64000).step(10).requiresAtMaxOneOf("Bandwidth", "Traffic"),
integerProperty("Bandwidth").unit("GB").min(10).max(1000).step(10).optional(), // TODO.spec integerProperty("Bandwidth").unit("GB").min(10).max(1000).step(10).requiresAtMaxOneOf("Bandwidth", "Traffic"), // TODO.spec
integerProperty("Multi").min(1).max(100).step(1).withDefault(1) integerProperty("Multi").min(1).max(100).step(1).withDefault(1)
.eachComprising( 25, unixUsers()) .eachComprising( 25, unixUsers())
.eachComprising( 5, databaseUsers()) .eachComprising( 5, databaseUsers())

View File

@ -9,10 +9,10 @@ class HsPrivateCloudBookingItemValidator extends HsBookingItemEntityValidator {
// @formatter:off // @formatter:off
integerProperty("CPU") .min( 1).max( 128).required().asTotalLimit(), integerProperty("CPU") .min( 1).max( 128).required().asTotalLimit(),
integerProperty("RAM").unit("GB") .min( 1).max( 512).required().asTotalLimit(), integerProperty("RAM").unit("GB") .min( 1).max( 512).required().asTotalLimit(),
integerProperty("SSD").unit("GB") .min( 25).max( 4000).step(25).required().asTotalLimit(), integerProperty("SSD").unit("GB") .min( 25).max( 4000).step(25).requiresExactlyOneOf("SSD", "HDD").withDefault(0).asTotalLimit(),
integerProperty("HDD").unit("GB") .min( 0).max(16000).step(250).withDefault(0).asTotalLimit(), integerProperty("HDD").unit("GB") .min( 0).max(16000).step(250).requiresExactlyOneOf("SSD", "HDD").withDefault(0).asTotalLimit(),
integerProperty("Traffic").unit("GB") .min(250).max(64000).step(250).required().asTotalLimit(), integerProperty("Traffic").unit("GB") .min(250).max(64000).step(250).requiresAtMaxOneOf("Bandwidth", "Traffic").asTotalLimit(),
integerProperty("Bandwidth").unit("GB") .min(250).max(64000).step(250).optional().asTotalLimit(), // TODO.spec integerProperty("Bandwidth").unit("GB") .min(250).max(64000).step(250).requiresAtMaxOneOf("Bandwidth", "Traffic").asTotalLimit(), // TODO.spec
// Alternatively we could specify it similarly to "Multi" option but exclusively counting: // Alternatively we could specify it similarly to "Multi" option but exclusively counting:
// integerProperty("Resource-Points") .min(4).max(100).required() // integerProperty("Resource-Points") .min(4).max(100).required()

View File

@ -68,7 +68,7 @@ public class HsOfficeMembershipEntity implements RbacObject, Stringifyable {
private static Stringify<HsOfficeMembershipEntity> stringify = stringify(HsOfficeMembershipEntity.class) private static Stringify<HsOfficeMembershipEntity> stringify = stringify(HsOfficeMembershipEntity.class)
.withProp(e -> MEMBER_NUMBER_TAG + e.getMemberNumber()) .withProp(e -> MEMBER_NUMBER_TAG + e.getMemberNumber())
.withProp(e -> e.getPartner().toShortString()) .withProp(HsOfficeMembershipEntity::getPartner)
.withProp(e -> e.getValidity().asString()) .withProp(e -> e.getValidity().asString())
.withProp(HsOfficeMembershipEntity::getStatus) .withProp(HsOfficeMembershipEntity::getStatus)
.quotedValues(false); .quotedValues(false);

View File

@ -13,10 +13,12 @@ import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collection; import java.util.Collection;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Objects; import java.util.Objects;
import java.util.Optional; import java.util.Optional;
import java.util.Set;
import java.util.function.Function; import java.util.function.Function;
import static java.lang.Boolean.FALSE; import static java.lang.Boolean.FALSE;
@ -30,7 +32,7 @@ import static org.apache.commons.lang3.ObjectUtils.isArray;
public abstract class ValidatableProperty<P extends ValidatableProperty<?, ?>, T> { public abstract class ValidatableProperty<P extends ValidatableProperty<?, ?>, T> {
protected static final String[] KEY_ORDER_HEAD = Array.of("propertyName"); protected static final String[] KEY_ORDER_HEAD = Array.of("propertyName");
protected static final String[] KEY_ORDER_TAIL = Array.of("required", "defaultValue", "readOnly", "writeOnly", "computed", "isTotalsValidator", "thresholdPercentage"); protected static final String[] KEY_ORDER_TAIL = Array.of("required", "requiresExactlyOneOf", "requiresAtMaxOneOf", "defaultValue", "readOnly", "writeOnly", "computed", "isTotalsValidator", "thresholdPercentage");
protected static final String[] KEY_ORDER = Array.join(KEY_ORDER_HEAD, KEY_ORDER_TAIL); protected static final String[] KEY_ORDER = Array.join(KEY_ORDER_HEAD, KEY_ORDER_TAIL);
final Class<T> type; final Class<T> type;
@ -40,6 +42,8 @@ public abstract class ValidatableProperty<P extends ValidatableProperty<?, ?>, T
private final String[] keyOrder; private final String[] keyOrder;
private Boolean required; private Boolean required;
private Set<String> requiresExactlyOneOf;
private Set<String> requiresAtMaxOneOf;
private T defaultValue; private T defaultValue;
@JsonIgnore @JsonIgnore
@ -100,9 +104,19 @@ protected void setDeferredInit(final Function<ValidatableProperty<?, ?>[], T[]>
return self(); return self();
} }
public ValidatableProperty<P, T> optional() { public P optional() {
required = FALSE; required = FALSE;
return this; return self();
}
public P requiresExactlyOneOf(final String... propNames) {
requiresExactlyOneOf = new LinkedHashSet<>(List.of(propNames));
return self();
}
public P requiresAtMaxOneOf(final String... propNames) {
requiresAtMaxOneOf = new LinkedHashSet<>(List.of(propNames));
return self();
} }
public P withDefault(final T value) { public P withDefault(final T value) {
@ -172,28 +186,59 @@ protected void setDeferredInit(final Function<ValidatableProperty<?, ?>[], T[]>
final var result = new ArrayList<String>(); final var result = new ArrayList<String>();
final var props = propsProvider.directProps(); final var props = propsProvider.directProps();
final var propValue = props.get(propertyName); final var propValue = props.get(propertyName);
if (propValue == null) { if (propValue == null) {
if (required) { if (required == TRUE) {
result.add(propertyName + "' is required but missing"); result.add(propertyName + "' is required but missing");
} }
validateRequiresExactlyOneOf(result, propsProvider);
} }
if (propValue != null){ if (propValue != null){
validateRequiresAtMaxOneOf(result, propsProvider);
if ( type.isInstance(propValue)) { if ( type.isInstance(propValue)) {
//noinspection unchecked //noinspection unchecked
validate(result, (T) propValue, propsProvider); validate(result, (T) propValue, propsProvider);
} else { } else {
result.add(propertyName + "' is expected to be of type " + type.getSimpleName() + ", " + result.add(propertyName + "' is expected to be of type " + type.getSimpleName() + ", " +
"but is of type " + propValue.getClass().getSimpleName() + ""); "but is of type " + propValue.getClass().getSimpleName());
} }
} }
return result; return result;
} }
private void validateRequiresExactlyOneOf(final ArrayList<String> result, final PropertiesProvider propsProvider) {
if (requiresExactlyOneOf != null ) {
final var allPropNames = propsProvider.directProps().keySet();
final var entriesWithValue = allPropNames.stream()
.filter(name -> requiresExactlyOneOf.contains(name))
.count();
if (entriesWithValue == 0) {
result.add(propertyName + "' is required once in group " + requiresExactlyOneOf + " but missing");
} else if (entriesWithValue > 1) {
result.add(propertyName + "' is required once in group " + requiresExactlyOneOf + " but multiple properties are set");
}
}
}
private void validateRequiresAtMaxOneOf(final ArrayList<String> result, final PropertiesProvider propsProvider) {
if (requiresAtMaxOneOf != null) {
final var allPropNames = propsProvider.directProps().keySet();
final var entriesWithValue = allPropNames.stream()
.filter(name -> requiresAtMaxOneOf.contains(name))
.count();
if (entriesWithValue > 1) {
result.add(propertyName + "' is required at max once in group " + requiresAtMaxOneOf
+ " but multiple properties are set");
}
}
}
protected abstract void validate(final List<String> result, final T propValue, final PropertiesProvider propProvider); protected abstract void validate(final List<String> result, final T propValue, final PropertiesProvider propProvider);
public void verifyConsistency(final Map.Entry<? extends Enum<?>, ?> typeDef) { public void verifyConsistency(final Map.Entry<? extends Enum<?>, ?> typeDef) {
if (required == null ) { if (required == null && requiresExactlyOneOf == null && requiresAtMaxOneOf == null) {
throw new IllegalStateException(typeDef.getKey() + "[" + propertyName + "] not fully initialized, please call either .required() or .optional()" ); throw new IllegalStateException(typeDef.getKey() + "[" + propertyName + "] not fully initialized, please call either .required(), .optional(), .withDefault(...), .requiresExactlyOneOf(...) or .requiresAtMaxOneOf(...)" );
} }
} }

View File

@ -53,7 +53,7 @@ class HsBookingItemEntityUnitTest {
void toStringContainsAllPropertiesAndResourcesSortedByKey() { void toStringContainsAllPropertiesAndResourcesSortedByKey() {
final var result = givenBookingItem.toString(); final var result = givenBookingItem.toString();
assertThat(result).isEqualToIgnoringWhitespace("HsBookingItemEntity(D-1234500:test project, CLOUD_SERVER, [2020-01-01,2031-01-01), some caption, { \"CPUs\": 2, \"HDD-storage\": 2048, \"SSD-storage\": 512 })"); assertThat(result).isEqualToIgnoringWhitespace("HsBookingItemEntity(D-1234500:test project, CLOUD_SERVER, [2020-01-01,2031-01-01), some caption, { \"CPU\": 2, \"HDD-storage\": 2048, \"SSD-storage\": 512 })");
} }
@Test @Test

View File

@ -171,8 +171,8 @@ class HsBookingItemRepositoryIntegrationTest extends ContextBasedTestWithCleanup
allTheseBookingItemsAreReturned( allTheseBookingItemsAreReturned(
result, result,
"HsBookingItemEntity(D-1000212:D-1000212 default project, MANAGED_WEBSPACE, [2022-10-01,), separate ManagedWebspace, { Daemons: 0, Multi: 1, SSD: 100, Traffic: 50 } )", "HsBookingItemEntity(D-1000212:D-1000212 default project, MANAGED_WEBSPACE, [2022-10-01,), separate ManagedWebspace, { Daemons: 0, Multi: 1, SSD: 100, Traffic: 50 } )",
"HsBookingItemEntity(D-1000212:D-1000212 default project, MANAGED_SERVER, [2022-10-01,), separate ManagedServer, { CPUs: 2, RAM: 8, SSD: 500, Traffic: 500 } )", "HsBookingItemEntity(D-1000212:D-1000212 default project, MANAGED_SERVER, [2022-10-01,), separate ManagedServer, { CPU: 2, RAM: 8, SSD: 500, Traffic: 500 } )",
"HsBookingItemEntity(D-1000212:D-1000212 default project, PRIVATE_CLOUD, [2024-04-01,), some PrivateCloud, { CPUs: 10, HDD: 10000, RAM: 32, SSD: 4000, Traffic: 2000 } )"); "HsBookingItemEntity(D-1000212:D-1000212 default project, PRIVATE_CLOUD, [2024-04-01,), some PrivateCloud, { CPU: 10, HDD: 10000, RAM: 32, SSD: 4000, Traffic: 2000 } )");
assertThat(result.stream().filter(bi -> bi.getRelatedHostingAsset()!=null).findAny()) assertThat(result.stream().filter(bi -> bi.getRelatedHostingAsset()!=null).findAny())
.as("at least one relatedProject expected, but none found => fetching relatedProject does not work") .as("at least one relatedProject expected, but none found => fetching relatedProject does not work")
.isNotEmpty(); .isNotEmpty();
@ -194,8 +194,8 @@ class HsBookingItemRepositoryIntegrationTest extends ContextBasedTestWithCleanup
exactlyTheseBookingItemsAreReturned( exactlyTheseBookingItemsAreReturned(
result, result,
"HsBookingItemEntity(D-1000111:D-1000111 default project, MANAGED_WEBSPACE, [2022-10-01,), separate ManagedWebspace, { Daemons: 0, Multi: 1, SSD: 100, Traffic: 50 } )", "HsBookingItemEntity(D-1000111:D-1000111 default project, MANAGED_WEBSPACE, [2022-10-01,), separate ManagedWebspace, { Daemons: 0, Multi: 1, SSD: 100, Traffic: 50 } )",
"HsBookingItemEntity(D-1000111:D-1000111 default project, MANAGED_SERVER, [2022-10-01,), separate ManagedServer, { CPUs: 2, RAM: 8, SSD: 500, Traffic: 500 } )", "HsBookingItemEntity(D-1000111:D-1000111 default project, MANAGED_SERVER, [2022-10-01,), separate ManagedServer, { CPU: 2, RAM: 8, SSD: 500, Traffic: 500 } )",
"HsBookingItemEntity(D-1000111:D-1000111 default project, PRIVATE_CLOUD, [2024-04-01,), some PrivateCloud, { CPUs: 10, HDD: 10000, RAM: 32, SSD: 4000, Traffic: 2000 } )"); "HsBookingItemEntity(D-1000111:D-1000111 default project, PRIVATE_CLOUD, [2024-04-01,), some PrivateCloud, { CPU: 10, HDD: 10000, RAM: 32, SSD: 4000, Traffic: 2000 } )");
} }
} }

View File

@ -38,7 +38,7 @@ class HsBookingItemEntityValidatorUnitTest {
// then // then
assertThat(result).isInstanceOf(ValidationException.class) assertThat(result).isInstanceOf(ValidationException.class)
.hasMessageContaining( .hasMessageContaining(
"'D-12345:test project:Test-Server.resources.CPUs' is required but missing", "'D-12345:test project:Test-Server.resources.CPU' is required but missing",
"'D-12345:test project:Test-Server.resources.RAM' is required but missing", "'D-12345:test project:Test-Server.resources.RAM' is required but missing",
"'D-12345:test project:Test-Server.resources.SSD' is required but missing", "'D-12345:test project:Test-Server.resources.SSD' is required but missing",
"'D-12345:test project:Test-Server.resources.Traffic' is required but missing"); "'D-12345:test project:Test-Server.resources.Traffic' is required but missing");

View File

@ -56,11 +56,12 @@ class HsCloudServerBookingItemValidatorUnitTest {
// then // then
assertThat(validator.properties()).map(Map::toString).containsExactlyInAnyOrder( assertThat(validator.properties()).map(Map::toString).containsExactlyInAnyOrder(
"{type=boolean, propertyName=active, defaultValue=true}", "{type=boolean, propertyName=active, defaultValue=true}",
"{type=integer, propertyName=CPUs, min=1, max=32, required=true}", "{type=integer, propertyName=CPU, min=1, max=32, required=true}",
"{type=integer, propertyName=RAM, unit=GB, min=1, max=128, required=true}", "{type=integer, propertyName=RAM, unit=GB, min=1, max=8192, required=true}",
"{type=integer, propertyName=SSD, unit=GB, min=0, max=1000, step=25, required=true}", "{type=integer, propertyName=SSD, unit=GB, min=0, max=1000, step=25, requiresExactlyOneOf=[SDD, HDD], defaultValue=0}",
"{type=integer, propertyName=HDD, unit=GB, min=0, max=4000, step=250, defaultValue=0}", "{type=integer, propertyName=HDD, unit=GB, min=0, max=4000, step=250, requiresExactlyOneOf=[SSD, HDD], defaultValue=0}",
"{type=integer, propertyName=Traffic, unit=GB, min=250, max=10000, step=250, required=true}", "{type=integer, propertyName=Traffic, unit=GB, min=250, max=10000, step=250, requiresAtMaxOneOf=[Bandwidth, Traffic]}",
"{type=integer, propertyName=Bandwidth, unit=GB, min=250, max=10000, step=250, requiresAtMaxOneOf=[Bandwidth, Traffic]}",
"{type=enumeration, propertyName=SLA-Infrastructure, values=[BASIC, EXT8H, EXT4H, EXT2H]}"); "{type=enumeration, propertyName=SLA-Infrastructure, values=[BASIC, EXT8H, EXT4H, EXT2H]}");
} }
@ -110,7 +111,7 @@ class HsCloudServerBookingItemValidatorUnitTest {
// then // then
assertThat(result).containsExactlyInAnyOrder( assertThat(result).containsExactlyInAnyOrder(
"'D-12345:Test-Project:Test Cloud.resources.CPUs' maximum total is 4, but actual total CPUs is 5", "'D-12345:Test-Project:Test Cloud.resources.CPU' maximum total is 4, but actual total CPU is 5",
"'D-12345:Test-Project:Test Cloud.resources.RAM' maximum total is 20 GB, but actual total RAM is 30 GB", "'D-12345:Test-Project:Test Cloud.resources.RAM' maximum total is 20 GB, but actual total RAM is 30 GB",
"'D-12345:Test-Project:Test Cloud.resources.SSD' maximum total is 100 GB, but actual total SSD is 150 GB", "'D-12345:Test-Project:Test Cloud.resources.SSD' maximum total is 100 GB, but actual total SSD is 150 GB",
"'D-12345:Test-Project:Test Cloud.resources.Traffic' maximum total is 5000 GB, but actual total Traffic is 5500 GB" "'D-12345:Test-Project:Test Cloud.resources.Traffic' maximum total is 5000 GB, but actual total Traffic is 5500 GB"

View File

@ -63,11 +63,12 @@ class HsManagedServerBookingItemValidatorUnitTest {
// then // then
assertThat(validator.properties()).map(Map::toString).containsExactlyInAnyOrder( assertThat(validator.properties()).map(Map::toString).containsExactlyInAnyOrder(
"{type=integer, propertyName=CPUs, min=1, max=32, required=true}", "{type=integer, propertyName=CPU, min=1, max=32, required=true}",
"{type=integer, propertyName=RAM, unit=GB, min=1, max=128, required=true}", "{type=integer, propertyName=RAM, unit=GB, min=1, max=128, required=true}",
"{type=integer, propertyName=SSD, unit=GB, min=25, max=1000, step=25, required=true, isTotalsValidator=true, thresholdPercentage=200}", "{type=integer, propertyName=SSD, unit=GB, min=25, max=2000, step=25, requiresExactlyOneOf=[SSD, HDD], defaultValue=0, isTotalsValidator=true, thresholdPercentage=200}",
"{type=integer, propertyName=HDD, unit=GB, min=0, max=4000, step=250, defaultValue=0, isTotalsValidator=true, thresholdPercentage=200}", "{type=integer, propertyName=HDD, unit=GB, min=0, max=10000, step=250, requiresExactlyOneOf=[SSD, HDD], defaultValue=0, isTotalsValidator=true, thresholdPercentage=200}",
"{type=integer, propertyName=Traffic, unit=GB, min=250, max=10000, step=250, required=true, isTotalsValidator=true, thresholdPercentage=200}", "{type=integer, propertyName=Traffic, unit=GB, min=250, max=64000, step=250, requiresAtMaxOneOf=[Bandwidth, Traffic], isTotalsValidator=true, thresholdPercentage=200}",
"{type=integer, propertyName=Bandwidth, unit=GB, min=250, max=64000, step=250, requiresAtMaxOneOf=[Bandwidth, Traffic], isTotalsValidator=true, thresholdPercentage=200}",
"{type=enumeration, propertyName=SLA-Platform, values=[BASIC, EXT8H, EXT4H, EXT2H], defaultValue=BASIC}", "{type=enumeration, propertyName=SLA-Platform, values=[BASIC, EXT8H, EXT4H, EXT2H], defaultValue=BASIC}",
"{type=boolean, propertyName=SLA-EMail}", // TODO.impl: falseIf-validation is missing in output "{type=boolean, propertyName=SLA-EMail}", // TODO.impl: falseIf-validation is missing in output
"{type=boolean, propertyName=SLA-Maria}", "{type=boolean, propertyName=SLA-Maria}",
@ -120,7 +121,7 @@ class HsManagedServerBookingItemValidatorUnitTest {
// then // then
assertThat(result).containsExactlyInAnyOrder( assertThat(result).containsExactlyInAnyOrder(
"'D-12345:Test-Project:null.resources.CPUs' maximum total is 4, but actual total CPUs is 5", "'D-12345:Test-Project:null.resources.CPU' maximum total is 4, but actual total CPU is 5",
"'D-12345:Test-Project:null.resources.RAM' maximum total is 20 GB, but actual total RAM is 30 GB", "'D-12345:Test-Project:null.resources.RAM' maximum total is 20 GB, but actual total RAM is 30 GB",
"'D-12345:Test-Project:null.resources.SSD' maximum total is 100 GB, but actual total SSD is 150 GB", "'D-12345:Test-Project:null.resources.SSD' maximum total is 100 GB, but actual total SSD is 150 GB",
"'D-12345:Test-Project:null.resources.Traffic' maximum total is 5000 GB, but actual total Traffic is 5500 GB" "'D-12345:Test-Project:null.resources.Traffic' maximum total is 5000 GB, but actual total Traffic is 5500 GB"

View File

@ -41,7 +41,7 @@ class HsManagedWebspaceBookingItemValidatorUnitTest {
// then // then
assertThat(result).containsExactlyInAnyOrder( assertThat(result).containsExactlyInAnyOrder(
"'D-12345:Test-Project:Test Managed-Webspace.resources.CPUs' is not expected but is set to '2'", "'D-12345:Test-Project:Test Managed-Webspace.resources.CPU' is not expected but is set to '2'",
"'D-12345:Test-Project:Test Managed-Webspace.resources.RAM' is not expected but is set to '25'", "'D-12345:Test-Project:Test Managed-Webspace.resources.RAM' is not expected but is set to '25'",
"'D-12345:Test-Project:Test Managed-Webspace.resources.SSD' is required but missing", "'D-12345:Test-Project:Test Managed-Webspace.resources.SSD' is required but missing",
"'D-12345:Test-Project:Test Managed-Webspace.resources.SLA-EMail' is not expected but is set to 'true'" "'D-12345:Test-Project:Test Managed-Webspace.resources.SLA-EMail' is not expected but is set to 'true'"
@ -55,11 +55,12 @@ class HsManagedWebspaceBookingItemValidatorUnitTest {
// then // then
assertThat(validator.properties()).map(Map::toString).containsExactlyInAnyOrder( assertThat(validator.properties()).map(Map::toString).containsExactlyInAnyOrder(
"{type=integer, propertyName=SSD, unit=GB, min=1, max=100, step=1, required=true}", "{type=integer, propertyName=SSD, unit=GB, min=1, max=2000, step=1, required=true}",
"{type=integer, propertyName=HDD, unit=GB, min=0, max=250, step=10}", "{type=integer, propertyName=HDD, unit=GB, min=0, max=10000, step=10}",
"{type=integer, propertyName=Traffic, unit=GB, min=10, max=1000, step=10, required=true}", "{type=integer, propertyName=Traffic, unit=GB, min=10, max=64000, step=10, requiresAtMaxOneOf=[Bandwidth, Traffic]}",
"{type=integer, propertyName=Bandwidth, unit=GB, min=10, max=1000, step=10, requiresAtMaxOneOf=[Bandwidth, Traffic]}",
"{type=integer, propertyName=Multi, min=1, max=100, step=1, defaultValue=1}", "{type=integer, propertyName=Multi, min=1, max=100, step=1, defaultValue=1}",
"{type=integer, propertyName=Daemons, min=0, max=10, defaultValue=0}", "{type=integer, propertyName=Daemons, min=0, max=16, defaultValue=0}",
"{type=boolean, propertyName=Online Office Server}", "{type=boolean, propertyName=Online Office Server}",
"{type=enumeration, propertyName=SLA-Platform, values=[BASIC, EXT24H], defaultValue=BASIC}"); "{type=enumeration, propertyName=SLA-Platform, values=[BASIC, EXT24H], defaultValue=BASIC}");
} }

View File

@ -124,7 +124,7 @@ class HsPrivateCloudBookingItemValidatorUnitTest {
// then // then
assertThat(result).containsExactlyInAnyOrder( assertThat(result).containsExactlyInAnyOrder(
"'D-12345:Test-Project:myPC.resources.CPUs' maximum total is 4, but actual total CPUs is 5", "'D-12345:Test-Project:myPC.resources.CPU' maximum total is 4, but actual total CPU is 5",
"'D-12345:Test-Project:myPC.resources.RAM' maximum total is 20 GB, but actual total RAM is 30 GB", "'D-12345:Test-Project:myPC.resources.RAM' maximum total is 20 GB, but actual total RAM is 30 GB",
"'D-12345:Test-Project:myPC.resources.SSD' maximum total is 100 GB, but actual total SSD is 150 GB", "'D-12345:Test-Project:myPC.resources.SSD' maximum total is 100 GB, but actual total SSD is 150 GB",
"'D-12345:Test-Project:myPC.resources.Traffic' maximum total is 5000 GB, but actual total Traffic is 5500 GB", "'D-12345:Test-Project:myPC.resources.Traffic' maximum total is 5000 GB, but actual total Traffic is 5500 GB",

View File

@ -207,6 +207,14 @@ public class CsvDataImport extends ContextBasedTest {
em.createNativeQuery("delete from tx_context where true").executeUpdate(); em.createNativeQuery("delete from tx_context where true").executeUpdate();
}).assertSuccessful(); }).assertSuccessful();
} }
void logError(final Runnable assertion) {
try {
assertion.run();
} catch (final AssertionError exc) {
System.err.println(exc);
}
}
} }
class Columns { class Columns {

View File

@ -75,7 +75,7 @@ import static org.assertj.core.api.Assumptions.assumeThat;
* *
* gw-importHostingAssets # comes from .aliases file and uses .environment * gw-importHostingAssets # comes from .aliases file and uses .environment
*/ */
@Tag("import") @Tag("importHostingAssets")
@DataJpaTest(properties = { @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:///spring_boot_testcontainers}",
"spring.datasource.username=${HSADMINNG_POSTGRES_ADMIN_USERNAME:ADMIN}", "spring.datasource.username=${HSADMINNG_POSTGRES_ADMIN_USERNAME:ADMIN}",
@ -251,7 +251,7 @@ public class ImportHostingAssets extends ImportOfficeData {
3000630=HsBookingItemEntity(D-1000000:hsh default project, MANAGED_WEBSPACE, [2001-06-01,), BI hsh00, { "HDD": 10, "Multi": 25, "SLA-Platform": "EXT24H", "SSD": 16, "Traffic": 50}), 3000630=HsBookingItemEntity(D-1000000:hsh default project, MANAGED_WEBSPACE, [2001-06-01,), BI hsh00, { "HDD": 10, "Multi": 25, "SLA-Platform": "EXT24H", "SSD": 16, "Traffic": 50}),
3000968=HsBookingItemEntity(D-1015200:rar default project, MANAGED_SERVER, [2013-04-01,), BI vm1061, { "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}), 3000968=HsBookingItemEntity(D-1015200:rar default project, MANAGED_SERVER, [2013-04-01,), BI vm1061, { "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}),
3000978=HsBookingItemEntity(D-1000000:hsh default project, MANAGED_SERVER, [2013-04-01,), BI vm1050, { "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}), 3000978=HsBookingItemEntity(D-1000000:hsh default project, MANAGED_SERVER, [2013-04-01,), BI vm1050, { "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}),
3001061=HsBookingItemEntity(D-1000300:mim default project, MANAGED_SERVER, [2013-08-19,), BI vm1068, { "CPU": 2, "RAM": 4, "SLA-EMail": true, "SLA-Maria": true, "SLA-Office": true, "SLA-PgSQL": true, "SLA-Platform": "EXT2H", "SLA-Web": true, "SSD": 75, "Traffic": 250}), 3001061=HsBookingItemEntity(D-1000300:mim default project, MANAGED_SERVER, [2013-08-19,), BI vm1068, { "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}),
3001094=HsBookingItemEntity(D-1000300:mim default project, MANAGED_WEBSPACE, [2013-09-10,), BI lug00, { "Multi": 5, "SLA-Platform": "EXT24H", "SSD": 1, "Traffic": 10}), 3001094=HsBookingItemEntity(D-1000300:mim default project, MANAGED_WEBSPACE, [2013-09-10,), BI lug00, { "Multi": 5, "SLA-Platform": "EXT24H", "SSD": 1, "Traffic": 10}),
3001112=HsBookingItemEntity(D-1000300:mim default project, MANAGED_WEBSPACE, [2013-09-17,), BI mim00, { "Multi": 5, "SLA-Platform": "EXT24H", "SSD": 3, "Traffic": 20}), 3001112=HsBookingItemEntity(D-1000300:mim default project, MANAGED_WEBSPACE, [2013-09-17,), BI mim00, { "Multi": 5, "SLA-Platform": "EXT24H", "SSD": 3, "Traffic": 20}),
3001447=HsBookingItemEntity(D-1000000:hsh default project, MANAGED_SERVER, [2014-11-28,), BI vm1093, { "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}), 3001447=HsBookingItemEntity(D-1000000:hsh default project, MANAGED_SERVER, [2014-11-28,), BI vm1093, { "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}),
@ -261,6 +261,32 @@ public class ImportHostingAssets extends ImportOfficeData {
"""); """);
} }
@Test
@Order(11400)
void validateBookingItems() {
bookingItems.forEach((id, bi) -> {
try {
HsBookingItemEntityValidatorRegistry.validated(bi);
} catch (final Exception exc) {
System.err.println("validation failed for id:" + id + "( " + bi + "): " + exc.getMessage());
}
});
}
@Test
@Order(11410)
void validateHostingAssets() {
hostingAssets.forEach((id, ha) -> {
try {
new HostingAssetEntitySaveProcessor(ha)
.preprocessEntity()
.validateEntity();
} catch (final Exception exc) {
System.err.println("validation failed for id:" + id + "( " + ha + "): " + exc.getMessage());
}
});
}
@Test @Test
@Order(19000) @Order(19000)
@Commit @Commit
@ -368,11 +394,11 @@ public class ImportHostingAssets extends ImportOfficeData {
.validity(toPostgresDateRange(created, cancelled)) .validity(toPostgresDateRange(created, cancelled))
.build(); .build();
bookingItems.put(PACKET_ID_OFFSET + packet_id, bookingItem); bookingItems.put(PACKET_ID_OFFSET + packet_id, bookingItem);
final var haType = determineHaType(basepacket_code); final var haType = determineHaType(basepacket_code);
assertThat(!free || haType == MANAGED_WEBSPACE || bookingItem.getRelatedProject().getDebitor().getDefaultPrefix().equals("hsh"))
logError(() -> assertThat(!free || haType == MANAGED_WEBSPACE || bookingItem.getRelatedProject().getDebitor().getDefaultPrefix().equals("hsh"))
.as("packet.free only supported for Hostsharing-Assets and ManagedWebspace in customer-ManagedServer, but is set for " + packet_name) .as("packet.free only supported for Hostsharing-Assets and ManagedWebspace in customer-ManagedServer, but is set for " + packet_name)
.isTrue(); .isTrue());
final var asset = HsHostingAssetEntity.builder() final var asset = HsHostingAssetEntity.builder()
.isLoaded(haType == MANAGED_WEBSPACE) // this turns off identifier validation to accept former default prefixes .isLoaded(haType == MANAGED_WEBSPACE) // this turns off identifier validation to accept former default prefixes

View File

@ -37,6 +37,7 @@ import static java.util.Optional.ofNullable;
import static net.hostsharing.hsadminng.mapper.PostgresDateRange.toPostgresDateRange; import static net.hostsharing.hsadminng.mapper.PostgresDateRange.toPostgresDateRange;
import static org.apache.commons.lang3.StringUtils.isBlank; import static org.apache.commons.lang3.StringUtils.isBlank;
import static org.apache.commons.lang3.StringUtils.isNotBlank; import static org.apache.commons.lang3.StringUtils.isNotBlank;
import static org.assertj.core.api.Assertions.as;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assumptions.assumeThat; import static org.assertj.core.api.Assumptions.assumeThat;
import static org.assertj.core.api.Fail.fail; import static org.assertj.core.api.Fail.fail;
@ -77,7 +78,7 @@ import static org.assertj.core.api.Fail.fail;
* *
* gw-importOfficeTables # comes from .aliases file and uses .environment * gw-importOfficeTables # comes from .aliases file and uses .environment
*/ */
@Tag("import") @Tag("importOfficeData")
@DataJpaTest(properties = { @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:///spring_boot_testcontainers}",
"spring.datasource.username=${HSADMINNG_POSTGRES_ADMIN_USERNAME:ADMIN}", "spring.datasource.username=${HSADMINNG_POSTGRES_ADMIN_USERNAME:ADMIN}",
@ -111,6 +112,7 @@ public class ImportOfficeData extends CsvDataImport {
512167, // 11139, partner without contractual contact 512167, // 11139, partner without contractual contact
512170, // 11142, partner without contractual contact 512170, // 11142, partner without contractual contact
511725, // 10764, partner without contractual contact 511725, // 10764, partner without contractual contact
// 512171, // 11143, partner without partner contact -- exc
-1 -1
); );
@ -498,15 +500,16 @@ public class ImportOfficeData extends CsvDataImport {
@Test @Test
@Order(2000) @Order(2000)
// @Disabled // FIXME
void verifyAllPartnersHavePersons() { void verifyAllPartnersHavePersons() {
partners.forEach((id, p) -> { partners.forEach((id, p) -> {
final var partnerRel = p.getPartnerRel(); final var partnerRel = p.getPartnerRel();
assertThat(partnerRel).describedAs("partner " + id + " without partnerRel").isNotNull(); assertThat(partnerRel).describedAs("partner " + id + " without partnerRel").isNotNull();
if ( id != 199 ) { if ( id != 199 ) {
assertThat(partnerRel.getContact()).describedAs("partner " + id + " without partnerRel.contact").isNotNull(); 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.getContact().getCaption()).describedAs("partner " + id + " without valid partnerRel.contact").isNotNull());
assertThat(partnerRel.getHolder()).describedAs("partner " + id + " without partnerRel.relHolder").isNotNull(); logError( () -> assertThat(partnerRel.getHolder()).describedAs("partner " + id + " without partnerRel.relHolder").isNotNull());
assertThat(partnerRel.getHolder().getPersonType()).describedAs("partner " + id + " without valid partnerRel.relHolder").isNotNull(); logError( () -> assertThat(partnerRel.getHolder().getPersonType()).describedAs("partner " + id + " without valid partnerRel.relHolder").isNotNull());
} }
}); });
} }
@ -729,7 +732,8 @@ public class ImportOfficeData extends CsvDataImport {
.map(this::trimAll) .map(this::trimAll)
.map(row -> new Record(columns, row)) .map(row -> new Record(columns, row))
.forEach(rec -> { .forEach(rec -> {
if (IGNORE_BUSINESS_PARTNERS.contains(rec.getInteger("bp_id"))) { final Integer bpId = rec.getInteger("bp_id");
if (IGNORE_BUSINESS_PARTNERS.contains(bpId)) {
return; return;
} }
@ -747,7 +751,7 @@ public class ImportOfficeData extends CsvDataImport {
.details(HsOfficePartnerDetailsEntity.builder().build()) .details(HsOfficePartnerDetailsEntity.builder().build())
.partnerRel(partnerRel) .partnerRel(partnerRel)
.build(); .build();
partners.put(rec.getInteger("bp_id"), partner); partners.put(bpId, partner);
final var debitorRel = addRelation( final var debitorRel = addRelation(
HsOfficeRelationType.DEBITOR, partnerRel.getHolder(), // partner person HsOfficeRelationType.DEBITOR, partnerRel.getHolder(), // partner person
@ -765,7 +769,7 @@ public class ImportOfficeData extends CsvDataImport {
.vatBusiness("GROSS".equals(rec.getString("indicator_vat"))) // TODO: remove .vatBusiness("GROSS".equals(rec.getString("indicator_vat"))) // TODO: remove
.vatId(rec.getString("uid_vat")) .vatId(rec.getString("uid_vat"))
.build(); .build();
debitors.put(rec.getInteger("bp_id"), debitor); debitors.put(bpId, debitor);
if (isNotBlank(rec.getString("member_since"))) { if (isNotBlank(rec.getString("member_since"))) {
assertThat(rec.getInteger("member_id")).isEqualTo(partner.getPartnerNumber()); assertThat(rec.getInteger("member_id")).isEqualTo(partner.getPartnerNumber());
@ -781,7 +785,7 @@ public class ImportOfficeData extends CsvDataImport {
? HsOfficeMembershipStatus.ACTIVE ? HsOfficeMembershipStatus.ACTIVE
: HsOfficeMembershipStatus.UNKNOWN) : HsOfficeMembershipStatus.UNKNOWN)
.build(); .build();
memberships.put(rec.getInteger("bp_id"), membership); memberships.put(bpId, membership);
} }
}); });
} }
@ -795,6 +799,10 @@ public class ImportOfficeData extends CsvDataImport {
.map(row -> new Record(columns, row)) .map(row -> new Record(columns, row))
.forEach(rec -> { .forEach(rec -> {
final var bpId = rec.getInteger("bp_id"); final var bpId = rec.getInteger("bp_id");
if (IGNORE_BUSINESS_PARTNERS.contains(bpId)) {
return;
}
final var member = ofNullable(memberships.get(bpId)) final var member = ofNullable(memberships.get(bpId))
.orElseGet(() -> createOnDemandMembership(bpId)); .orElseGet(() -> createOnDemandMembership(bpId));

View File

@ -110,7 +110,7 @@ packet_component_id;packet_id;quantity;basecomponent_code;created;cancelled
1339235;19959;1;MULTI;2023-10-27; 1339235;19959;1;MULTI;2023-10-27;
1341088;1061;0;SLAOFFIC2H;2023-12-14; 1341088;1061;0;SLAOFFIC2H;2023-12-14;
1341089;1061;0;SLAOFFIC8H;2023-12-14; 1341089;1061;0;SLAOFFIC8H;2023-12-14;
1341090;1061;0;STORAGE;2023-12-14; 1341090;1061;256000;STORAGE;2023-12-14;
1341091;1061;0;SLAMAIL4H;2023-12-14; 1341091;1061;0;SLAMAIL4H;2023-12-14;
1341092;1061;0;SLAMAIL2H;2023-12-14; 1341092;1061;0;SLAMAIL2H;2023-12-14;
1341093;1061;0;SLAPLAT2H;2023-12-14; 1341093;1061;0;SLAPLAT2H;2023-12-14;
@ -118,7 +118,7 @@ packet_component_id;packet_id;quantity;basecomponent_code;created;cancelled
1341095;1061;0;SLAPLAT4H;2023-12-14; 1341095;1061;0;SLAPLAT4H;2023-12-14;
1341096;1061;1;SLAPGSQL8H;2023-12-14; 1341096;1061;1;SLAPGSQL8H;2023-12-14;
1341097;1061;2;CPU;2023-12-14; 1341097;1061;2;CPU;2023-12-14;
1341098;1061;76800;QUOTA;2023-12-14; 1341098;1061;0;QUOTA;2023-12-14;
1341099;1061;0;SLAMAIL8H;2023-12-14; 1341099;1061;0;SLAMAIL8H;2023-12-14;
1341100;1061;1;SLABASIC;2023-12-14; 1341100;1061;1;SLABASIC;2023-12-14;
1341101;1061;1;SLAMARIA8H;2023-12-14; 1341101;1061;1;SLAMARIA8H;2023-12-14;

1 packet_component_id packet_id quantity basecomponent_code created cancelled
110 1339235 19959 1 MULTI 2023-10-27
111 1341088 1061 0 SLAOFFIC2H 2023-12-14
112 1341089 1061 0 SLAOFFIC8H 2023-12-14
113 1341090 1061 0 256000 STORAGE 2023-12-14
114 1341091 1061 0 SLAMAIL4H 2023-12-14
115 1341092 1061 0 SLAMAIL2H 2023-12-14
116 1341093 1061 0 SLAPLAT2H 2023-12-14
118 1341095 1061 0 SLAPLAT4H 2023-12-14
119 1341096 1061 1 SLAPGSQL8H 2023-12-14
120 1341097 1061 2 CPU 2023-12-14
121 1341098 1061 76800 0 QUOTA 2023-12-14
122 1341099 1061 0 SLAMAIL8H 2023-12-14
123 1341100 1061 1 SLABASIC 2023-12-14
124 1341101 1061 1 SLAMARIA8H 2023-12-14