From 71119a665be3e3ccf8c1885ac65cb6b221551208 Mon Sep 17 00:00:00 2001 From: Rk Somasundaram Date: Sun, 26 Jan 2025 22:59:29 -0800 Subject: [PATCH] Add Tygrys-specific email notification customization --- customBundle.properties | 2 + pom.xml | 453 +++++++++++++++++- .../generator/TemplateRenderer.java | 20 +- .../tygrys/TygrysInvoiceFormatter.java | 212 ++++++++ .../TygrysInvoiceFormatterFactory.java | 80 ++++ .../templates/InvoiceCreation.mustache | 3 +- .../tygrys/TygrysInvoiceFormatterTest.java | 234 +++++++++ .../resources/customBundle_en_US.properties | 2 + src/test/resources/defaultBundle.properties | 2 + .../resources/defaultBundle_en_US.properties | 2 + 10 files changed, 1004 insertions(+), 6 deletions(-) create mode 100644 customBundle.properties create mode 100644 src/main/java/org/killbill/billing/plugin/notification/generator/formatters/tygrys/TygrysInvoiceFormatter.java create mode 100644 src/main/java/org/killbill/billing/plugin/notification/generator/formatters/tygrys/factory/TygrysInvoiceFormatterFactory.java create mode 100644 src/test/java/org/killbill/billing/plugin/notification/generator/formatters/tygrys/TygrysInvoiceFormatterTest.java create mode 100644 src/test/resources/customBundle_en_US.properties create mode 100644 src/test/resources/defaultBundle.properties create mode 100644 src/test/resources/defaultBundle_en_US.properties diff --git a/customBundle.properties b/customBundle.properties new file mode 100644 index 0000000..48580bf --- /dev/null +++ b/customBundle.properties @@ -0,0 +1,2 @@ +key1=value1 +key2=value2 diff --git a/pom.xml b/pom.xml index 09fbc6b..55ff626 100644 --- a/pom.xml +++ b/pom.xml @@ -3,6 +3,7 @@ ~ Copyright 2010-2014 Ning, Inc. ~ Copyright 2014-2020 Groupon, Inc ~ Copyright 2014-2021 The Billing Project, LLC + ~ Copyright 2025 The Tygrys Project, LLC ~ ~ The Billing Project licenses this file to you under the Apache License, version 2.0 ~ (the "License"); you may not use this file except in compliance with the @@ -25,7 +26,7 @@ org.kill-bill.billing.plugin.java killbill-email-notifications-plugin - 0.8.0 + 0.8.1 bundle Kill Bill OSGI Email Notifications Plugin Kill Bill Email Notifications Plugin @@ -44,19 +45,325 @@ false true spotbugs-exclude.xml + 0.24.10 false org.killbill.billing.plugin.notification.api,org.killbill.billing.plugin.notification.generator.* org.killbill.billing.plugin.notification.* + + + + com.google.inject.extensions + guice-multibindings + 4.2.3 + + + + com.sun.mail + javax.mail + 1.6.2 + + + io.netty + netty-common + 4.1.84.Final + + + joda-time + joda-time + 2.12.5 + + + + net.bytebuddy + byte-buddy + 1.14.5 + + + net.bytebuddy + byte-buddy-agent + 1.14.5 + + + org.apache.shiro + shiro-core + 1.12.0 + + + org.apache.shiro + shiro-guice + 1.12.0 + + + org.kill-bill.billing + killbill-invoice + 0.24.10 + + + org.kill-bill.billing.plugin + killbill-plugin-api-invoice + + + org.kill-bill.commons + killbill-concurrent + + + org.kill-bill.commons + killbill-locker + + + org.kill-bill.commons + killbill-utils + + + org.kill-bill.billing + killbill-platform-api + + + org.kill-bill.billing + killbill-platform-base + + + net.bytebuddy + byte-buddy + + + org.kill-bill.commons + killbill-config-magic + + + org.kill-bill.commons + killbill-xmlloader + + + org.kill-bill.billing + killbill-platform-osgi + + + org.kill-bill.billing.plugin + killbill-plugin-api-notification + + + org.kill-bill.billing + killbill-platform-lifecycle + + + org.kill-bill.commons + killbill-queue + + + org.kill-bill.commons + killbill-metrics-api + + + org.kill-bill.commons + killbill-clock + + + org.kill-bill.commons + killbill-embeddeddb-common + + + org.kill-bill.commons + killbill-jdbi + + + org.kill-bill.billing + killbill-platform-osgi-api + + + io.netty + netty-common + + + org.weakref + jmxutils + + + org.apache.shiro + shiro-guice + + + org.slf4j + jcl-over-slf4j + + + org.apache.shiro + shiro-core + + + org.slf4j + slf4j-api + + + + + org.kill-bill.billing + killbill-platform-api + 0.41.9 + + + org.kill-bill.billing + killbill-platform-base + 0.41.9 + + + org.kill-bill.billing + killbill-platform-lifecycle + 0.41.9 + + + org.kill-bill.billing + killbill-platform-osgi + 0.41.9 + + + org.kill-bill.billing + killbill-platform-osgi-api + 0.41.9 + + + org.kill-bill.billing.plugin + killbill-plugin-api-invoice + 0.27.3 + + + org.kill-bill.billing.plugin + killbill-plugin-api-notification + 0.27.1 + + + org.kill-bill.commons + killbill-clock + 0.26.2 + + + + org.kill-bill.commons + killbill-concurrent + 0.26.2 + + + org.kill-bill.commons + killbill-config-magic + 0.26.2 + + + org.kill-bill.commons + killbill-embeddeddb-common + 0.26.2 + + + org.kill-bill.commons + killbill-jdbi + 0.26.2 + + + org.kill-bill.commons + killbill-locker + 0.26.2 + + + org.kill-bill.commons + killbill-metrics-api + 0.26.0 + + + org.kill-bill.commons + killbill-queue + 0.26.2 + + + org.kill-bill.commons + killbill-utils + 0.26.2 + + + org.kill-bill.commons + killbill-xmlloader + 0.26.2 + + + org.mockito + mockito-core + 4.9.0 + + + net.bytebuddy + byte-buddy + + + net.bytebuddy + byte-buddy-agent + + + + + org.mockito + mockito-junit-jupiter + 4.9.0 + + + org.mockito + mockito-core + + + net.bytebuddy + byte-buddy + + + net.bytebuddy + byte-buddy-agent + + + + + org.reactivestreams + reactive-streams + 1.0.4 + + + org.slf4j + slf4j-api + 2.0.9 + + + org.weakref + jmxutils + 1.23 + + + + org.xerial.snappy + snappy-java + 1.1.10.4 + + + + + + com.fasterxml.jackson.core + jackson-core + com.google.code.findbugs jsr305 + + com.google.code.gson + gson + 2.10.1 + + com.google.guava guava + + com.google.inject + guice + com.samskivert jmustache @@ -101,12 +408,17 @@ joda-time joda-time - provided org.apache.commons commons-email 1.5 + + + com.sun.mail + javax.mail + + org.apache.felix @@ -120,11 +432,20 @@ org.jooby jooby + 1.6.9 ch.qos.logback logback-classic + + org.slf4j + slf4j-api + + + com.google.inject.extensions + guice-multibindings + @@ -138,11 +459,54 @@ + + + org.junit.jupiter + junit-jupiter-api + 5.11.4 + test + + + net.bytebuddy + byte-buddy + + + org.mockito + mockito-core + + + net.bytebuddy + byte-buddy-agent + + + org.kill-bill.billing killbill-api provided + + org.kill-bill.billing + killbill-client-java + 1.3.7 + + + org.slf4j + slf4j-api + + + + + org.kill-bill.billing + killbill-invoice + 0.24.10 + + + org.slf4j + slf4j-api + + + org.kill-bill.billing killbill-platform-osgi-api @@ -158,6 +522,17 @@ killbill-platform-test test + + org.kill-bill.billing.plugin + killbill-plugin-api-currency + 0.8.5 + provided + + + org.kill-bill.billing.plugin + killbill-plugin-api-invoice + 0.27.3 + org.kill-bill.billing.plugin killbill-plugin-api-notification @@ -218,9 +593,21 @@ testing-mysql-server test + org.mockito mockito-core + 4.9.0 + test + + + + + org.mockito + mockito-junit-jupiter + 5.15.2 test @@ -231,6 +618,7 @@ org.slf4j slf4j-simple + 2.0.9 test @@ -239,13 +627,34 @@ test + + + central + https://repo.maven.apache.org/maven2 + + + killbill + https://oss.sonatype.org/content/repositories/releases/ + + + killbill-snapshots + https://oss.sonatype.org/content/repositories/snapshots/ + + + + + src/test/resources + + org.apache.maven.plugins maven-enforcer-plugin + 3.3.0 + ${enforcer.skip} @@ -253,8 +662,35 @@ com.google.guava:guava:[12,) + + + org.apache.shiro:shiro-guice + org.xerial.snappy:snappy-java + + + + + enforce + + enforce + + + + + + + org.reactivestreams:reactive-streams + org.slf4j:slf4j-api + org.apache.shiro:shiro-core + io.netty:netty-common + + + + + + @@ -271,11 +707,24 @@ **/*.properties + **/*.iml + + org.apache.rat + apache-rat-plugin + 0.15 + + + **/*.md + **/*.json + **/target/** + + + diff --git a/src/main/java/org/killbill/billing/plugin/notification/generator/TemplateRenderer.java b/src/main/java/org/killbill/billing/plugin/notification/generator/TemplateRenderer.java index 07206fb..a8e9a0d 100644 --- a/src/main/java/org/killbill/billing/plugin/notification/generator/TemplateRenderer.java +++ b/src/main/java/org/killbill/billing/plugin/notification/generator/TemplateRenderer.java @@ -2,6 +2,7 @@ * Copyright 2010-2014 Ning, Inc. * Copyright 2014-2020 Groupon, Inc * Copyright 2014-2021 The Billing Project, LLC + * Copyright 2025 Tigase Inc. * * The Billing Project licenses this file to you under the Apache License, version 2.0 * (the "License"); you may not use this file except in compliance with the @@ -40,6 +41,7 @@ import org.killbill.billing.plugin.notification.email.EmailContent; import org.killbill.billing.plugin.notification.exception.EmailNotificationException; import org.killbill.billing.plugin.notification.generator.formatters.DefaultInvoiceFormatter; +import org.killbill.billing.plugin.notification.generator.formatters.tygrys.TygrysInvoiceFormatter; import org.killbill.billing.plugin.notification.generator.formatters.PaymentFormatter; import org.killbill.billing.plugin.notification.templates.TemplateEngine; import org.killbill.billing.plugin.notification.templates.TemplateType; @@ -113,16 +115,28 @@ private EmailContent getEmailContent(final TemplateType templateType, final Acco if (subscription != null) { data.put("subscription", subscription); } + if (invoice != null) { - // look for a custom InvoiceFormatter via our factory service tracker, if available + // Look for a custom InvoiceFormatter final InvoiceFormatterFactory formatterFactory = (invoiceFormatterTracker != null ? invoiceFormatterTracker.getService() : null); InvoiceFormatter formattedInvoice = (formatterFactory != null - ? formatterFactory.createInvoiceFormatter(text, invoice, locale, context) : null); - if ( formattedInvoice == null ) { + ? formatterFactory.createInvoiceFormatter(text, invoice, locale, context) : null); + if (formattedInvoice == null) { formattedInvoice = new DefaultInvoiceFormatter(text, invoice, locale); } + + // Add formatted invoice to the data model data.put("invoice", formattedInvoice); + + // If the formatter is TygrysInvoiceFormatter, inject invoice attribute "subscriptionName" + if (formattedInvoice instanceof TygrysInvoiceFormatter) { + final String subscriptionName = ((TygrysInvoiceFormatter) formattedInvoice).getSubscriptionName(); + if (subscriptionName != null) { + data.put("subscriptionName", subscriptionName); + } + } } + if (paymentTransaction != null) { final PaymentFormatter formattedPayment = new PaymentFormatter(paymentTransaction, locale); data.put("payment", formattedPayment); diff --git a/src/main/java/org/killbill/billing/plugin/notification/generator/formatters/tygrys/TygrysInvoiceFormatter.java b/src/main/java/org/killbill/billing/plugin/notification/generator/formatters/tygrys/TygrysInvoiceFormatter.java new file mode 100644 index 0000000..6a6dc89 --- /dev/null +++ b/src/main/java/org/killbill/billing/plugin/notification/generator/formatters/tygrys/TygrysInvoiceFormatter.java @@ -0,0 +1,212 @@ +package org.killbill.billing.plugin.notification.generator.formatters.tygrys; + +/* + * Copyright 2025 Tigase Inc. + * + * The Billing Project licenses this file to you under the Apache License, version 2.0 + * (the "License"); you may not use this file except in compliance with the + * License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import java.util.List; +import java.util.Map; +import java.util.Locale; +import java.net.http.HttpResponse; +import java.util.ResourceBundle; +import java.io.ByteArrayInputStream; +import java.net.HttpURLConnection; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.killbill.billing.client.KillBillHttpClient; +import org.killbill.billing.client.model.gen.CustomField; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.killbill.billing.invoice.api.formatters.InvoiceFormatter; +import org.killbill.billing.invoice.template.formatters.DefaultInvoiceFormatter; +import com.google.common.collect.ArrayListMultimap; +import com.google.common.collect.Multimap; +import org.killbill.billing.client.RequestOptions; +import org.killbill.billing.invoice.api.InvoiceItem; +import org.killbill.billing.invoice.api.Invoice; +import org.killbill.billing.client.KillBillClientException; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; + +import java.io.BufferedReader; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.stream.Collectors; +import java.time.format.DateTimeFormatter; +import com.google.common.collect.ArrayListMultimap; +import com.google.common.collect.Multimap; +import org.killbill.billing.currency.api.CurrencyConversionApi; + +public class TygrysInvoiceFormatter extends DefaultInvoiceFormatter { + + public static final String killbillLOCAL_URL = "http://localhost:8080"; + public static final String killbillENDPOINT = "/1.0/kb/customFields"; + public static final String killbillINVOICE_ENDPOINT = "/1.0/kb/invoices"; + public static final String killbillCUSTOM_FIELD_INVOICE_NAME = "name"; + public static final String invoiceNameUNNAMED_INVOICE = ""; + public static final String tygrysKILLBILL_DEFAULT_USER = "tygrys"; + public static final String tygrysKILLBILL_DEFAULT_PASSWD = "TBD"; + public static final String tygrysKILLBILL_API_KEY = "TBD"; + public static final String tygrysKILLBILL_API_SECRET = "TBD"; + + private static final DateTimeFormatter DEFAULT_DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd"); + private static final Locale DEFAULT_LOCALE = Locale.US; + + private KillBillHttpClient killBillClient; + private static final Logger logger = LoggerFactory.getLogger(TygrysInvoiceFormatter.class); + + private Invoice invoice; + public Invoice getInvoice() { return this.invoice; } + public void setInvoice(Invoice givenInvoice) { this.invoice = givenInvoice; } + + // Custom Invoice Attributes + private String subscriptionName; + + public TygrysInvoiceFormatter(final String defaultLocale, final String catalogBundlePath, final Invoice invoice, Locale locale, final CurrencyConversionApi currencyConversionApi, final ResourceBundle bundle, final ResourceBundle defaultBundle, KillBillHttpClient killBillClient) throws Exception { + super(defaultLocale, catalogBundlePath, invoice, locale, currencyConversionApi, bundle, defaultBundle); + this.killBillClient = killBillClient; + setInvoice(invoice); + logger.info("Instantiating TygrysInvoiceFormatter"); + } + + public TygrysInvoiceFormatter(final String defaultLocale, final String catalogBundlePath, final Invoice invoice, Locale locale, final CurrencyConversionApi currencyConversionApi, final ResourceBundle bundle, final ResourceBundle defaultBundle, final String killbillUrl, final String user, final String pwd, final String apiKey, final String apiSecret) throws Exception { + this(defaultLocale, defaultLocale, invoice, locale, currencyConversionApi, bundle, defaultBundle, new KillBillHttpClient(killbillUrl, user, pwd, apiKey, apiSecret)); + } + + public TygrysInvoiceFormatter(final String defaultLocale, final String catalogBundlePath, final Invoice invoice, Locale locale, final CurrencyConversionApi currencyConversionApi, final ResourceBundle bundle, final ResourceBundle defaultBundle, final String user, final String pwd, final String apiKey, final String apiSecret) throws Exception { + this(defaultLocale, catalogBundlePath, invoice, locale, currencyConversionApi, bundle, defaultBundle, killbillLOCAL_URL, user, pwd, apiKey, apiSecret); + } + + public TygrysInvoiceFormatter(final String defaultLocale, final String catalogBundlePath, final Invoice invoice, final CurrencyConversionApi currencyConversionApi, Locale locale, final ResourceBundle bundle, final ResourceBundle defaultBundle) throws Exception { + this(defaultLocale, catalogBundlePath, + invoice, locale, currencyConversionApi, bundle, defaultBundle, + tygrysKILLBILL_DEFAULT_USER, tygrysKILLBILL_DEFAULT_PASSWD, tygrysKILLBILL_API_KEY, tygrysKILLBILL_API_SECRET); + } + + static InputStream createNullInputStream() { + return new ByteArrayInputStream(new byte[0]); + } + + static boolean isNullInputStream(InputStream stream) { + // Check if the stream is a ByteArrayInputStream and its content length is zero + if (stream instanceof ByteArrayInputStream) { + ByteArrayInputStream byteArrayInputStream = (ByteArrayInputStream) stream; + return byteArrayInputStream.available() == 0; + } + return false; // Not a ByteArrayInputStream or not empty + } + + /* + * getCustomAttributesFromKillBill() gets the custo attributes associated with the + * given invoice (identified by invoide id) from KillBill. The implementation can use + * REST or JDBC to dip into the killbill database. + * + * @param invoiceId the Identity of the invoice in question + * @returns stream of custom attributes. + */ + public InputStream getCustomAttributesFromKillBill(final String invoiceId) throws KillBillClientException { + Multimap queryParms = ArrayListMultimap.create(); + queryParms.put("objectId", invoiceId); + queryParms.put("objectType", "INVOICE"); + + RequestOptions requestOptions = RequestOptions.builder() + .withQueryParams(queryParms.asMap()) + .build(); + + logger.info("getCustomAttributesFromKillBill: requestOptions = " + requestOptions); + final String endpoint = killbillENDPOINT + "?objectId=" + invoiceId + "&objectType=INVOICE"; + logger.info("getCustomAttributesFromKillBill: endpoint = " + endpoint); + + final HttpResponse httpResp = killBillClient.doGet(endpoint, requestOptions); + logger.info("getCustomAttributesFromKillBill: httpResp status = " + httpResp.statusCode()); + + // Be prepared to yield a null input stream... + return httpResp.body(); + } + + public String extractAttributesFromStream(InputStream attributeStream) { + String attributes = ""; + try (BufferedReader reader = new BufferedReader( + new InputStreamReader(attributeStream, StandardCharsets.UTF_8))) { + attributes = reader.lines().collect(Collectors.joining("\n")); + } catch (Exception e) { + e.printStackTrace(); + logger.error("Exception while extracting invoice name from the input stream: ", e); + } + + + logger.info("extractAttributesFromStream : attributes = " + attributes); + return attributes; // Empty string of attributes... + } + + /* + * Extract the invoice name fro the list of custom attributes returned from KillBill. + * + * @param customFields the list of custom attributes. + * @returns name of the invoice or invoiceNameUNNAMED_INVOICE, if none is found. + */ + public String extractInvoiceName(final List customFields) { + logger.info("extractInvoiceName : customFields = " + customFields); + return customFields.stream() + .filter(field -> killbillCUSTOM_FIELD_INVOICE_NAME.equals(field.getName())) + .map(CustomField::getValue) + .findFirst() + .orElse(invoiceNameUNNAMED_INVOICE); + } + + /* + * Method to retrieve the custom invoice atribute, per the guidelines in + * customization of email notification template. + */ + public String getSubscriptionName() { + if (this.subscriptionName != null && !this.subscriptionName.isBlank()) { + // Subscrition name already computed - yield cached value + return this.subscriptionName; + } + + if (invoice == null) { + logger.error("getInvoiceName: invoiceId is invoiceNameUNNAMED_INVOICE"); + return invoiceNameUNNAMED_INVOICE; + } + + final String invoiceId = invoice.getId().toString(); + logger.info("getInvoiceName: invoiceId = " + invoiceId); + try { + InputStream attributeStream = getCustomAttributesFromKillBill(invoiceId); + if (isNullInputStream(attributeStream)) { + return invoiceNameUNNAMED_INVOICE; + } + + // Convert the InputStream to a String + final String attributesStr = extractAttributesFromStream(attributeStream); + + // Parse the JSON response into a list of CustomField objects + ObjectMapper objectMapper = new ObjectMapper(); + final List customFields = objectMapper.readValue(attributesStr, new TypeReference>() {}); + this.subscriptionName = extractInvoiceName(customFields); + logger.info("Successfully extracted '" + this.subscriptionName + "' as the name of the invoice with ID: " + invoiceId); + return this.subscriptionName; + } catch (Exception e) { + e.printStackTrace(); + logger.error("Exception while retrieving invoice name: ", e); + } + + return invoiceNameUNNAMED_INVOICE; + } +} diff --git a/src/main/java/org/killbill/billing/plugin/notification/generator/formatters/tygrys/factory/TygrysInvoiceFormatterFactory.java b/src/main/java/org/killbill/billing/plugin/notification/generator/formatters/tygrys/factory/TygrysInvoiceFormatterFactory.java new file mode 100644 index 0000000..035e6ed --- /dev/null +++ b/src/main/java/org/killbill/billing/plugin/notification/generator/formatters/tygrys/factory/TygrysInvoiceFormatterFactory.java @@ -0,0 +1,80 @@ +package org.killbill.billing.plugin.notification.generator.formatters.tygrys.factory; + +/* + * Copyright 2025 Tigase Inc. + * + * The Billing Project licenses this file to you under the Apache License, version 2.0 + * (the "License"); you may not use this file except in compliance with the + * License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import java.io.PrintWriter; +import java.io.StringWriter; +import java.util.List; +import java.util.Map; +import java.util.Locale; +import java.net.http.HttpResponse; +import java.util.Locale; +import java.util.ResourceBundle; +import java.util.List; +import java.nio.charset.StandardCharsets; +import java.time.format.DateTimeFormatter; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.killbill.billing.currency.api.CurrencyConversionApi; +import org.killbill.billing.invoice.api.Invoice; +import org.killbill.billing.invoice.api.formatters.InvoiceFormatter; +import org.killbill.billing.invoice.plugin.api.InvoiceFormatterFactory; +import org.killbill.billing.plugin.notification.generator.formatters.tygrys.TygrysInvoiceFormatter; +import org.killbill.billing.invoice.template.formatters.DefaultInvoiceFormatter; + +public class TygrysInvoiceFormatterFactory implements InvoiceFormatterFactory { + + private static final Logger logger = LoggerFactory.getLogger(TygrysInvoiceFormatterFactory.class); + + public TygrysInvoiceFormatterFactory() { + super(); + logger.info("Initialize TygrysInvoiceFormatterFactory"); + } + + @Override + public InvoiceFormatter createInvoiceFormatter(final String defaultLocale, final String catalogBundlePath, + final Invoice invoice, final Locale locale, + final CurrencyConversionApi currencyConversionApi, + ResourceBundle bundle, ResourceBundle defaultBundle) { + try { + return new TygrysInvoiceFormatter(defaultLocale, catalogBundlePath, invoice, currencyConversionApi, locale, + bundle, defaultBundle); + } + catch (Exception exc) { + logger.error("Failed to instantiate TygrysInvoiceFormatter due to exception: %s", getStackTraceAsString(exc)); + } + + /* + * If we fail to instantiate TygrysInvoiceFormatter, yield an instance of DefaltInvoiceFormatter - this will + * not yield the custom attributes, but will print the basic invoice. + */ + logger.warn("Yielding an instance of DefaultInvoiceFormatter - custom attributes will be elided."); + return new DefaultInvoiceFormatter(defaultLocale, catalogBundlePath, invoice, locale, currencyConversionApi, bundle, defaultBundle); + } + + public static String getStackTraceAsString(final Exception e) { + StringWriter sw = new StringWriter(); + PrintWriter pw = new PrintWriter(sw); + e.printStackTrace(pw); + return sw.toString(); + } +} diff --git a/src/main/resources/org/killbill/billing/plugin/notification/templates/InvoiceCreation.mustache b/src/main/resources/org/killbill/billing/plugin/notification/templates/InvoiceCreation.mustache index 10b68bc..589d9cc 100644 --- a/src/main/resources/org/killbill/billing/plugin/notification/templates/InvoiceCreation.mustache +++ b/src/main/resources/org/killbill/billing/plugin/notification/templates/InvoiceCreation.mustache @@ -24,6 +24,7 @@ {{text.invoiceTitle}} {{text.invoicePrefix}}{{invoice.invoiceNumber}}
+ Subscription: {{subscriptionName}}
{{text.invoiceDate}}{{invoice.formattedInvoiceDate}} @@ -92,4 +93,4 @@ - \ No newline at end of file + diff --git a/src/test/java/org/killbill/billing/plugin/notification/generator/formatters/tygrys/TygrysInvoiceFormatterTest.java b/src/test/java/org/killbill/billing/plugin/notification/generator/formatters/tygrys/TygrysInvoiceFormatterTest.java new file mode 100644 index 0000000..ca577ab --- /dev/null +++ b/src/test/java/org/killbill/billing/plugin/notification/generator/formatters/tygrys/TygrysInvoiceFormatterTest.java @@ -0,0 +1,234 @@ +package org.killbill.billing.plugin.notification.generator.formatters.tygrys; + +/* + * Copyright 2025 Tigase Inc. + * + * The Billing Project licenses this file to you under the Apache License, version 2.0 + * (the "License"); you may not use this file except in compliance with the + * License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.util.Locale; +import java.util.ResourceBundle; +import java.util.UUID; +import java.net.http.HttpResponse; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import com.google.gson.Gson; +import java.util.concurrent.ThreadLocalRandom; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.Disabled; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; +import org.mockito.MockitoAnnotations; +import org.mockito.Mock; +import org.mockito.InjectMocks; + +import org.killbill.billing.client.KillBillHttpClient; +import org.killbill.billing.invoice.api.Invoice; +import org.killbill.billing.currency.api.CurrencyConversionApi; + +class TygrysInvoiceFormatterTest { + + private static final Logger logger = LoggerFactory.getLogger(TygrysInvoiceFormatterTest.class); + + @Mock private KillBillHttpClient mockKillBillClient; + @Mock private Invoice mockInvoice; + @InjectMocks private TygrysInvoiceFormatter formatter; + @BeforeEach void setUp() throws Exception { + MockitoAnnotations.openMocks(this); + + // Mock Invoice ID + when(mockInvoice.getId()).thenReturn(UUID.randomUUID()); + + // Initialize the TygrysInvoiceFormatter + formatter = new TygrysInvoiceFormatter( + "en_US", + "/path/to/catalogBundle", + mockInvoice, + Locale.US, + mock(CurrencyConversionApi.class), + ResourceBundle.getBundle("defaultBundle"), + ResourceBundle.getBundle("customBundle"), + mockKillBillClient + ); + } + + private String customAttributeUrl(final String invoiceId) { + return String.format("%s?objectId=%s&objectType=INVOICE", TygrysInvoiceFormatter.killbillENDPOINT, invoiceId); + } + + private String createFakeStream(final String invoiceName) { + return createFakeStream(invoiceName, false); + } + + private String createFakeStream(final String invoiceName, final Boolean generateSecondaryAttrs) { + Gson gson = new Gson(); + List> attributesList = new ArrayList<>(); + logger.info("createFakeStream: invoiceName = " + (invoiceName == null ? "":invoiceName)); + + + if (invoiceName != null) { + // Add invoice attributes + Map attribute1 = new HashMap<>(); + attribute1.put("name", TygrysInvoiceFormatter.killbillCUSTOM_FIELD_INVOICE_NAME); + attribute1.put("value", invoiceName); + attributesList.add(attribute1); + } + + if (generateSecondaryAttrs) { + + // Generate a random number of additional attributes + int numberOfRandomAttributes = ThreadLocalRandom.current().nextInt(3, 10); // Random number between 3 and 10 + for (int i = 0; i < numberOfRandomAttributes; i++) { + Map randomAttribute = new HashMap<>(); + randomAttribute.put("name", "randomName" + i); // Unique random name + randomAttribute.put("value", "randomValue" + ThreadLocalRandom.current().nextInt(1, 100)); // Random value + attributesList.add(randomAttribute); + } + } + + // Return JSon format.. + return gson.toJson(attributesList); + } + + @Test + void testGetCustomAttributesFromKillBill_withStubbedCustomAttributes() throws Exception { + // Mock the InputStream to simulate KillBill API response + final String invoiceName = "Test-Invoice"; + final String mockResponse = createFakeStream(invoiceName); + InputStream mockStream = new ByteArrayInputStream(mockResponse.getBytes()); + + // Mock the Invoice ID + UUID invoiceId = UUID.randomUUID(); + when(mockInvoice.getId()).thenReturn(invoiceId); + + // Stub the getCustomAttributesFromKillBill method + when(mockKillBillClient.doGet(anyString(), any())) + .thenReturn(mock(HttpResponse.class)); + when(mockKillBillClient.doGet(anyString(), any()).body()).thenReturn(mockStream); + + // Expected endpoint + final String expectedEndpoint = customAttributeUrl(invoiceId.toString()); + + // Call the method under test + InputStream attributeStream = formatter.getCustomAttributesFromKillBill(invoiceId.toString()); + + // Verify and assert results + verify(mockKillBillClient).doGet(eq(expectedEndpoint), any()); // Verify the expected endpoint + } + + @Test void testExtractAttributesFromStream_withStubbedCustomAttributes() throws Exception { + // Create parameterized mock stream + final String givenInvoiceName = "CocaCola-Invoice"; + final String mockResponse = createFakeStream(givenInvoiceName); + InputStream mockStream = new ByteArrayInputStream(mockResponse.getBytes()); + + // Mock the Invoice ID + UUID invoiceId = UUID.randomUUID(); + when(mockInvoice.getId()).thenReturn(invoiceId); + + // Stub the getCustomAttributesFromKillBill method + when(mockKillBillClient.doGet(anyString(), any())) + .thenReturn(mock(HttpResponse.class)); + when(mockKillBillClient.doGet(anyString(), any()).body()).thenReturn(mockStream); + + // Expected endpoint + final String expectedEndpoint = customAttributeUrl(invoiceId.toString()); + + // Call the method under test + InputStream attributeStream = formatter.getCustomAttributesFromKillBill(invoiceId.toString()); + final String attrs = formatter.extractAttributesFromStream(attributeStream); + + // Verify the expected endpoint + verify(mockKillBillClient).doGet(eq(expectedEndpoint), any()); + + logger.info("testExtractAttributesFromStream_withStubbedCustomAttributes: attrs = " + attrs); + logger.info("testExtractAttributesFromStream_withStubbedCustomAttributes: mockResponse = " + mockResponse); + assertEquals(mockResponse, attrs); + } + + @Test + void testGetInvoiceName_withNullInvoice() throws Exception { + // Test behaviour when the invoice is null - negative test cases + formatter.setInvoice(null); + + // Create parameterized mock stream + final String givenInvoiceName = "CocaCola-Invoice"; + final String mockResponse = createFakeStream(givenInvoiceName); + logger.info("testGetInvoiceName_withNullInvoice: mockResponse = " + mockResponse); + logger.info("testGetInvoiceName_withNullInvoice: mockResponse bytes = " + mockResponse.getBytes()); + + InputStream mockStream = new ByteArrayInputStream(mockResponse.getBytes()); + logger.info("testGetInvoiceName_withNullInvoice: is null mockResponse? = " + TygrysInvoiceFormatter.isNullInputStream(mockStream)); + + // Call the method under test + final String retrievedInvoiceName = formatter.getSubscriptionName(); + logger.info("testGetInvoiceName_withNullInvoice: retrieved invoiceName = " + retrievedInvoiceName); + + // Verify and assert results + verifyNoInteractions(mockKillBillClient); // Because invoice is null, formatter does not interact with killbill at all. + assertEquals(TygrysInvoiceFormatter.invoiceNameUNNAMED_INVOICE, retrievedInvoiceName); + } + + // Test behaviour when the invoice tag is missing - negative test cases + @Test + void testGetInvoiceName_withMissingInvoiceNameTag() throws Exception { + // Create parameterized mock stream + final String givenInvoiceName = null; + + // Mock Invoice ID + final UUID randomUuid = UUID.randomUUID(); + when(mockInvoice.getId()).thenReturn(randomUuid); + + // Expected endpoint + final String expectedEndpoint = customAttributeUrl(randomUuid.toString()); + + final String mockResponse = createFakeStream(givenInvoiceName); + InputStream mockStream = new ByteArrayInputStream(mockResponse.getBytes()); + + // Call the method under test + final String retrievedInvoiceName = formatter.getSubscriptionName(); + logger.info("testGetInvoiceName_withNullInvoice: retrieved invoiceName = " + retrievedInvoiceName); + + // Verify and assert results + verify(mockKillBillClient).doGet(eq(expectedEndpoint), any()); // Verify the expected endpoint + assertEquals(TygrysInvoiceFormatter.invoiceNameUNNAMED_INVOICE, retrievedInvoiceName); + } + + @Test + void testGetInvoiceName_withStubbedCustomAttributes() throws Exception { + // Create parameterized mock stream + final String givenInvoiceName = "AlwaysUpNetworks-Invoice"; + final String mockResponse = createFakeStream(givenInvoiceName, true); + InputStream mockStream = new ByteArrayInputStream(mockResponse.getBytes()); + + // Stub the getCustomAttributesFromKillBill method + when(mockKillBillClient.doGet(anyString(), any())) + .thenReturn(mock(HttpResponse.class)); + when(mockKillBillClient.doGet(anyString(), any()).body()).thenReturn(mockStream); + + // Call the method under test + final String retrievedInvoiceName = formatter.getSubscriptionName(); + logger.info("testGetInvoiceName_withStubbedCustomAttributes: retrieved invoiceName = " + retrievedInvoiceName); + + // Verify and assert results + verify(mockKillBillClient).doGet(contains(TygrysInvoiceFormatter.killbillENDPOINT), any()); + assertEquals(givenInvoiceName, retrievedInvoiceName); + } +} diff --git a/src/test/resources/customBundle_en_US.properties b/src/test/resources/customBundle_en_US.properties new file mode 100644 index 0000000..48580bf --- /dev/null +++ b/src/test/resources/customBundle_en_US.properties @@ -0,0 +1,2 @@ +key1=value1 +key2=value2 diff --git a/src/test/resources/defaultBundle.properties b/src/test/resources/defaultBundle.properties new file mode 100644 index 0000000..48580bf --- /dev/null +++ b/src/test/resources/defaultBundle.properties @@ -0,0 +1,2 @@ +key1=value1 +key2=value2 diff --git a/src/test/resources/defaultBundle_en_US.properties b/src/test/resources/defaultBundle_en_US.properties new file mode 100644 index 0000000..48580bf --- /dev/null +++ b/src/test/resources/defaultBundle_en_US.properties @@ -0,0 +1,2 @@ +key1=value1 +key2=value2