killbill-memoizeit

payment: add APIs to retrieve all payments for a given tenant Signed-off-by:

1/9/2014 1:08:27 PM

Details

diff --git a/jaxrs/src/main/java/com/ning/billing/jaxrs/resources/JaxrsResource.java b/jaxrs/src/main/java/com/ning/billing/jaxrs/resources/JaxrsResource.java
index f7046e9..0234816 100644
--- a/jaxrs/src/main/java/com/ning/billing/jaxrs/resources/JaxrsResource.java
+++ b/jaxrs/src/main/java/com/ning/billing/jaxrs/resources/JaxrsResource.java
@@ -80,6 +80,7 @@ public interface JaxrsResource {
 
     public static final String QUERY_PAYMENT_EXTERNAL = "externalPayment";
     public static final String QUERY_PAYMENT_WITH_REFUNDS_AND_CHARGEBACKS = "withRefundsAndChargebacks";
+    public static final String QUERY_PAYMENT_PLUGIN_NAME = "pluginName";
 
     public static final String QUERY_TAGS = "tagList";
     public static final String QUERY_TAGS_INCLUDED_DELETED = "includedDeleted";
diff --git a/jaxrs/src/main/java/com/ning/billing/jaxrs/resources/PaymentResource.java b/jaxrs/src/main/java/com/ning/billing/jaxrs/resources/PaymentResource.java
index 8f99a77..2856f16 100644
--- a/jaxrs/src/main/java/com/ning/billing/jaxrs/resources/PaymentResource.java
+++ b/jaxrs/src/main/java/com/ning/billing/jaxrs/resources/PaymentResource.java
@@ -16,7 +16,10 @@
 
 package com.ning.billing.jaxrs.resources;
 
+import java.io.IOException;
+import java.io.OutputStream;
 import java.math.BigDecimal;
+import java.net.URI;
 import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.List;
@@ -35,8 +38,10 @@ import javax.ws.rs.Path;
 import javax.ws.rs.PathParam;
 import javax.ws.rs.Produces;
 import javax.ws.rs.QueryParam;
+import javax.ws.rs.WebApplicationException;
 import javax.ws.rs.core.Response;
 import javax.ws.rs.core.Response.Status;
+import javax.ws.rs.core.StreamingOutput;
 import javax.ws.rs.core.UriInfo;
 
 import com.ning.billing.ObjectType;
@@ -66,9 +71,13 @@ import com.ning.billing.util.api.TagDefinitionApiException;
 import com.ning.billing.util.api.TagUserApi;
 import com.ning.billing.util.callcontext.CallContext;
 import com.ning.billing.util.callcontext.TenantContext;
+import com.ning.billing.util.entity.Pagination;
 
+import com.fasterxml.jackson.core.JsonGenerator;
 import com.google.common.base.Function;
+import com.google.common.base.Strings;
 import com.google.common.collect.Collections2;
+import com.google.common.collect.ImmutableMap;
 import com.google.inject.Inject;
 
 import static javax.ws.rs.core.MediaType.APPLICATION_JSON;
@@ -130,6 +139,76 @@ public class PaymentResource extends JaxRsResourceBase {
         return Response.status(Status.OK).entity(paymentJson).build();
     }
 
+    @GET
+    @Path("/" + PAGINATION)
+    @Produces(APPLICATION_JSON)
+    public Response getPayments(@QueryParam(QUERY_SEARCH_OFFSET) @DefaultValue("0") final Long offset,
+                                @QueryParam(QUERY_SEARCH_LIMIT) @DefaultValue("100") final Long limit,
+                                @QueryParam(QUERY_PAYMENT_PLUGIN_NAME) final String pluginName,
+                                @javax.ws.rs.core.Context final HttpServletRequest request) throws PaymentApiException {
+        final TenantContext tenantContext = context.createContext(request);
+
+        final Pagination<Payment> payments;
+        final Map<String, String> nextUriParams = new HashMap<String, String>();
+        if (Strings.isNullOrEmpty(pluginName)) {
+            payments = paymentApi.getPayments(offset, limit, tenantContext);
+        } else {
+            payments = paymentApi.getPayments(offset, limit, pluginName, tenantContext);
+            nextUriParams.put(QUERY_PAYMENT_PLUGIN_NAME, pluginName);
+        }
+
+        final URI nextPageUri = uriBuilder.nextPage(PaymentResource.class, "getPayments", payments.getNextOffset(), limit, nextUriParams);
+        return buildStreamingPaymentsResponse(payments, nextPageUri);
+    }
+
+    @GET
+    @Path("/" + SEARCH + "/{searchKey:" + ANYTHING_PATTERN + "}")
+    @Produces(APPLICATION_JSON)
+    public Response searchPayments(@PathParam("searchKey") final String searchKey,
+                                   @QueryParam(QUERY_SEARCH_OFFSET) @DefaultValue("0") final Long offset,
+                                   @QueryParam(QUERY_SEARCH_LIMIT) @DefaultValue("100") final Long limit,
+                                   @QueryParam(QUERY_PAYMENT_PLUGIN_NAME) final String pluginName,
+                                   @javax.ws.rs.core.Context final HttpServletRequest request) throws PaymentApiException, AccountApiException {
+        final TenantContext tenantContext = context.createContext(request);
+
+        // Search the plugin(s)
+        final Pagination<Payment> payments;
+        if (Strings.isNullOrEmpty(pluginName)) {
+            payments = paymentApi.searchPayments(searchKey, offset, limit, tenantContext);
+        } else {
+            payments = paymentApi.searchPayments(searchKey, offset, limit, pluginName, tenantContext);
+        }
+
+        final URI nextPageUri = uriBuilder.nextPage(PaymentResource.class, "searchPayments", payments.getNextOffset(), limit, ImmutableMap.<String, String>of());
+        return buildStreamingPaymentsResponse(payments, nextPageUri);
+    }
+
+    private Response buildStreamingPaymentsResponse(final Pagination<Payment> payments, final URI nextPageUri) {
+        final StreamingOutput json = new StreamingOutput() {
+            @Override
+            public void write(final OutputStream output) throws IOException, WebApplicationException {
+                final JsonGenerator generator = mapper.getFactory().createJsonGenerator(output);
+                generator.configure(JsonGenerator.Feature.AUTO_CLOSE_TARGET, false);
+
+                generator.writeStartArray();
+                for (final Payment payment : payments) {
+                    final PaymentJson asJson = new PaymentJson(payment, null);
+                    generator.writeObject(asJson);
+                }
+                generator.writeEndArray();
+                generator.close();
+            }
+        };
+        return Response.status(Status.OK)
+                       .entity(json)
+                       .header(HDR_PAGINATION_CURRENT_OFFSET, payments.getCurrentOffset())
+                       .header(HDR_PAGINATION_NEXT_OFFSET, payments.getNextOffset())
+                       .header(HDR_PAGINATION_TOTAL_NB_RECORDS, payments.getTotalNbRecords())
+                       .header(HDR_PAGINATION_MAX_NB_RECORDS, payments.getMaxNbRecords())
+                       .header(HDR_PAGINATION_NEXT_PAGE_URI, nextPageUri)
+                       .build();
+    }
+
     @PUT
     @Path("/{paymentId:" + UUID_PATTERN + "}")
     @Consumes(APPLICATION_JSON)

NEWS 3(+2 -1)

diff --git a/NEWS b/NEWS
index 2038119..8922870 100644
--- a/NEWS
+++ b/NEWS
@@ -1,5 +1,6 @@
 0.8.8
-    Update killbill-oss-parent to 0.5.14
+    Add APIs to retrieve all payments for a given tenant
+    Update killbill-oss-parent to 0.5.15
 
 0.8.7
     DDL: remove unused paid_through_date column
diff --git a/payment/src/main/java/com/ning/billing/payment/api/DefaultPaymentApi.java b/payment/src/main/java/com/ning/billing/payment/api/DefaultPaymentApi.java
index 0c0173b..e1777d2 100644
--- a/payment/src/main/java/com/ning/billing/payment/api/DefaultPaymentApi.java
+++ b/payment/src/main/java/com/ning/billing/payment/api/DefaultPaymentApi.java
@@ -87,6 +87,16 @@ public class DefaultPaymentApi implements PaymentApi {
     }
 
     @Override
+    public Pagination<Payment> getPayments(final Long offset, final Long limit, final TenantContext context) {
+        return paymentProcessor.getPayments(offset, limit, context, internalCallContextFactory.createInternalTenantContext(context));
+    }
+
+    @Override
+    public Pagination<Payment> getPayments(final Long offset, final Long limit, final String pluginName, final TenantContext tenantContext) throws PaymentApiException {
+        return paymentProcessor.getPayments(offset, limit, pluginName, tenantContext, internalCallContextFactory.createInternalTenantContext(tenantContext));
+    }
+
+    @Override
     public Payment getPayment(final UUID paymentId, final boolean withPluginInfo, final TenantContext context) throws PaymentApiException {
         final Payment payment = paymentProcessor.getPayment(paymentId, withPluginInfo, internalCallContextFactory.createInternalTenantContext(context));
         if (payment == null) {
@@ -133,7 +143,7 @@ public class DefaultPaymentApi implements PaymentApi {
     @Override
     public void notifyPendingRefundOfStateChanged(final Account account, final UUID refundId, final boolean isSuccess, final CallContext context) throws PaymentApiException {
         refundProcessor.notifyPendingRefundOfStateChanged(account, refundId, isSuccess,
-                                                                 internalCallContextFactory.createInternalCallContext(account.getId(), context));
+                                                          internalCallContextFactory.createInternalCallContext(account.getId(), context));
     }
 
     @Override
diff --git a/payment/src/main/java/com/ning/billing/payment/core/PaymentProcessor.java b/payment/src/main/java/com/ning/billing/payment/core/PaymentProcessor.java
index db6fcad..3f36672 100644
--- a/payment/src/main/java/com/ning/billing/payment/core/PaymentProcessor.java
+++ b/payment/src/main/java/com/ning/billing/payment/core/PaymentProcessor.java
@@ -69,6 +69,7 @@ import com.ning.billing.payment.retry.AutoPayRetryService.AutoPayRetryServiceSch
 import com.ning.billing.payment.retry.FailedPaymentRetryService.FailedPaymentRetryServiceScheduler;
 import com.ning.billing.payment.retry.PluginFailureRetryService.PluginFailureRetryServiceScheduler;
 import com.ning.billing.tag.TagInternalApi;
+import com.ning.billing.util.callcontext.TenantContext;
 import com.ning.billing.util.config.PaymentConfig;
 import com.ning.billing.util.dao.NonEntityDao;
 import com.ning.billing.util.entity.DefaultPagination;
@@ -147,6 +148,72 @@ public class PaymentProcessor extends ProcessorBase {
         return fromPaymentModelDao(model, pluginInfo, context);
     }
 
+    public Pagination<Payment> getPayments(final Long offset, final Long limit, final TenantContext tenantContext, final InternalTenantContext internalTenantContext) {
+        // Note that we cannot easily do streaming here, since we would have to rely on the statistics
+        // returned by the Pagination objects from the plugins and we probably don't want to do that (if
+        // one plugin gets it wrong, it may starve the others).
+        final List<Payment> allResults = new LinkedList<Payment>();
+        Long totalNbRecords = 0L;
+        Long maxNbRecords = 0L;
+
+        // Search in all plugins (we treat the full set of results as a union with respect to offset/limit)
+        boolean firstSearch = true;
+        for (final String pluginName : getAvailablePlugins()) {
+            try {
+                final Pagination<Payment> payments;
+                if (allResults.size() >= limit) {
+                    // We have enough results, we just keep going (limit 1) to get the stats
+                    payments = getPayments(firstSearch ? offset : 0L, 1L, pluginName, tenantContext, internalTenantContext);
+                    // Required to close database connections
+                    ImmutableList.<Payment>copyOf(payments);
+                } else {
+                    payments = getPayments(firstSearch ? offset : 0L, limit - allResults.size(), pluginName, tenantContext, internalTenantContext);
+                    allResults.addAll(ImmutableList.<Payment>copyOf(payments));
+                }
+                firstSearch = false;
+                totalNbRecords += payments.getTotalNbRecords();
+                maxNbRecords += payments.getMaxNbRecords();
+            } catch (final PaymentApiException e) {
+                log.warn("Error while searching plugin " + pluginName, e);
+                // Non-fatal, continue to search other plugins
+            }
+        }
+
+        return new DefaultPagination<Payment>(offset, limit, totalNbRecords, maxNbRecords, allResults.iterator());
+    }
+
+    public Pagination<Payment> getPayments(final Long offset, final Long limit, final String pluginName, final TenantContext tenantContext, final InternalTenantContext internalTenantContext) throws PaymentApiException {
+        final PaymentPluginApi pluginApi = getPaymentPluginApi(pluginName);
+
+        // Find all payments for all accounts
+        final Pagination<PaymentModelDao> paymentModelDaos = paymentDao.getPayments(pluginName, offset, limit, internalTenantContext);
+
+        return new DefaultPagination<Payment>(paymentModelDaos,
+                                              limit,
+                                              Iterators.<Payment>filter(Iterators.<PaymentModelDao, Payment>transform(paymentModelDaos.iterator(),
+                                                                                                                      new Function<PaymentModelDao, Payment>() {
+                                                                                                                          @Override
+                                                                                                                          public Payment apply(final PaymentModelDao paymentModelDao) {
+                                                                                                                              final PaymentInfoPlugin pluginInfo;
+                                                                                                                              try {
+                                                                                                                                  pluginInfo = pluginApi.getPaymentInfo(paymentModelDao.getAccountId(), paymentModelDao.getId(), tenantContext);
+                                                                                                                              } catch (final PaymentPluginApiException e) {
+                                                                                                                                  log.warn("Unable to find payment  id " + paymentModelDao.getId() + " in plugin " + pluginName);
+                                                                                                                                  return null;
+                                                                                                                              }
+
+                                                                                                                              if (pluginInfo.getKbPaymentId() == null) {
+                                                                                                                                  // Garbage from the plugin?
+                                                                                                                                  log.debug("Plugin {} returned a payment without a kbPaymentId", pluginName);
+                                                                                                                                  return null;
+                                                                                                                              }
+
+                                                                                                                              return fromPaymentModelDao(paymentModelDao, pluginInfo, internalTenantContext);
+                                                                                                                          }
+                                                                                                                      }),
+                                                                        Predicates.<Payment>notNull()));
+    }
+
     public Pagination<Payment> searchPayments(final String searchKey, final Long offset, final Long limit, final InternalTenantContext internalTenantContext) {
         // Note that we cannot easily do streaming here, since we would have to rely on the statistics
         // returned by the Pagination objects from the plugins and we probably don't want to do that (if
@@ -403,7 +470,7 @@ public class PaymentProcessor extends ProcessorBase {
                 final List<PaymentAttemptModelDao> attempts = paymentDao.getAttemptsForPayment(paymentId, context);
                 final PaymentAttemptModelDao lastAttempt = attempts.get(attempts.size() - 1);
                 final PaymentStatus newPaymentStatus = isSuccess ? PaymentStatus.SUCCESS : PaymentStatus.PAYMENT_FAILURE_ABORTED;
-                paymentDao.updatePaymentAndAttemptOnCompletion(paymentId, newPaymentStatus, payment.getProcessedAmount(), payment.getProcessedCurrency(), lastAttempt.getId(),null, null, context);
+                paymentDao.updatePaymentAndAttemptOnCompletion(paymentId, newPaymentStatus, payment.getProcessedAmount(), payment.getProcessedCurrency(), lastAttempt.getId(), null, null, context);
                 return null;
             }
         });
diff --git a/payment/src/main/java/com/ning/billing/payment/dao/DefaultPaymentDao.java b/payment/src/main/java/com/ning/billing/payment/dao/DefaultPaymentDao.java
index ccc0e17..8d3cf23 100644
--- a/payment/src/main/java/com/ning/billing/payment/dao/DefaultPaymentDao.java
+++ b/payment/src/main/java/com/ning/billing/payment/dao/DefaultPaymentDao.java
@@ -54,7 +54,6 @@ public class DefaultPaymentDao implements PaymentDao {
         this.transactionalSqlDao = new EntitySqlDaoTransactionalJdbiWrapper(dbi, clock, cacheControllerDispatcher, nonEntityDao);
     }
 
-
     @Override
     public PaymentAttemptModelDao getPaymentAttempt(final UUID attemptId, final InternalTenantContext context) {
         return transactionalSqlDao.execute(new EntitySqlDaoTransactionWrapper<PaymentAttemptModelDao>() {
@@ -74,7 +73,6 @@ public class DefaultPaymentDao implements PaymentDao {
                 final PaymentSqlDao transactional = entitySqlDaoWrapperFactory.become(PaymentSqlDao.class);
                 transactional.create(payment, context);
 
-
                 entitySqlDaoWrapperFactory.become(PaymentAttemptSqlDao.class).create(attempt, context);
 
                 return transactional.getById(payment.getId().toString(), context);
@@ -301,6 +299,34 @@ public class DefaultPaymentDao implements PaymentDao {
     }
 
     @Override
+    public Pagination<PaymentModelDao> getPayments(final String pluginName, final Long offset, final Long limit, final InternalTenantContext context) {
+        // Note: the connection will be busy as we stream the results out: hence we cannot use
+        // SQL_CALC_FOUND_ROWS / FOUND_ROWS on the actual query.
+        // We still need to know the actual number of results, mainly for the UI so that it knows if it needs to fetch
+        // more pages. To do that, we perform a dummy search query with SQL_CALC_FOUND_ROWS (but limit 1).
+        final Long count = transactionalSqlDao.execute(new EntitySqlDaoTransactionWrapper<Long>() {
+            @Override
+            public Long inTransaction(final EntitySqlDaoWrapperFactory<EntitySqlDao> entitySqlDaoWrapperFactory) throws Exception {
+                final PaymentSqlDao sqlDao = entitySqlDaoWrapperFactory.become(PaymentSqlDao.class);
+                final Iterator<PaymentModelDao> dumbIterator = sqlDao.getByPluginName(pluginName, offset, 1L, context);
+                // Make sure to go through the results to close the connection
+                while (dumbIterator.hasNext()) {
+                    dumbIterator.next();
+                }
+                return sqlDao.getFoundRows(context);
+            }
+        });
+
+        // We usually always want to wrap our queries in an EntitySqlDaoTransactionWrapper... except here.
+        // Since we want to stream the results out, we don't want to auto-commit when this method returns.
+        final PaymentSqlDao paymentSqlDao = transactionalSqlDao.onDemand(PaymentSqlDao.class);
+        final Long totalCount = paymentSqlDao.getCount(context);
+        final Iterator<PaymentModelDao> results = paymentSqlDao.getByPluginName(pluginName, offset, limit, context);
+
+        return new DefaultPagination<PaymentModelDao>(offset, limit, count, totalCount, results);
+    }
+
+    @Override
     public PaymentModelDao getPayment(final UUID paymentId, final InternalTenantContext context) {
         return transactionalSqlDao.execute(new EntitySqlDaoTransactionWrapper<PaymentModelDao>() {
             @Override
diff --git a/payment/src/main/java/com/ning/billing/payment/dao/PaymentDao.java b/payment/src/main/java/com/ning/billing/payment/dao/PaymentDao.java
index 2c2e76e..613a8d0 100644
--- a/payment/src/main/java/com/ning/billing/payment/dao/PaymentDao.java
+++ b/payment/src/main/java/com/ning/billing/payment/dao/PaymentDao.java
@@ -45,6 +45,8 @@ public interface PaymentDao {
 
     public PaymentModelDao getLastPaymentForPaymentMethod(UUID accountId, UUID paymentMethodId, InternalTenantContext context);
 
+    public Pagination<PaymentModelDao> getPayments(String pluginName, Long offset, Long limit, InternalTenantContext context);
+
     public PaymentModelDao getPayment(UUID paymentId, InternalTenantContext context);
 
     public List<PaymentAttemptModelDao> getAttemptsForPayment(UUID paymentId, InternalTenantContext context);
diff --git a/payment/src/main/java/com/ning/billing/payment/dao/PaymentSqlDao.java b/payment/src/main/java/com/ning/billing/payment/dao/PaymentSqlDao.java
index 8a1749f..e234194 100644
--- a/payment/src/main/java/com/ning/billing/payment/dao/PaymentSqlDao.java
+++ b/payment/src/main/java/com/ning/billing/payment/dao/PaymentSqlDao.java
@@ -18,18 +18,20 @@ package com.ning.billing.payment.dao;
 
 import java.math.BigDecimal;
 import java.util.Date;
+import java.util.Iterator;
 import java.util.List;
 
 import org.skife.jdbi.v2.sqlobject.Bind;
 import org.skife.jdbi.v2.sqlobject.BindBean;
 import org.skife.jdbi.v2.sqlobject.SqlQuery;
 import org.skife.jdbi.v2.sqlobject.SqlUpdate;
+import org.skife.jdbi.v2.sqlobject.customizers.FetchSize;
 
+import com.ning.billing.callcontext.InternalCallContext;
+import com.ning.billing.callcontext.InternalTenantContext;
 import com.ning.billing.catalog.api.Currency;
 import com.ning.billing.payment.api.Payment;
 import com.ning.billing.util.audit.ChangeType;
-import com.ning.billing.callcontext.InternalCallContext;
-import com.ning.billing.callcontext.InternalTenantContext;
 import com.ning.billing.util.entity.dao.Audited;
 import com.ning.billing.util.entity.dao.EntitySqlDao;
 import com.ning.billing.util.entity.dao.EntitySqlDaoStringTemplate;
@@ -65,5 +67,14 @@ public interface PaymentSqlDao extends EntitySqlDao<PaymentModelDao, Payment> {
     @SqlQuery
     List<PaymentModelDao> getPaymentsForAccount(@Bind("accountId") final String accountId,
                                                 @BindBean final InternalTenantContext context);
+
+    @SqlQuery
+    // Magic value to force MySQL to stream from the database
+    // See http://dev.mysql.com/doc/refman/5.0/en/connector-j-reference-implementation-notes.html (ResultSet)
+    @FetchSize(Integer.MIN_VALUE)
+    public Iterator<PaymentModelDao> getByPluginName(@Bind("pluginName") final String pluginName,
+                                                     @Bind("offset") final Long offset,
+                                                     @Bind("rowCount") final Long rowCount,
+                                                     @BindBean final InternalTenantContext context);
 }
 
diff --git a/payment/src/main/resources/com/ning/billing/payment/dao/PaymentSqlDao.sql.stg b/payment/src/main/resources/com/ning/billing/payment/dao/PaymentSqlDao.sql.stg
index 3f9624c..cc119e5 100644
--- a/payment/src/main/resources/com/ning/billing/payment/dao/PaymentSqlDao.sql.stg
+++ b/payment/src/main/resources/com/ning/billing/payment/dao/PaymentSqlDao.sql.stg
@@ -104,4 +104,14 @@ where id = :id
 ;
 >>
 
-
+getByPluginName(pluginName, offset, rowCount) ::= <<
+select SQL_CALC_FOUND_ROWS
+<allTableFields("t.")>
+from <tableName()> t
+join payment_methods pm on pm.id = t.payment_method_id
+where pm.plugin_name = :pluginName
+and pm.is_active = 1
+order by record_id
+limit :offset, :rowCount
+;
+>>
diff --git a/payment/src/test/java/com/ning/billing/payment/dao/MockPaymentDao.java b/payment/src/test/java/com/ning/billing/payment/dao/MockPaymentDao.java
index 2a245c6..3e14944 100644
--- a/payment/src/test/java/com/ning/billing/payment/dao/MockPaymentDao.java
+++ b/payment/src/test/java/com/ning/billing/payment/dao/MockPaymentDao.java
@@ -108,6 +108,11 @@ public class MockPaymentDao implements PaymentDao {
     }
 
     @Override
+    public Pagination<PaymentModelDao> getPayments(final String pluginName, final Long offset, final Long limit, final InternalTenantContext context) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
     public PaymentModelDao getPayment(final UUID paymentId, final InternalTenantContext context) {
         return payments.get(paymentId);
     }
@@ -186,13 +191,11 @@ public class MockPaymentDao implements PaymentDao {
         return null;
     }
 
-
     @Override
     public void updateRefundStatus(final UUID refundId, final RefundStatus status, final BigDecimal processedAmount, final Currency processedCurrency, final InternalCallContext context) {
         return;
     }
 
-
     @Override
     public RefundModelDao getRefund(final UUID refundId, final InternalTenantContext context) {
         return null;

pom.xml 2(+1 -1)

diff --git a/pom.xml b/pom.xml
index 1b696e9..78df4c9 100644
--- a/pom.xml
+++ b/pom.xml
@@ -19,7 +19,7 @@
     <parent>
         <artifactId>killbill-oss-parent</artifactId>
         <groupId>com.ning.billing</groupId>
-        <version>0.5.14</version>
+        <version>0.5.15</version>
     </parent>
     <artifactId>killbill</artifactId>
     <version>0.8.8-SNAPSHOT</version>