Details
diff --git a/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestIntegrationParentInvoice.java b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestIntegrationParentInvoice.java
index 8139872..ef0a637 100644
--- a/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestIntegrationParentInvoice.java
+++ b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestIntegrationParentInvoice.java
@@ -21,6 +21,7 @@ import java.math.BigDecimal;
import java.util.List;
import org.joda.time.DateTime;
+import org.joda.time.LocalDate;
import org.killbill.billing.account.api.Account;
import org.killbill.billing.api.TestApiListener.NextEvent;
import org.killbill.billing.catalog.api.BillingActionPolicy;
@@ -29,6 +30,7 @@ import org.killbill.billing.catalog.api.Currency;
import org.killbill.billing.catalog.api.ProductCategory;
import org.killbill.billing.entitlement.api.DefaultEntitlement;
import org.killbill.billing.invoice.api.Invoice;
+import org.killbill.billing.invoice.api.InvoiceApiException;
import org.killbill.billing.invoice.api.InvoiceItemType;
import org.killbill.billing.invoice.api.InvoiceStatus;
import org.killbill.billing.payment.api.Payment;
@@ -631,7 +633,7 @@ public class TestIntegrationParentInvoice extends TestIntegrationBase {
assertEquals(childInvoice.getInvoiceItems().get(1).getAmount().compareTo(BigDecimal.valueOf(241.62)), 0);
// check equal parent invoice
- parentInvoices = invoiceUserApi.getInvoicesByAccount(parentInvoice.getId(), false, callContext);
+ parentInvoices = invoiceUserApi.getInvoicesByAccount(parentAccount.getId(), false, callContext);
assertEquals(parentInvoices.size(), 2);
parentInvoice = parentInvoices.get(1);
@@ -718,7 +720,7 @@ public class TestIntegrationParentInvoice extends TestIntegrationBase {
assertEquals(childInvoice.getInvoiceItems().get(1).getAmount().compareTo(BigDecimal.valueOf(241.62)), 0);
// check equal parent invoice
- parentInvoices = invoiceUserApi.getInvoicesByAccount(parentInvoice.getId(), false, callContext);
+ parentInvoices = invoiceUserApi.getInvoicesByAccount(parentAccount.getId(), false, callContext);
assertEquals(parentInvoices.size(), 2);
parentInvoice = parentInvoices.get(1);
@@ -752,7 +754,7 @@ public class TestIntegrationParentInvoice extends TestIntegrationBase {
assertEquals(childInvoice.getInvoiceItems().get(1).getAmount().compareTo(BigDecimal.valueOf(-241.62)), 0);
// check equal parent invoice
- parentInvoices = invoiceUserApi.getInvoicesByAccount(parentInvoice.getId(), false, callContext);
+ parentInvoices = invoiceUserApi.getInvoicesByAccount(parentAccount.getId(), false, callContext);
assertEquals(parentInvoices.size(), 4);
parentInvoice = parentInvoices.get(3);
@@ -763,4 +765,102 @@ public class TestIntegrationParentInvoice extends TestIntegrationBase {
}
+ // Scenario 6: Transfer credit
+ @Test(groups = "slow")
+ public void testParentInvoiceTransferCredit() throws Exception {
+
+ final int billingDay = 14;
+ final DateTime initialCreationDate = new DateTime(2014, 5, 15, 0, 0, 0, 0, testTimeZone);
+ // set clock to the initial start date
+ clock.setTime(initialCreationDate);
+
+ final Account parentAccount = createAccountWithNonOsgiPaymentMethod(getAccountData(billingDay));
+ final Account childAccount = createAccountWithNonOsgiPaymentMethod(getChildAccountData(billingDay, parentAccount.getId(), true));
+
+ busHandler.pushExpectedEvents(NextEvent.INVOICE);
+ invoiceUserApi.insertCredit(childAccount.getId(), new BigDecimal("250"), new LocalDate(clock.getUTCNow(), childAccount.getTimeZone()), childAccount.getCurrency(), true, null, callContext);
+ assertListenerStatus();
+
+ BigDecimal childAccountCBA = invoiceUserApi.getAccountCBA(childAccount.getId(), callContext);
+ assertEquals(childAccountCBA.compareTo(BigDecimal.valueOf(250)), 0);
+
+ BigDecimal parentAccountCBA = invoiceUserApi.getAccountCBA(parentAccount.getId(), callContext);
+ assertEquals(parentAccountCBA.compareTo(BigDecimal.ZERO), 0);
+
+ busHandler.pushExpectedEvents(NextEvent.INVOICE);
+ invoiceUserApi.transferChildCreditToParent(childAccount.getId(), callContext);
+ assertListenerStatus();
+
+ childAccountCBA = invoiceUserApi.getAccountCBA(childAccount.getId(), callContext);
+ assertEquals(childAccountCBA.compareTo(BigDecimal.ZERO), 0);
+
+ parentAccountCBA = invoiceUserApi.getAccountCBA(parentAccount.getId(), callContext);
+ assertEquals(parentAccountCBA.compareTo(BigDecimal.valueOf(250)), 0);
+
+ final List<Invoice> childInvoices = invoiceUserApi.getInvoicesByAccount(childAccount.getId(), false, callContext);
+ assertEquals(childInvoices.size(), 2);
+
+ final Invoice childInvoice = childInvoices.get(1);
+ assertEquals(childInvoice.getNumberOfItems(), 2);
+ assertEquals(childInvoice.getInvoiceItems().get(0).getInvoiceItemType(), InvoiceItemType.EXTERNAL_CHARGE);
+ assertEquals(childInvoice.getInvoiceItems().get(0).getAmount().compareTo(BigDecimal.valueOf(250)), 0);
+ assertEquals(childInvoice.getInvoiceItems().get(1).getInvoiceItemType(), InvoiceItemType.CBA_ADJ);
+ assertEquals(childInvoice.getInvoiceItems().get(1).getAmount().compareTo(BigDecimal.valueOf(-250)), 0);
+
+ // check equal parent invoice
+ final List<Invoice> parentInvoices = invoiceUserApi.getInvoicesByAccount(parentAccount.getId(), false, callContext);
+ assertEquals(parentInvoices.size(), 1);
+
+ final Invoice parentInvoice = parentInvoices.get(0);
+ assertEquals(parentInvoice.getNumberOfItems(), 3);
+ assertEquals(parentInvoice.getInvoiceItems().get(0).getInvoiceItemType(), InvoiceItemType.CREDIT_ADJ);
+ assertEquals(parentInvoice.getInvoiceItems().get(0).getAmount().compareTo(BigDecimal.valueOf(-250)), 0);
+ assertEquals(parentInvoice.getInvoiceItems().get(1).getInvoiceItemType(), InvoiceItemType.CBA_ADJ);
+ assertEquals(parentInvoice.getInvoiceItems().get(1).getAmount().compareTo(BigDecimal.valueOf(250)), 0);
+ assertEquals(parentInvoice.getInvoiceItems().get(2).getInvoiceItemType(), InvoiceItemType.PARENT_SUMMARY);
+ assertEquals(parentInvoice.getInvoiceItems().get(2).getAmount().compareTo(BigDecimal.ZERO), 0);
+ }
+
+ // Scenario 6-b: Transfer credit
+ @Test(groups = "slow", expectedExceptions = InvoiceApiException.class,
+ expectedExceptionsMessageRegExp = ".* does not have credit")
+ public void testParentInvoiceTransferCreditAccountWithoutCredit() throws Exception {
+
+ final int billingDay = 14;
+ final DateTime initialCreationDate = new DateTime(2014, 5, 15, 0, 0, 0, 0, testTimeZone);
+ // set clock to the initial start date
+ clock.setTime(initialCreationDate);
+
+ final Account parentAccount = createAccountWithNonOsgiPaymentMethod(getAccountData(billingDay));
+ final Account childAccount = createAccountWithNonOsgiPaymentMethod(getChildAccountData(billingDay, parentAccount.getId(), true));
+
+ BigDecimal childAccountCBA = invoiceUserApi.getAccountCBA(childAccount.getId(), callContext);
+ assertEquals(childAccountCBA.compareTo(BigDecimal.ZERO), 0);
+
+ BigDecimal parentAccountCBA = invoiceUserApi.getAccountCBA(parentAccount.getId(), callContext);
+ assertEquals(parentAccountCBA.compareTo(BigDecimal.ZERO), 0);
+
+ invoiceUserApi.transferChildCreditToParent(childAccount.getId(), callContext);
+
+ }
+
+ // Scenario 6-c: Transfer credit
+ @Test(groups = "slow", expectedExceptions = InvoiceApiException.class,
+ expectedExceptionsMessageRegExp = ".* does not have a Parent Account associated")
+ public void testParentInvoiceTransferCreditAccountNoParent() throws Exception {
+
+ final int billingDay = 14;
+ final DateTime initialCreationDate = new DateTime(2014, 5, 15, 0, 0, 0, 0, testTimeZone);
+ // set clock to the initial start date
+ clock.setTime(initialCreationDate);
+
+ final Account account = createAccountWithNonOsgiPaymentMethod(getChildAccountData(billingDay, null, true));
+
+ BigDecimal childAccountCBA = invoiceUserApi.getAccountCBA(account.getId(), callContext);
+ assertEquals(childAccountCBA.compareTo(BigDecimal.ZERO), 0);
+
+ invoiceUserApi.transferChildCreditToParent(account.getId(), callContext);
+
+ }
+
}
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/api/user/DefaultInvoiceUserApi.java b/invoice/src/main/java/org/killbill/billing/invoice/api/user/DefaultInvoiceUserApi.java
index 51949f1..10d3f5f 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/api/user/DefaultInvoiceUserApi.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/api/user/DefaultInvoiceUserApi.java
@@ -533,4 +533,27 @@ public class DefaultInvoiceUserApi implements InvoiceUserApi {
dao.changeInvoiceStatus(invoiceId, InvoiceStatus.COMMITTED, internalCallContext);
}
+ @Override
+ public void transferChildCreditToParent(final UUID childAccountId, final CallContext callContext) throws InvoiceApiException {
+
+ final Account childAccount;
+ final InternalTenantContext internalContext = internalCallContextFactory.createInternalTenantContext(childAccountId, ObjectType.INVOICE, callContext);
+ try {
+ childAccount = accountUserApi.getAccountById(childAccountId, internalContext);
+ } catch (AccountApiException e) {
+ throw new InvoiceApiException(e);
+ }
+
+ if (childAccount.getParentAccountId() == null) {
+ throw new InvoiceApiException(ErrorCode.ACCOUNT_DOES_NOT_HAVE_PARENT_ACCOUNT, childAccountId);
+ }
+
+ final BigDecimal accountCBA = getAccountCBA(childAccountId, callContext);
+ if (accountCBA.compareTo(BigDecimal.ZERO) <= 0) {
+ throw new InvoiceApiException(ErrorCode.CHILD_ACCOUNT_MISSING_CREDIT, childAccountId);
+ }
+
+ dao.transferChildCreditToParent(childAccount, callContext);
+
+ }
}
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/dao/DefaultInvoiceDao.java b/invoice/src/main/java/org/killbill/billing/invoice/dao/DefaultInvoiceDao.java
index 80f77f8..11863c3 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/dao/DefaultInvoiceDao.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/dao/DefaultInvoiceDao.java
@@ -29,10 +29,10 @@ import java.util.UUID;
import javax.annotation.Nullable;
import org.joda.time.DateTime;
-import org.joda.time.DateTimeZone;
import org.joda.time.LocalDate;
import org.killbill.billing.ErrorCode;
import org.killbill.billing.ObjectType;
+import org.killbill.billing.account.api.Account;
import org.killbill.billing.callcontext.InternalCallContext;
import org.killbill.billing.callcontext.InternalTenantContext;
import org.killbill.billing.catalog.api.Currency;
@@ -43,17 +43,21 @@ import org.killbill.billing.invoice.api.DefaultInvoicePaymentErrorEvent;
import org.killbill.billing.invoice.api.DefaultInvoicePaymentInfoEvent;
import org.killbill.billing.invoice.api.Invoice;
import org.killbill.billing.invoice.api.InvoiceApiException;
+import org.killbill.billing.invoice.api.InvoiceItem;
import org.killbill.billing.invoice.api.InvoiceItemType;
import org.killbill.billing.invoice.api.InvoicePaymentType;
import org.killbill.billing.invoice.api.InvoiceStatus;
import org.killbill.billing.invoice.api.user.DefaultInvoiceAdjustmentEvent;
import org.killbill.billing.invoice.api.user.DefaultInvoiceCreationEvent;
+import org.killbill.billing.invoice.model.CreditAdjInvoiceItem;
import org.killbill.billing.invoice.model.DefaultInvoice;
+import org.killbill.billing.invoice.model.ExternalChargeInvoiceItem;
import org.killbill.billing.invoice.notification.NextBillingDatePoster;
import org.killbill.billing.invoice.notification.ParentInvoiceCommitmentPoster;
import org.killbill.billing.util.UUIDs;
import org.killbill.billing.util.cache.Cachable.CacheType;
import org.killbill.billing.util.cache.CacheControllerDispatcher;
+import org.killbill.billing.util.callcontext.CallContext;
import org.killbill.billing.util.callcontext.InternalCallContextFactory;
import org.killbill.billing.util.config.definition.InvoiceConfig;
import org.killbill.billing.util.dao.NonEntityDao;
@@ -1100,4 +1104,73 @@ public class DefaultInvoiceDao extends EntityDaoBase<InvoiceModelDao, Invoice, I
}
});
}
+
+ @Override
+ public void transferChildCreditToParent(final Account childAccount, final CallContext context) throws InvoiceApiException {
+ transactionalSqlDao.execute(new EntitySqlDaoTransactionWrapper<Void>() {
+ @Override
+ public Void inTransaction(final EntitySqlDaoWrapperFactory entitySqlDaoWrapperFactory) throws Exception {
+
+ final InternalCallContext childInternalCallContext = internalCallContextFactory.createInternalCallContext(childAccount.getId(), context);
+ final InternalCallContext parentInternalCallContext = internalCallContextFactory.createInternalCallContext(childAccount.getParentAccountId(), context);
+
+ final InvoiceSqlDao invoiceSqlDao = entitySqlDaoWrapperFactory.become(InvoiceSqlDao.class);
+ final InvoiceItemSqlDao transInvoiceItemSqlDao = entitySqlDaoWrapperFactory.become(InvoiceItemSqlDao.class);
+
+ // create child and parent invoices
+
+ final DateTime effectiveDate = context.getCreatedDate();
+ final BigDecimal accountCBA = getAccountCBA(childAccount.getId(), childInternalCallContext);
+
+ // create external charge to child account
+ final Invoice invoiceForExternalCharge = new DefaultInvoice(childAccount.getId(), effectiveDate.toLocalDate(),
+ effectiveDate.toLocalDate(),
+ childAccount.getCurrency(), InvoiceStatus.COMMITTED);
+ final String chargeDescription = "Charge to move credit from child to parent account";
+ final InvoiceItem externalChargeItem = new ExternalChargeInvoiceItem(UUIDs.randomUUID(),
+ effectiveDate,
+ invoiceForExternalCharge.getId(),
+ childAccount.getId(),
+ null,
+ chargeDescription,
+ effectiveDate.toLocalDate(),
+ accountCBA,
+ childAccount.getCurrency());
+ invoiceForExternalCharge.addInvoiceItem(externalChargeItem);
+
+ // create credit to parent account
+ final Invoice invoiceForCredit = new DefaultInvoice(childAccount.getParentAccountId(), effectiveDate.toLocalDate(), effectiveDate.toLocalDate(),
+ childAccount.getCurrency(), InvoiceStatus.DRAFT);
+ final String creditDescription = "Credit migrated from child account " + childAccount.getId();
+ final InvoiceItem creditItem = new CreditAdjInvoiceItem(UUIDs.randomUUID(),
+ effectiveDate,
+ invoiceForCredit.getId(),
+ childAccount.getParentAccountId(),
+ effectiveDate.toLocalDate(),
+ creditDescription,
+ // Note! The amount is negated here!
+ accountCBA.negate(),
+ childAccount.getCurrency());
+ invoiceForCredit.addInvoiceItem(creditItem);
+
+
+ // save invoices and invoice items
+ InvoiceModelDao childInvoice = new InvoiceModelDao(invoiceForExternalCharge);
+ invoiceSqlDao.create(childInvoice, childInternalCallContext);
+ createInvoiceItemFromTransaction(transInvoiceItemSqlDao, new InvoiceItemModelDao(externalChargeItem), childInternalCallContext);
+
+ InvoiceModelDao parentInvoice = new InvoiceModelDao(invoiceForCredit);
+ invoiceSqlDao.create(parentInvoice, parentInternalCallContext);
+ createInvoiceItemFromTransaction(transInvoiceItemSqlDao, new InvoiceItemModelDao(creditItem), parentInternalCallContext);
+
+ // add CBA complexity and notify bus on child invoice creation
+ cbaDao.addCBAComplexityFromTransaction(childInvoice.getId(), entitySqlDaoWrapperFactory, childInternalCallContext);
+ notifyBusOfInvoiceCreation(entitySqlDaoWrapperFactory, childInvoice, childInternalCallContext);
+
+ cbaDao.addCBAComplexityFromTransaction(parentInvoice.getId(), entitySqlDaoWrapperFactory, parentInternalCallContext);
+
+ return null;
+ }
+ });
+ }
}
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/dao/InvoiceDao.java b/invoice/src/main/java/org/killbill/billing/invoice/dao/InvoiceDao.java
index 5989a30..216e47d 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/dao/InvoiceDao.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/dao/InvoiceDao.java
@@ -26,6 +26,7 @@ import java.util.UUID;
import javax.annotation.Nullable;
import org.joda.time.LocalDate;
+import org.killbill.billing.account.api.Account;
import org.killbill.billing.callcontext.InternalCallContext;
import org.killbill.billing.callcontext.InternalTenantContext;
import org.killbill.billing.catalog.api.Currency;
@@ -33,6 +34,7 @@ import org.killbill.billing.invoice.InvoiceDispatcher.FutureAccountNotifications
import org.killbill.billing.invoice.api.Invoice;
import org.killbill.billing.invoice.api.InvoiceApiException;
import org.killbill.billing.invoice.api.InvoiceStatus;
+import org.killbill.billing.util.callcontext.CallContext;
import org.killbill.billing.util.entity.Pagination;
import org.killbill.billing.util.entity.dao.EntityDao;
@@ -193,4 +195,13 @@ public interface InvoiceDao extends EntityDao<InvoiceModelDao, Invoice, InvoiceA
* @throws InvoiceApiException if any unexpected error occurs
*/
void updateInvoiceItemAmount(UUID invoiceItemId, BigDecimal amount, InternalCallContext context) throws InvoiceApiException;
+
+ /**
+ * Move a given child credit to the parent level
+ *
+ * @param childAccount the child account
+ * @param context the tenant context
+ * @throws InvoiceApiException if any unexpected error occurs
+ */
+ void transferChildCreditToParent(Account childAccount, CallContext context) throws InvoiceApiException;
}
diff --git a/invoice/src/test/java/org/killbill/billing/invoice/dao/MockInvoiceDao.java b/invoice/src/test/java/org/killbill/billing/invoice/dao/MockInvoiceDao.java
index 46edf8c..7844d52 100644
--- a/invoice/src/test/java/org/killbill/billing/invoice/dao/MockInvoiceDao.java
+++ b/invoice/src/test/java/org/killbill/billing/invoice/dao/MockInvoiceDao.java
@@ -28,6 +28,7 @@ import java.util.Map;
import java.util.UUID;
import org.joda.time.LocalDate;
+import org.killbill.billing.account.api.Account;
import org.killbill.billing.callcontext.InternalCallContext;
import org.killbill.billing.callcontext.InternalTenantContext;
import org.killbill.billing.catalog.api.Currency;
@@ -36,6 +37,7 @@ import org.killbill.billing.invoice.api.Invoice;
import org.killbill.billing.invoice.api.InvoiceApiException;
import org.killbill.billing.invoice.api.InvoiceStatus;
import org.killbill.billing.invoice.api.user.DefaultInvoiceCreationEvent;
+import org.killbill.billing.util.callcontext.CallContext;
import org.killbill.billing.util.entity.DefaultPagination;
import org.killbill.billing.util.entity.Pagination;
import org.killbill.billing.util.entity.dao.MockEntityDaoBase;
@@ -400,4 +402,8 @@ public class MockInvoiceDao extends MockEntityDaoBase<InvoiceModelDao, Invoice,
throw new UnsupportedOperationException();
}
+ @Override
+ public void transferChildCreditToParent(final Account childAccount, final CallContext context) throws InvoiceApiException {
+ throw new UnsupportedOperationException();
+ }
}
diff --git a/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/AccountResource.java b/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/AccountResource.java
index 93c03db..d40a274 100644
--- a/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/AccountResource.java
+++ b/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/AccountResource.java
@@ -120,7 +120,6 @@ import org.killbill.commons.metrics.MetricTag;
import org.killbill.commons.metrics.TimedResource;
import com.google.common.base.Function;
-import com.google.common.base.Preconditions;
import com.google.common.base.Predicate;
import com.google.common.collect.Collections2;
import com.google.common.collect.ImmutableList;
@@ -1314,4 +1313,26 @@ public class AccountResource extends JaxRsResourceBase {
return Response.status(Status.OK).entity(accountJson).build();
}
+ @TimedResource
+ @POST
+ @Path("/{childAccountId:" + UUID_PATTERN + "}/" + TRANSFER_CREDIT)
+ @Consumes(APPLICATION_JSON)
+ @Produces(APPLICATION_JSON)
+ @ApiOperation(value = "Move a given child credit to the parent level")
+ @ApiResponses(value = {@ApiResponse(code = 400, message = "Account does not have credit"),
+ @ApiResponse(code = 404, message = "Account not found")})
+ public Response transferChildCreditToParent(@PathParam("childAccountId") final String childAccountIdString,
+ @HeaderParam(HDR_CREATED_BY) final String createdBy,
+ @HeaderParam(HDR_REASON) final String reason,
+ @HeaderParam(HDR_COMMENT) final String comment,
+ @javax.ws.rs.core.Context final HttpServletRequest request,
+ @javax.ws.rs.core.Context final UriInfo uriInfo) throws InvoiceApiException {
+
+ final CallContext callContext = context.createContext(createdBy, reason, comment, request);
+ final UUID childAccountId = UUID.fromString(childAccountIdString);
+
+ invoiceApi.transferChildCreditToParent(childAccountId, callContext);
+ return Response.status(Response.Status.OK).build();
+ }
+
}
diff --git a/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/JaxrsResource.java b/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/JaxrsResource.java
index 57f12d3..e08ea4d 100644
--- a/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/JaxrsResource.java
+++ b/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/JaxrsResource.java
@@ -256,6 +256,6 @@ public interface JaxrsResource {
public static final String MIGRATION = "migration";
public static final String CHILDREN = "children";
- public static final String CHILDREN_PATH = PREFIX + "/" + CHILDREN;
+ public static final String TRANSFER_CREDIT = "transferCredit";
}