diff --git a/src/java/ABTProductsPUTGenerator/.idea/misc.xml b/src/java/ABTProductsPUTGenerator/.idea/misc.xml index 7ace097..82dbec8 100644 --- a/src/java/ABTProductsPUTGenerator/.idea/misc.xml +++ b/src/java/ABTProductsPUTGenerator/.idea/misc.xml @@ -8,7 +8,7 @@ - + \ No newline at end of file diff --git a/src/java/ABTProductsPUTGenerator/bin/ABTProductsPUTGenerator.jar b/src/java/ABTProductsPUTGenerator/bin/ABTProductsPUTGenerator.jar index d36d6ec..2cb678f 100644 Binary files a/src/java/ABTProductsPUTGenerator/bin/ABTProductsPUTGenerator.jar and b/src/java/ABTProductsPUTGenerator/bin/ABTProductsPUTGenerator.jar differ diff --git a/src/java/ABTProductsPUTGenerator/bin/README.md b/src/java/ABTProductsPUTGenerator/bin/README.md index 3d0dad5..9a26306 100644 --- a/src/java/ABTProductsPUTGenerator/bin/README.md +++ b/src/java/ABTProductsPUTGenerator/bin/README.md @@ -1,7 +1,9 @@ # ABTProducts PUT request body generator Simple tool to quickly edit HTM products via ABTProducts REST API. -- Requires JRE 21 +- Requires JRE 17 + +## Generating a PUT output (that you need to supply to PUT API yourself): - Run via: `java -jar ABTProductsPUTGenerator.jar` - Specify custom input/output path via: `java -jar ABTProductsPUTGenerator.jar ` - Takes a ABTProducts GET response body in JSON format (product details) @@ -10,3 +12,14 @@ Simple tool to quickly edit HTM products via ABTProducts REST API. - `curl -X PUT -H 'Content-Type: application/json' {baseUrl}/abt/abtproducts/1.0/38 --data @output.json` - Default input path: /input.json - Default output path: /output.json (output is overwritten if it exists) + +## Bulk clearing (set to null) of a certain product attribute for all productIds in a product-tree (SE product reponse) +- Run via: `java -jar ABTProductsPUTGenerator.jar clearAttribute ` +- Takes a SE GET product tree response body or ABTProducts GET response body in JSON format +- Also needs the attribute to clear and the WSO2 environment and valid WSO2 bearer token for that environment +- Performs the following operations: + - Finds all productId's in the given product(tree) and for each productId found: + - GETs productdetails via ABTProducts API + - Converts it to PUT request body using the functionality in the previous section + - Replaces with `null` + - Actually sends the modified PUT request body to ABTProducts PUT API diff --git a/src/java/ABTProductsPUTGenerator/pom.xml b/src/java/ABTProductsPUTGenerator/pom.xml index 6c41c8c..0cdc759 100644 --- a/src/java/ABTProductsPUTGenerator/pom.xml +++ b/src/java/ABTProductsPUTGenerator/pom.xml @@ -58,8 +58,8 @@ org.apache.maven.plugins maven-compiler-plugin - 21 - 21 + 17 + 17 diff --git a/src/java/ABTProductsPUTGenerator/src/main/java/nl/htm/ovpay/abt/ABTProductsPUTGenerator.java b/src/java/ABTProductsPUTGenerator/src/main/java/nl/htm/ovpay/abt/ABTProductsPUTGenerator.java index b2910d3..2e53f85 100644 --- a/src/java/ABTProductsPUTGenerator/src/main/java/nl/htm/ovpay/abt/ABTProductsPUTGenerator.java +++ b/src/java/ABTProductsPUTGenerator/src/main/java/nl/htm/ovpay/abt/ABTProductsPUTGenerator.java @@ -3,8 +3,11 @@ package nl.htm.ovpay.abt; import java.io.File; import java.io.IOException; import java.io.InputStream; +import java.util.Arrays; import java.util.List; import java.util.Map; +import java.util.Scanner; +import java.util.stream.Collectors; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -19,6 +22,15 @@ public class ABTProductsPUTGenerator { private static final Logger LOGGER = LoggerFactory.getLogger(ABTProductsPUTGenerator.class); public static void main(String[] args) throws Exception { + LOGGER.info("Starting ABTProductsPUTGenerator with arguments {}", Arrays.stream(args).toList()); + if (args.length > 0 && args[0].equalsIgnoreCase("clearAttribute")) { + clearAttribute(args); + } else { + generateOutput(args); + } + } + + private static void generateOutput(String[] args) throws Exception { if (args.length != 2) { LOGGER.info("To modify input/output path, use: java -jar ABTProductsPUTGenerator.jar "); } @@ -40,6 +52,49 @@ public class ABTProductsPUTGenerator { } } + private static void clearAttribute(String[] args) throws Exception { + if (args.length != 5) { + LOGGER.error("Incorrect input parameters!"); + LOGGER.error("To clear attribute, use: java -jar ABTProductsPUTGenerator.jar clearAttribute "); + return; + } + + var inputFile = args[1]; + var attributeToClear = args[2]; + var environment = args[3]; + var wso2BearerToken = args[4]; + + try (InputStream is = getInputStream(inputFile)) { + ObjectMapper mapper = new ObjectMapper(); + JsonNode jsonNode = mapper.readTree(is); + var productIds = jsonNode.findValues("productId").stream().filter(JsonNode::isValueNode).map(JsonNode::asText).toList(); + + LOGGER.info("Found productIds to process: {}", productIds); + LOGGER.warn("Are you SURE you want to set attribute \"{}\" for all these productIds on environment {} to NULL?", attributeToClear, environment); + LOGGER.warn("Type 'yes' to continue..."); + var input = new Scanner(System.in).nextLine(); + if (!input.equalsIgnoreCase("yes")) { + LOGGER.info("Aborting..."); + return; + } + + for (var productId : productIds) { + LOGGER.info("Getting product details for product {}...", productId); + var productJsonString = APIHelper.getProductDetails(environment, productId, wso2BearerToken); + var productJsonNode = mapper.readTree(productJsonString); + var putJsonNode = processJsonNode(productJsonNode); + LOGGER.info("Clearing attribute \"{}\" from product with productId {}...", attributeToClear, productId); + ((ObjectNode)putJsonNode).putRawValue(attributeToClear, null); + + LOGGER.info("PUT product details for product with productId {} with cleared attribute \"{}\"...", productId, attributeToClear); + LOGGER.info("PUT product details with JSON body: {}", putJsonNode.toPrettyString()); + APIHelper.putProductDetails(environment, productId, putJsonNode.toString(), wso2BearerToken); + } + + LOGGER.info("DONE clearing attribute \"{}\" for productIds {}!", attributeToClear, productIds); + } + } + private static InputStream getInputStream(String filePath) throws IOException { var externalResource = new File(filePath); if (externalResource.exists()) { diff --git a/src/java/ABTProductsPUTGenerator/src/main/java/nl/htm/ovpay/abt/APIHelper.java b/src/java/ABTProductsPUTGenerator/src/main/java/nl/htm/ovpay/abt/APIHelper.java new file mode 100644 index 0000000..f1b36a0 --- /dev/null +++ b/src/java/ABTProductsPUTGenerator/src/main/java/nl/htm/ovpay/abt/APIHelper.java @@ -0,0 +1,89 @@ +package nl.htm.ovpay.abt; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.HttpURLConnection; +import java.net.URL; +import java.net.URLConnection; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.security.KeyManagementException; +import java.security.NoSuchAlgorithmException; +import java.util.HashMap; +import java.util.Map; +import java.util.StringJoiner; +import javax.net.ssl.HttpsURLConnection; +import javax.net.ssl.SSLContext; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class APIHelper { + + private static final Logger LOGGER = LoggerFactory.getLogger(APIHelper.class); + private static final String PRODUCT_DETAILS_URI = "https://services..api.htm.nl/abt/abtproducts/1.0/products/"; + + + public static String envToUriPart(String environment) { + return switch (environment) { + case "DEV" -> "dev"; + case "ACC" -> "acc"; + case "PRD" -> ""; + default -> throw new IllegalArgumentException("Invalid environment: " + environment); + }; + } + + public static String getProductDetails(String environment, String productId, String wso2BearerToken) throws Exception { + var envUriPart = envToUriPart(environment); + var getProductDetailsUri = PRODUCT_DETAILS_URI.replace("", envUriPart).replace("", productId); + + SSLContext sc = SSLContext.getInstance("SSL"); + sc.init(null, DummyX509TrustManager.getDummyArray(), new java.security.SecureRandom()); + HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory()); + + URL url = new URL(getProductDetailsUri); + URLConnection con = url.openConnection(); + HttpURLConnection http = (HttpURLConnection)con; + http.setRequestMethod("GET"); + http.setDoOutput(false); + http.setRequestProperty("Authorization", "Bearer " + wso2BearerToken); + http.connect(); + + try(InputStream is = http.getInputStream()) { + return new String(is.readAllBytes(), StandardCharsets.UTF_8); + } + } + + public static void putProductDetails(String environment, String productId, String jsonBody, String wso2BearerToken) throws Exception { + var envUriPart = envToUriPart(environment); + var putProductDetailsUri = PRODUCT_DETAILS_URI.replace("", envUriPart).replace("", productId); + + LOGGER.info("PUT product details to URI: {}", putProductDetailsUri); + + SSLContext sc = SSLContext.getInstance("SSL"); + sc.init(null, DummyX509TrustManager.getDummyArray(), new java.security.SecureRandom()); + HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory()); + + URL url = new URL(putProductDetailsUri); + URLConnection con = url.openConnection(); + HttpURLConnection http = (HttpURLConnection)con; + http.setRequestMethod("PUT"); + http.setDoOutput(true); + http.setRequestProperty("Authorization", "Bearer " + wso2BearerToken); + http.setRequestProperty("Content-Type", "application/json"); + + byte[] out = jsonBody.getBytes(StandardCharsets.UTF_8); + int length = out.length; + http.setFixedLengthStreamingMode(length); + http.connect(); + + try(OutputStream os = http.getOutputStream()) { + os.write(out); + } + + try(InputStream is = http.getInputStream()) { + LOGGER.info("Got response from PUT API: {}", new String(is.readAllBytes(), StandardCharsets.UTF_8)); + } + } +} diff --git a/src/java/ABTProductsPUTGenerator/src/main/java/nl/htm/ovpay/abt/DummyX509TrustManager.java b/src/java/ABTProductsPUTGenerator/src/main/java/nl/htm/ovpay/abt/DummyX509TrustManager.java new file mode 100644 index 0000000..205fe6c --- /dev/null +++ b/src/java/ABTProductsPUTGenerator/src/main/java/nl/htm/ovpay/abt/DummyX509TrustManager.java @@ -0,0 +1,38 @@ +package nl.htm.ovpay.abt; + +import java.security.cert.X509Certificate; +import javax.net.ssl.TrustManager; +import javax.net.ssl.X509TrustManager; + +public final class DummyX509TrustManager implements X509TrustManager { + + private static DummyX509TrustManager INSTANCE; + + private DummyX509TrustManager() { + // prevent instantiation + } + + public static DummyX509TrustManager getInstance() { + if (INSTANCE == null) { + INSTANCE = new DummyX509TrustManager(); + } + return INSTANCE; + } + + public static TrustManager[] getDummyArray() { + if (INSTANCE == null) { + INSTANCE = new DummyX509TrustManager(); + } + return new TrustManager[] { INSTANCE }; + } + + public X509Certificate[] getAcceptedIssuers() { + return null; + } + + public void checkClientTrusted(X509Certificate[] certs, String authType) { + } + + public void checkServerTrusted(X509Certificate[] certs, String authType) { + } +} diff --git a/src/java/ABTProductsPUTGenerator/src/main/resources/input.json b/src/java/ABTProductsPUTGenerator/src/main/resources/input.json index 376fe2d..85fc22f 100644 --- a/src/java/ABTProductsPUTGenerator/src/main/resources/input.json +++ b/src/java/ABTProductsPUTGenerator/src/main/resources/input.json @@ -1,23 +1,33 @@ { - "productId": 251, - "fikoArticleNumber": null, + "productId": 663, "parentProductId": null, - "gboPackageTemplateId": "30901", + "layerInfo": { + "layerInfoId": 7, + "choiceKey": "isRenewable", + "choiceLabel": "Kies voor een doorlopend abonnement of een enkele termijn", + "isCustomChoice": false + }, + "fikoArticleNumber": null, + "gboPackageTemplateId": "30001", "tapConnectProductCode": null, - "productName": "MaxTestPOST-21-okt-test-1 edited PUT", - "productDescription": "21-okt-test-1 edited PUT - reis met 90% korting gedurende de eerste F&F pilot!", - "validityPeriod": null, - "productTranslations": null, + "productName": "Test OVPAY-2306", + "productDescription": "Test OVPAY-2306 (sellingPeriods in kindje verwijderen en later opnieuw weer kunnen toevoegen)", + "validityPeriod": { + "validityPeriodId": 782, + "fromInclusive": "2025-12-31T23:00:00.000Z", + "toInclusive": "2026-03-30T22:00:00.000Z" + }, + "productTranslations": [], + "allowedGboAgeProfiles": [], "productOwner": { "productOwnerId": 1, - "name": "Corneel Verstoep", - "organization": "HTM" + "name": "Wie dit leest", + "organization": "... is een aap." }, - "marketSegments": null, - "customerSegments": null, - "allowedGboAgeProfiles": null, + "marketSegments": [], + "customerSegments": [], "productCategory": { - "productCategoryId": 9, + "productCategoryId": 1, "isTravelProduct": true, "name": "Kortingsabonnement" }, @@ -25,32 +35,10 @@ "requiredCustomerLevelId": 1, "name": "guest" }, - "requiredProducts": null, - "incompatibleProducts": null, - "mandatoryCustomerDataItems": [ - { - "mandatoryCustomerDataItemId": 4, - "customerDataItem": "emailAddress" - }, - { - "mandatoryCustomerDataItemId": 5, - "customerDataItem": "address" - } - ], - "requiredGboPersonalAttributes": [ - { - "requiredGboPersonalAttributeId": 1, - "name": "NAME" - }, - { - "requiredGboPersonalAttributeId": 2, - "name": "BIRTHDATE" - }, - { - "requiredGboPersonalAttributeId": 3, - "name": "PHOTO" - } - ], + "requiredProducts": [], + "incompatibleProducts": [], + "mandatoryCustomerDataItems": [], + "requiredGboPersonalAttributes": [], "tokenTypes": [ { "tokenTypeId": 1, @@ -61,72 +49,36 @@ "paymentMomentId": 1, "name": "prepaid" }, - "serviceOptions": null, - "validityDuration": "P7D", - "maxStartInFutureDuration": "P6W", - "isRenewable": false, + "serviceOptions": [ + { + "serviceOptionId": 4, + "action": "cancel_notAllowed", + "description": "Stopzetting is niet toegestaan (doorgaans in combinatie met refund_notAllowed)" + }, + { + "serviceOptionId": 10, + "action": "refund_notAllowed", + "description": "Terugbetaling niet toegestaan (doorgaans in combinatie met cancel_notAllowed)" + } + ], + "validityDuration": "P1W", + "maxStartInFutureDuration": "P1W", + "isRenewable": null, "sendInvoice": false, - "imageReference": "https://www.htm.nl/media/leif2leu/htm-logo-mobile.svg", - "productPageUrl": "https://www.htm.nl/nog-onbekende-product-pagina", - "termsUrl": "https://www.htm.nl/nog-onbekende-productvoorwaarden-pagina", + "imageReference": null, + "productPageUrl": null, + "termsUrl": null, "isSellableAtHtm": true, "needsSolvencyCheckConsumer": false, "needsSolvencyCheckBusiness": false, "sellingPeriods": [ { - "sellingPeriodId": 240, - "fromInclusive": "2024-09-06T00:00:00.000+00:00", - "toInclusive": "2024-12-29T23:59:59.000+00:00", + "sellingPeriodId": 1382, + "fromInclusive": "2025-12-31T23:00:00.000Z", + "toInclusive": "2026-03-30T22:00:00.000Z", "salesTouchpoint": { - "salesTouchpointId": 6, - "name": "Service-engine", - "isActive": true, - "retailer": { - "retailerId": 1000, - "name": "HTM intern beheer", - "street": "Koningin Julianaplein", - "number": 10, - "numberAddition": null, - "postalCode": "2595 AA", - "city": "Den Haag", - "country": "Nederland", - "emailAddress": "info@htm.nl", - "phoneNumber": "070 374 9002", - "taxId": null, - "imageReference": "https://www.htm.nl/typo3conf/ext/htm_template/Resources/Public/img/logo.svg" - } - }, - "forbiddenPaymentMethods": null, - "sellingPrices": [ - { - "sellingPriceId": 318, - "taxCode": "V21", - "taxPercentage": 21.0000, - "amountExclTax": 94, - "amountInclTax": 100, - "fromInclusive": "2024-09-06T00:00:00.000+00:00", - "toInclusive": "2024-12-18T23:59:59.000+00:00", - "internalPrice": 92.0000 - }, - { - "sellingPriceId": 319, - "taxCode": "V21", - "taxPercentage": 21.0000, - "amountExclTax": 98, - "amountInclTax": 102, - "fromInclusive": "2024-12-19T00:00:00.000+00:00", - "toInclusive": "2024-12-29T23:59:59.000+00:00", - "internalPrice": 0.0000 - } - ] - }, - { - "sellingPeriodId": 241, - "fromInclusive": "2024-09-06T00:00:00.000+00:00", - "toInclusive": "2024-12-29T23:59:59.000+00:00", - "salesTouchpoint": { - "salesTouchpointId": 5, - "name": "Servicewinkel (Team Incident Masters)", + "salesTouchpointId": 3, + "name": "Website", "isActive": true, "retailer": { "retailerId": 1001, @@ -139,64 +91,196 @@ "country": "Nederland", "emailAddress": "info@htm.nl", "phoneNumber": "070 374 9002", - "taxId": null, - "imageReference": "https://www.htm.nl/typo3conf/ext/htm_template/Resources/Public/img/logo.svg" + "taxId": 572309345923, + "imageReference": "https://www.htm.nl/media/leif2leu/htm-logo-mobile.svg" } }, - "forbiddenPaymentMethods": [ + "forbiddenPaymentMethods": [], + "sellingPrices": [] + } + ], + "purchasePrices": [], + "productVariants": [ + { + "productId": 664, + "parentProductId": 663, + "layerInfo": { + "layerInfoId": null, + "choiceKey": null, + "choiceLabel": null, + "isCustomChoice": false + }, + "fikoArticleNumber": null, + "gboPackageTemplateId": "30001", + "tapConnectProductCode": null, + "productName": "Losse week - Test OVPAY-2306", + "productDescription": "Test OVPAY-2306 (sellingPeriods in kindje verwijderen en later opnieuw weer kunnen toevoegen)", + "validityPeriod": { + "validityPeriodId": 783, + "fromInclusive": "2025-12-31T23:00:00.000Z", + "toInclusive": "2026-03-30T22:00:00.000Z" + }, + "productTranslations": [], + "allowedGboAgeProfiles": [], + "productOwner": { + "productOwnerId": 1, + "name": "Wie dit leest", + "organization": "... is een aap." + }, + "marketSegments": [], + "customerSegments": [], + "productCategory": { + "productCategoryId": 1, + "isTravelProduct": true, + "name": "Kortingsabonnement" + }, + "requiredCustomerLevel": { + "requiredCustomerLevelId": 1, + "name": "guest" + }, + "requiredProducts": [], + "incompatibleProducts": [], + "mandatoryCustomerDataItems": [], + "requiredGboPersonalAttributes": [], + "tokenTypes": [ { - "forbiddenPaymentMethodId": 2, - "name": "creditcard", - "issuer": "Visa" + "tokenTypeId": 1, + "name": "EMV" } ], - "sellingPrices": [ + "paymentMoment": { + "paymentMomentId": 1, + "name": "prepaid" + }, + "serviceOptions": [ { - "sellingPriceId": 320, - "taxCode": "V21", - "taxPercentage": 21.0000, - "amountExclTax": 94, - "amountInclTax": 100, - "fromInclusive": "2024-09-06T00:00:00.000+00:00", - "toInclusive": "2024-12-18T23:59:59.000+00:00", - "internalPrice": 92.0000 + "serviceOptionId": 4, + "action": "cancel_notAllowed", + "description": "Stopzetting is niet toegestaan (doorgaans in combinatie met refund_notAllowed)" }, { - "sellingPriceId": 321, - "taxCode": "V21", - "taxPercentage": 21.0000, - "amountExclTax": 98, - "amountInclTax": 102, - "fromInclusive": "2024-12-19T00:00:00.000+00:00", - "toInclusive": "2024-12-29T23:59:59.000+00:00", - "internalPrice": 0.0000 + "serviceOptionId": 10, + "action": "refund_notAllowed", + "description": "Terugbetaling niet toegestaan (doorgaans in combinatie met cancel_notAllowed)" } - ] - } - ], - "purchasePrices": [ - { - "purchasePriceId": 184, - "taxCode": "V21", - "taxPercentage": 21.0000, - "amountExclTax": 0, - "amountInclTax": 0, - "fromInclusive": "2024-09-01T00:00:00.000+00:00", - "toInclusive": "2024-12-31T23:59:59.000+00:00" - } - ], - "auditTrail": [ - { - "auditTrailId": 228, - "action": "update", - "user": "api", - "timestamp": "2024-10-21T09:00:30.410+00:00" + ], + "validityDuration": "P1W", + "maxStartInFutureDuration": "P1W", + "isRenewable": false, + "sendInvoice": false, + "imageReference": null, + "productPageUrl": null, + "termsUrl": null, + "isSellableAtHtm": true, + "needsSolvencyCheckConsumer": false, + "needsSolvencyCheckBusiness": false, + "sellingPeriods": [ + { + "sellingPeriodId": 1384, + "fromInclusive": "2025-12-31T23:00:00.000Z", + "toInclusive": "2026-03-30T22:00:00.000Z", + "salesTouchpoint": { + "salesTouchpointId": 3, + "name": "Website", + "isActive": true, + "retailer": { + "retailerId": 1001, + "name": "HTM externe touchpoints", + "street": "Koningin Julianaplein", + "number": 10, + "numberAddition": null, + "postalCode": "2595 AA", + "city": "Den Haag", + "country": "Nederland", + "emailAddress": "info@htm.nl", + "phoneNumber": "070 374 9002", + "taxId": 572309345923, + "imageReference": "https://www.htm.nl/media/leif2leu/htm-logo-mobile.svg" + } + }, + "forbiddenPaymentMethods": [], + "sellingPrices": [] + } + ], + "purchasePrices": [], + "productVariants": [] }, { - "auditTrailId": 227, - "action": "insert", - "user": "api", - "timestamp": "2024-10-21T08:58:39.237+00:00" + "productId": 665, + "parentProductId": 663, + "layerInfo": { + "layerInfoId": null, + "choiceKey": null, + "choiceLabel": null, + "isCustomChoice": false + }, + "fikoArticleNumber": null, + "gboPackageTemplateId": "30001", + "tapConnectProductCode": null, + "productName": "Doorlopend - Test OVPAY-2306", + "productDescription": "Test OVPAY-2306 (sellingPeriods in kindje verwijderen en later opnieuw weer kunnen toevoegen)", + "validityPeriod": { + "validityPeriodId": 784, + "fromInclusive": "2025-12-31T23:00:00.000Z", + "toInclusive": "2026-03-30T22:00:00.000Z" + }, + "productTranslations": [], + "allowedGboAgeProfiles": [], + "productOwner": { + "productOwnerId": 1, + "name": "Wie dit leest", + "organization": "... is een aap." + }, + "marketSegments": [], + "customerSegments": [], + "productCategory": { + "productCategoryId": 1, + "isTravelProduct": true, + "name": "Kortingsabonnement" + }, + "requiredCustomerLevel": { + "requiredCustomerLevelId": 1, + "name": "guest" + }, + "requiredProducts": [], + "incompatibleProducts": [], + "mandatoryCustomerDataItems": [], + "requiredGboPersonalAttributes": [], + "tokenTypes": [ + { + "tokenTypeId": 1, + "name": "EMV" + } + ], + "paymentMoment": { + "paymentMomentId": 1, + "name": "prepaid" + }, + "serviceOptions": [ + { + "serviceOptionId": 4, + "action": "cancel_notAllowed", + "description": "Stopzetting is niet toegestaan (doorgaans in combinatie met refund_notAllowed)" + }, + { + "serviceOptionId": 10, + "action": "refund_notAllowed", + "description": "Terugbetaling niet toegestaan (doorgaans in combinatie met cancel_notAllowed)" + } + ], + "validityDuration": "P1W", + "maxStartInFutureDuration": "P1W", + "isRenewable": true, + "sendInvoice": false, + "imageReference": null, + "productPageUrl": null, + "termsUrl": null, + "isSellableAtHtm": true, + "needsSolvencyCheckConsumer": false, + "needsSolvencyCheckBusiness": false, + "sellingPeriods": [], + "purchasePrices": [], + "productVariants": [] } ] } \ No newline at end of file