killbill-aplcache

Merge pull request #597 from matias-aguero-hs/256-tag-overdue-condition #256

8/9/2016 1:00:36 PM

Details

diff --git a/overdue/src/main/java/org/killbill/billing/overdue/calculator/BillingStateCalculator.java b/overdue/src/main/java/org/killbill/billing/overdue/calculator/BillingStateCalculator.java
index 7d594fc..d1bec8d 100644
--- a/overdue/src/main/java/org/killbill/billing/overdue/calculator/BillingStateCalculator.java
+++ b/overdue/src/main/java/org/killbill/billing/overdue/calculator/BillingStateCalculator.java
@@ -19,6 +19,7 @@ package org.killbill.billing.overdue.calculator;
 import java.math.BigDecimal;
 import java.util.Collection;
 import java.util.Comparator;
+import java.util.List;
 import java.util.NoSuchElementException;
 import java.util.SortedSet;
 import java.util.TreeSet;
@@ -26,6 +27,7 @@ import java.util.UUID;
 
 import org.joda.time.DateTimeZone;
 import org.joda.time.LocalDate;
+import org.killbill.billing.ObjectType;
 import org.killbill.billing.account.api.ImmutableAccountData;
 import org.killbill.billing.callcontext.InternalTenantContext;
 import org.killbill.billing.invoice.api.Invoice;
@@ -33,6 +35,7 @@ import org.killbill.billing.invoice.api.InvoiceInternalApi;
 import org.killbill.billing.overdue.config.api.BillingState;
 import org.killbill.billing.overdue.config.api.OverdueException;
 import org.killbill.billing.payment.api.PaymentResponse;
+import org.killbill.billing.tag.TagInternalApi;
 import org.killbill.billing.util.tag.Tag;
 import org.killbill.clock.Clock;
 
@@ -41,6 +44,7 @@ import com.google.inject.Inject;
 public class BillingStateCalculator {
 
     private final InvoiceInternalApi invoiceApi;
+    private final TagInternalApi tagApi;
     private final Clock clock;
 
     protected class InvoiceDateComparator implements Comparator<Invoice> {
@@ -57,9 +61,10 @@ public class BillingStateCalculator {
     }
 
     @Inject
-    public BillingStateCalculator(final InvoiceInternalApi invoiceApi, final Clock clock) {
+    public BillingStateCalculator(final InvoiceInternalApi invoiceApi, final Clock clock, final TagInternalApi tagApi) {
         this.invoiceApi = invoiceApi;
         this.clock = clock;
+        this.tagApi = tagApi;
     }
 
     public BillingState calculateBillingState(final ImmutableAccountData account, final InternalTenantContext context) throws OverdueException {
@@ -75,7 +80,8 @@ public class BillingStateCalculator {
             idOfEarliestUnpaidInvoice = invoice.getId();
         }
         final PaymentResponse responseForLastFailedPayment = PaymentResponse.INSUFFICIENT_FUNDS; //TODO MDW
-        final Tag[] tags = new Tag[]{}; //TODO MDW
+        final List<Tag> accountTags = tagApi.getTags(account.getId(), ObjectType.ACCOUNT, context);
+        final Tag[] tags = accountTags.toArray(new Tag[accountTags.size()]);
 
         return new BillingState(account.getId(), numberOfUnpaidInvoices, unpaidInvoiceBalance, dateOfEarliestUnpaidInvoice, account.getTimeZone(), idOfEarliestUnpaidInvoice, responseForLastFailedPayment, tags);
     }
diff --git a/overdue/src/main/java/org/killbill/billing/overdue/config/DefaultOverdueCondition.java b/overdue/src/main/java/org/killbill/billing/overdue/config/DefaultOverdueCondition.java
index ee30861..3cca103 100644
--- a/overdue/src/main/java/org/killbill/billing/overdue/config/DefaultOverdueCondition.java
+++ b/overdue/src/main/java/org/killbill/billing/overdue/config/DefaultOverdueCondition.java
@@ -57,8 +57,11 @@ public class DefaultOverdueCondition extends ValidatingConfig<DefaultOverdueConf
     @XmlElement(required = false, name = "response")
     private PaymentResponse[] responseForLastFailedPayment;
 
-    @XmlElement(required = false, name = "controlTag")
-    private ControlTagType controlTag;
+    @XmlElement(required = false, name = "controlTagInclusion")
+    private ControlTagType controlTagInclusion;
+
+    @XmlElement(required = false, name = "controlTagExclusion")
+    private ControlTagType controlTagExclusion;
 
     @Override
     public boolean evaluate(final BillingState state, final LocalDate date) {
@@ -73,7 +76,8 @@ public class DefaultOverdueCondition extends ValidatingConfig<DefaultOverdueConf
                 (timeSinceEarliestUnpaidInvoiceEqualsOrExceeds == null ||
                  (unpaidInvoiceTriggerDate != null && !unpaidInvoiceTriggerDate.isAfter(date))) &&
                 (responseForLastFailedPayment == null || responseIsIn(state.getResponseForLastFailedPayment(), responseForLastFailedPayment)) &&
-                (controlTag == null || isTagIn(controlTag, state.getTags()));
+                (controlTagInclusion == null || isTagIn(controlTagInclusion, state.getTags())) &&
+                (controlTagExclusion == null || isTagNotIn(controlTagExclusion, state.getTags()));
     }
 
     private boolean responseIsIn(final PaymentResponse actualResponse,
@@ -95,6 +99,15 @@ public class DefaultOverdueCondition extends ValidatingConfig<DefaultOverdueConf
         return false;
     }
 
+    private boolean isTagNotIn(final ControlTagType tagType, final Tag[] tags) {
+        for (final Tag t : tags) {
+            if (t.getTagDefinitionId().equals(tagType.getId())) {
+                return false;
+            }
+        }
+        return true;
+    }
+
     @Override
     public ValidationErrors validate(final DefaultOverdueConfig root,
                                      final ValidationErrors errors) {
@@ -135,8 +148,13 @@ public class DefaultOverdueCondition extends ValidatingConfig<DefaultOverdueConf
     }
 
     @Override
-    public ControlTagType getControlTagType() {
-        return controlTag;
+    public ControlTagType getInclusionControlTagType() {
+        return controlTagInclusion;
+    }
+
+    @Override
+    public ControlTagType getExclusionControlTagType() {
+        return controlTagExclusion;
     }
 
     @Override
@@ -146,7 +164,8 @@ public class DefaultOverdueCondition extends ValidatingConfig<DefaultOverdueConf
         sb.append(", totalUnpaidInvoiceBalanceEqualsOrExceeds=").append(totalUnpaidInvoiceBalanceEqualsOrExceeds);
         sb.append(", timeSinceEarliestUnpaidInvoiceEqualsOrExceeds=").append(timeSinceEarliestUnpaidInvoiceEqualsOrExceeds);
         sb.append(", responseForLastFailedPayment=").append(Arrays.toString(responseForLastFailedPayment));
-        sb.append(", controlTag=").append(controlTag);
+        sb.append(", controlTagInclusion=").append(controlTagInclusion);
+        sb.append(", controlTagExclusion=").append(controlTagExclusion);
         sb.append('}');
         return sb.toString();
     }
diff --git a/overdue/src/test/java/org/killbill/billing/overdue/calculator/TestBillingStateCalculator.java b/overdue/src/test/java/org/killbill/billing/overdue/calculator/TestBillingStateCalculator.java
index d8f7b9d..a5db764 100644
--- a/overdue/src/test/java/org/killbill/billing/overdue/calculator/TestBillingStateCalculator.java
+++ b/overdue/src/test/java/org/killbill/billing/overdue/calculator/TestBillingStateCalculator.java
@@ -60,7 +60,7 @@ public class TestBillingStateCalculator extends OverdueTestSuiteNoDB {
 
         Mockito.when(invoiceApi.getUnpaidInvoicesByAccountId(Mockito.<UUID>any(), Mockito.<LocalDate>any(), Mockito.<InternalTenantContext>any())).thenReturn(invoices);
 
-        return new BillingStateCalculator(invoiceApi, clock) {
+        return new BillingStateCalculator(invoiceApi, clock, tagInternalApi) {
             @Override
             public BillingState calculateBillingState(final ImmutableAccountData overdueable,
                                                       final InternalTenantContext context) {
diff --git a/overdue/src/test/java/org/killbill/billing/overdue/config/TestCondition.java b/overdue/src/test/java/org/killbill/billing/overdue/config/TestCondition.java
index 1e6ee8c..99efef6 100644
--- a/overdue/src/test/java/org/killbill/billing/overdue/config/TestCondition.java
+++ b/overdue/src/test/java/org/killbill/billing/overdue/config/TestCondition.java
@@ -139,7 +139,7 @@ public class TestCondition extends OverdueTestSuiteNoDB {
     public void testHasControlTag() throws Exception {
         final String xml =
                 "<condition>" +
-                "	<controlTag>OVERDUE_ENFORCEMENT_OFF</controlTag>" +
+                "	<controlTagInclusion>OVERDUE_ENFORCEMENT_OFF</controlTagInclusion>" +
                 "</condition>";
         final InputStream is = new ByteArrayInputStream(xml.getBytes());
         final MockCondition c = XMLLoader.getObjectFromStreamNoValidation(is, MockCondition.class);
diff --git a/overdue/src/test/java/org/killbill/billing/overdue/glue/TestOverdueModule.java b/overdue/src/test/java/org/killbill/billing/overdue/glue/TestOverdueModule.java
index 39ead21..8d603af 100644
--- a/overdue/src/test/java/org/killbill/billing/overdue/glue/TestOverdueModule.java
+++ b/overdue/src/test/java/org/killbill/billing/overdue/glue/TestOverdueModule.java
@@ -21,7 +21,6 @@ package org.killbill.billing.overdue.glue;
 import java.util.List;
 import java.util.UUID;
 
-import org.joda.time.DateTime;
 import org.killbill.billing.callcontext.InternalCallContext;
 import org.killbill.billing.callcontext.InternalTenantContext;
 import org.killbill.billing.entitlement.api.BlockingState;
@@ -49,8 +48,6 @@ import org.killbill.billing.util.glue.CallContextModule;
 import org.killbill.billing.util.glue.ConfigModule;
 import org.killbill.billing.util.glue.CustomFieldModule;
 import org.killbill.billing.util.glue.MemoryGlobalLockerModule;
-import org.killbill.clock.Clock;
-import org.killbill.clock.ClockMock;
 
 import com.google.inject.name.Names;
 
@@ -73,7 +70,7 @@ public class TestOverdueModule extends DefaultOverdueModule {
         install(new MockAccountModule(configSource));
         install(new MockEntitlementModule(configSource, new ApplicatorBlockingApi()));
         install(new MockInvoiceModule(configSource));
-        install(new MockTagModule(configSource));
+        install(new MockTagModule(configSource, true));
         install(new TemplateModule(configSource));
         install(new MockTenantModule(configSource));
         install(new MemoryGlobalLockerModule(configSource));
diff --git a/overdue/src/test/java/org/killbill/billing/overdue/OverdueTestSuiteNoDB.java b/overdue/src/test/java/org/killbill/billing/overdue/OverdueTestSuiteNoDB.java
index 0e1f5e5..d7d6462 100644
--- a/overdue/src/test/java/org/killbill/billing/overdue/OverdueTestSuiteNoDB.java
+++ b/overdue/src/test/java/org/killbill/billing/overdue/OverdueTestSuiteNoDB.java
@@ -37,6 +37,7 @@ import org.killbill.billing.overdue.notification.OverdueNotifier;
 import org.killbill.billing.overdue.notification.OverduePoster;
 import org.killbill.billing.overdue.service.DefaultOverdueService;
 import org.killbill.billing.overdue.wrapper.OverdueWrapperFactory;
+import org.killbill.billing.tag.TagInternalApi;
 import org.killbill.billing.tenant.api.TenantInternalApi;
 import org.killbill.billing.util.cache.CacheControllerDispatcher;
 import org.killbill.billing.util.callcontext.InternalCallContextFactory;
@@ -102,6 +103,8 @@ public abstract class OverdueTestSuiteNoDB extends GuicyKillbillTestSuiteNoDB {
     protected OverdueCacheInvalidationCallback cacheInvalidationCallback;
     @Inject
     protected TenantInternalApi tenantInternalApi;
+    @Inject
+    protected TagInternalApi tagInternalApi;
 
     @BeforeClass(groups = "fast")
     protected void beforeClass() throws Exception {
diff --git a/overdue/src/test/java/org/killbill/billing/overdue/TestOverdueHelper.java b/overdue/src/test/java/org/killbill/billing/overdue/TestOverdueHelper.java
index 3f09ee6..f805d5d 100644
--- a/overdue/src/test/java/org/killbill/billing/overdue/TestOverdueHelper.java
+++ b/overdue/src/test/java/org/killbill/billing/overdue/TestOverdueHelper.java
@@ -25,22 +25,24 @@ import java.util.UUID;
 
 import org.joda.time.DateTimeZone;
 import org.joda.time.LocalDate;
+import org.killbill.billing.ObjectType;
+import org.killbill.billing.account.api.AccountApiException;
+import org.killbill.billing.account.api.AccountInternalApi;
 import org.killbill.billing.account.api.ImmutableAccountData;
+import org.killbill.billing.callcontext.InternalTenantContext;
+import org.killbill.billing.entitlement.api.BlockingState;
+import org.killbill.billing.invoice.api.Invoice;
+import org.killbill.billing.invoice.api.InvoiceInternalApi;
+import org.killbill.billing.invoice.api.InvoiceItem;
+import org.killbill.billing.junction.BlockingInternalApi;
 import org.killbill.billing.overdue.api.OverdueState;
 import org.killbill.billing.overdue.glue.TestOverdueModule.ApplicatorBlockingApi;
+import org.killbill.billing.subscription.api.user.SubscriptionBaseApiException;
+import org.killbill.billing.tag.TagInternalApi;
+import org.killbill.billing.util.tag.Tag;
 import org.mockito.Mockito;
 import org.testng.Assert;
 
-import org.killbill.billing.account.api.AccountApiException;
-import org.killbill.billing.invoice.api.Invoice;
-import org.killbill.billing.invoice.api.InvoiceItem;
-import org.killbill.billing.entitlement.api.BlockingState;
-import org.killbill.billing.subscription.api.user.SubscriptionBaseApiException;
-import org.killbill.billing.callcontext.InternalTenantContext;
-import org.killbill.billing.account.api.AccountInternalApi;
-import org.killbill.billing.invoice.api.InvoiceInternalApi;
-import org.killbill.billing.junction.BlockingInternalApi;
-
 import com.google.inject.Inject;
 
 public class TestOverdueHelper {
@@ -79,6 +81,7 @@ public class TestOverdueHelper {
             "               <timeSinceEarliestUnpaidInvoiceEqualsOrExceeds>" +
             "                   <unit>DAYS</unit><number>30</number>" +
             "               </timeSinceEarliestUnpaidInvoiceEqualsOrExceeds>" +
+            "               <controlTagInclusion>TEST</controlTagInclusion>" +
             "           </condition>" +
             "           <externalMessage>Reached OD1</externalMessage>" +
             "           <blockChanges>true</blockChanges>" +
@@ -93,14 +96,17 @@ public class TestOverdueHelper {
     private final AccountInternalApi accountInternalApi;
     private final InvoiceInternalApi invoiceInternalApi;
     private final BlockingInternalApi blockingInternalApi;
+    private final TagInternalApi tagInternalApi;
 
     @Inject
     public TestOverdueHelper(final AccountInternalApi accountInternalApi,
                              final InvoiceInternalApi invoiceInternalApi,
-                             final BlockingInternalApi blockingInternalApi) {
+                             final BlockingInternalApi blockingInternalApi,
+                             final TagInternalApi tagInternalApi) {
         this.accountInternalApi = accountInternalApi;
         this.invoiceInternalApi = invoiceInternalApi;
         this.blockingInternalApi = blockingInternalApi;
+        this.tagInternalApi = tagInternalApi;
     }
 
     public void checkStateApplied(final OverdueState state) {
@@ -138,6 +144,15 @@ public class TestOverdueHelper {
         invoices.add(invoice);
         Mockito.when(invoiceInternalApi.getUnpaidInvoicesByAccountId(Mockito.<UUID>any(), Mockito.<LocalDate>any(), Mockito.<InternalTenantContext>any())).thenReturn(invoices);
 
+        final Tag tag = Mockito.mock(Tag.class);
+        Mockito.when(tag.getObjectId()).thenReturn(accountId);
+        Mockito.when(tag.getObjectType()).thenReturn(ObjectType.ACCOUNT);
+        Mockito.when(tag.getTagDefinitionId()).thenReturn(new UUID(0, 6));
+        final List<Tag> tags = new ArrayList<Tag>();
+        tags.add(tag);
+        Mockito.when(tagInternalApi.getTags(Mockito.eq(account.getId()), Mockito.eq(ObjectType.ACCOUNT), Mockito.<InternalTenantContext>any()))
+               .thenReturn(tags);
+
         return account;
     }
 
diff --git a/overdue/src/test/resources/OverdueConfigSchema.xsd b/overdue/src/test/resources/OverdueConfigSchema.xsd
index d8f014b..7b95f24 100644
--- a/overdue/src/test/resources/OverdueConfigSchema.xsd
+++ b/overdue/src/test/resources/OverdueConfigSchema.xsd
@@ -71,7 +71,8 @@
 </xs:sequence>
 </xs:complexType>
 </xs:element>
-<xs:element minOccurs="0" name="controlTag" type="controlTagType"/>
+<xs:element minOccurs="0" name="controlTagInclusion" type="controlTagType"/>
+<xs:element minOccurs="0" name="controlTagExclusion" type="controlTagType"/>
 </xs:sequence>
 </xs:extension>
 </xs:complexContent>
diff --git a/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/KillbillClient.java b/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/KillbillClient.java
index d504896..1380883 100644
--- a/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/KillbillClient.java
+++ b/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/KillbillClient.java
@@ -147,6 +147,19 @@ public abstract class KillbillClient extends GuicyKillbillTestSuiteWithEmbeddedD
         return accountJson;
     }
 
+    protected Account createAccountNoPMBundleAndSubscription() throws Exception {
+        // Create an account with no payment method
+        final Account accountJson = createAccount();
+        assertNotNull(accountJson);
+
+        // Add a bundle, subscription and move the clock to get the first invoice
+        final Subscription subscriptionJson = createEntitlement(accountJson.getAccountId(), UUID.randomUUID().toString(), "Shotgun",
+                                                                ProductCategory.BASE, BillingPeriod.MONTHLY, true);
+        assertNotNull(subscriptionJson);
+
+        return accountJson;
+    }
+
     protected Account createAccountNoPMBundleAndSubscriptionAndWaitForFirstInvoice() throws Exception {
         // Create an account with no payment method
         final Account accountJson = createAccount();
diff --git a/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestOverdue.java b/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestOverdue.java
index 6685955..f2f8f2c 100644
--- a/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestOverdue.java
+++ b/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestOverdue.java
@@ -27,6 +27,8 @@ import org.killbill.billing.client.model.Invoice;
 import org.killbill.billing.client.model.InvoicePayment;
 import org.killbill.billing.client.model.Invoices;
 import org.killbill.billing.client.model.Payment;
+import org.killbill.billing.client.model.Tags;
+import org.killbill.billing.util.tag.ControlTagType;
 import org.testng.Assert;
 import org.testng.annotations.Test;
 
@@ -46,7 +48,6 @@ public class TestOverdue extends TestJaxrsBase {
         Assert.assertNotNull(overdueConfig);
     }
 
-
     @Test(groups = "slow", description = "Can retrieve the account overdue status")
     public void testOverdueStatus() throws Exception {
         // Create an account without a payment method
@@ -99,4 +100,83 @@ public class TestOverdue extends TestJaxrsBase {
         // Verify we're in clear state
         Assert.assertTrue(killBillClient.getOverdueStateForAccount(accountJson.getAccountId()).getIsClearState());
     }
+
+    @Test(groups = "slow", description = "Allow overdue condition by control tag defined in overdue config xml file")
+    public void testControlTagOverdueConfig() throws Exception {
+        final String overdueConfigPath = Resources.getResource("overdueWithControlTag.xml").getPath();
+        killBillClient.uploadXMLOverdueConfig(overdueConfigPath, requestOptions);
+
+        // Create an account without a payment method and assign a TEST tag
+        final Account accountJson = createAccountNoPMBundleAndSubscription();
+        final Tags accountTag = killBillClient.createAccountTag(accountJson.getAccountId(), ControlTagType.TEST.getId(), requestOptions);
+        assertEquals(accountTag.get(0).getTagDefinitionId(), ControlTagType.TEST.getId());
+
+        // Create an account without a TEST tag
+        final Account accountJsonNoTag = createAccountNoPMBundleAndSubscription();
+
+        // No payment will be triggered as the account doesn't have a payment method
+        clock.addMonths(1);
+        crappyWaitForLackOfProperSynchonization();
+
+        // Get the invoices
+        final List<Invoice> invoices = killBillClient.getInvoicesForAccount(accountJson.getAccountId(), requestOptions);
+        // 2 invoices but look for the non zero dollar one
+        assertEquals(invoices.size(), 2);
+
+        final List<Invoice> invoicesNoTag = killBillClient.getInvoicesForAccount(accountJsonNoTag.getAccountId(), requestOptions);
+        // 2 invoices but look for the non zero dollar one
+        assertEquals(invoicesNoTag.size(), 2);
+
+        // We're still clear - see the configuration
+        Assert.assertTrue(killBillClient.getOverdueStateForAccount(accountJson.getAccountId(), requestOptions).getIsClearState());
+        Assert.assertTrue(killBillClient.getOverdueStateForAccount(accountJsonNoTag.getAccountId(), requestOptions).getIsClearState());
+
+        clock.addDays(30);
+        crappyWaitForLackOfProperSynchonization();
+
+        // This account is expected to move to OD1 state because it matches with controlTag defined
+        Assert.assertEquals(killBillClient.getOverdueStateForAccount(accountJson.getAccountId(), requestOptions).getName(), "OD1");
+        // This account is not expected to move to OD1 state because it does not match with controlTag defined
+        Assert.assertTrue(killBillClient.getOverdueStateForAccount(accountJsonNoTag.getAccountId(), requestOptions).getIsClearState());
+    }
+
+    @Test(groups = "slow", description = "Allow overdue condition by exclusion control tag defined in overdue config xml file")
+    public void testExclusionControlTagOverdueConfig() throws Exception {
+        final String overdueConfigPath = Resources.getResource("overdueWithExclusionControlTag.xml").getPath();
+        killBillClient.uploadXMLOverdueConfig(overdueConfigPath, requestOptions);
+
+        // Create an account without a payment method and assign a TEST tag
+        final Account accountJson = createAccountNoPMBundleAndSubscription();
+        final Tags accountTag = killBillClient.createAccountTag(accountJson.getAccountId(), ControlTagType.TEST.getId(), requestOptions);
+        assertEquals(accountTag.get(0).getTagDefinitionId(), ControlTagType.TEST.getId());
+
+        // Create an account without a TEST tag
+        final Account accountJsonNoTag = createAccountNoPMBundleAndSubscription();
+
+        // move a month a wait for invoicing
+        // No payment will be triggered as the account doesn't have a payment method
+        clock.addMonths(1);
+        crappyWaitForLackOfProperSynchonization();
+
+        // Get the invoices
+        final List<Invoice> invoices = killBillClient.getInvoicesForAccount(accountJson.getAccountId(), requestOptions);
+        // 2 invoices but look for the non zero dollar one
+        assertEquals(invoices.size(), 2);
+
+        final List<Invoice> invoicesNoTag = killBillClient.getInvoicesForAccount(accountJsonNoTag.getAccountId(), requestOptions);
+        // 2 invoices but look for the non zero dollar one
+        assertEquals(invoicesNoTag.size(), 2);
+
+        // We're still clear - see the configuration
+        Assert.assertTrue(killBillClient.getOverdueStateForAccount(accountJson.getAccountId(), requestOptions).getIsClearState());
+        Assert.assertTrue(killBillClient.getOverdueStateForAccount(accountJsonNoTag.getAccountId(), requestOptions).getIsClearState());
+
+        clock.addDays(30);
+        crappyWaitForLackOfProperSynchonization();
+
+        // This account is not expected to move to OD1 state because it does not match with exclusion controlTag defined
+        Assert.assertTrue(killBillClient.getOverdueStateForAccount(accountJson.getAccountId(), requestOptions).getIsClearState());
+        // This account is expected to move to OD1 state because it matches with exclusion controlTag defined
+        Assert.assertEquals(killBillClient.getOverdueStateForAccount(accountJsonNoTag.getAccountId(), requestOptions).getName(), "OD1");
+    }
 }
diff --git a/profiles/killbill/src/test/resources/overdueWithControlTag.xml b/profiles/killbill/src/test/resources/overdueWithControlTag.xml
new file mode 100644
index 0000000..6d20b0e
--- /dev/null
+++ b/profiles/killbill/src/test/resources/overdueWithControlTag.xml
@@ -0,0 +1,63 @@
+<!--
+  ~ Copyright 2014-2016 Groupon, Inc
+  ~ Copyright 2014-2016 The Billing 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
+  ~ 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.
+  -->
+
+<overdueConfig>
+   <accountOverdueStates>
+       <state name="OD3">
+           <condition>
+               <timeSinceEarliestUnpaidInvoiceEqualsOrExceeds>
+                   <unit>DAYS</unit><number>50</number>
+               </timeSinceEarliestUnpaidInvoiceEqualsOrExceeds>
+               <controlTagInclusion>TEST</controlTagInclusion>
+           </condition>
+           <externalMessage>Reached OD3</externalMessage>
+           <blockChanges>true</blockChanges>
+           <disableEntitlementAndChangesBlocked>true</disableEntitlementAndChangesBlocked>
+           <autoReevaluationInterval>
+               <unit>DAYS</unit><number>5</number>
+           </autoReevaluationInterval>
+       </state>
+       <state name="OD2">
+           <condition>
+               <timeSinceEarliestUnpaidInvoiceEqualsOrExceeds>
+                   <unit>DAYS</unit><number>40</number>
+               </timeSinceEarliestUnpaidInvoiceEqualsOrExceeds>
+               <controlTagInclusion>TEST</controlTagInclusion>
+           </condition>
+           <externalMessage>Reached OD2</externalMessage>
+           <blockChanges>true</blockChanges>
+           <disableEntitlementAndChangesBlocked>true</disableEntitlementAndChangesBlocked>
+           <autoReevaluationInterval>
+               <unit>DAYS</unit><number>5</number>
+           </autoReevaluationInterval>
+       </state>
+       <state name="OD1">
+           <condition>
+               <timeSinceEarliestUnpaidInvoiceEqualsOrExceeds>
+                   <unit>DAYS</unit><number>30</number>
+               </timeSinceEarliestUnpaidInvoiceEqualsOrExceeds>
+               <controlTagInclusion>TEST</controlTagInclusion>
+           </condition>
+           <externalMessage>Reached OD1</externalMessage>
+           <blockChanges>true</blockChanges>
+           <disableEntitlementAndChangesBlocked>false</disableEntitlementAndChangesBlocked>
+           <autoReevaluationInterval>
+               <unit>DAYS</unit><number>5</number>
+           </autoReevaluationInterval>
+       </state>
+   </accountOverdueStates>
+</overdueConfig>
diff --git a/profiles/killbill/src/test/resources/overdueWithExclusionControlTag.xml b/profiles/killbill/src/test/resources/overdueWithExclusionControlTag.xml
new file mode 100644
index 0000000..1da2113
--- /dev/null
+++ b/profiles/killbill/src/test/resources/overdueWithExclusionControlTag.xml
@@ -0,0 +1,63 @@
+<!--
+  ~ Copyright 2014-2016 Groupon, Inc
+  ~ Copyright 2014-2016 The Billing 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
+  ~ 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.
+  -->
+
+<overdueConfig>
+   <accountOverdueStates>
+       <state name="OD3">
+           <condition>
+               <timeSinceEarliestUnpaidInvoiceEqualsOrExceeds>
+                   <unit>DAYS</unit><number>50</number>
+               </timeSinceEarliestUnpaidInvoiceEqualsOrExceeds>
+               <controlTagExclusion>TEST</controlTagExclusion>
+           </condition>
+           <externalMessage>Reached OD3</externalMessage>
+           <blockChanges>true</blockChanges>
+           <disableEntitlementAndChangesBlocked>true</disableEntitlementAndChangesBlocked>
+           <autoReevaluationInterval>
+               <unit>DAYS</unit><number>5</number>
+           </autoReevaluationInterval>
+       </state>
+       <state name="OD2">
+           <condition>
+               <timeSinceEarliestUnpaidInvoiceEqualsOrExceeds>
+                   <unit>DAYS</unit><number>40</number>
+               </timeSinceEarliestUnpaidInvoiceEqualsOrExceeds>
+               <controlTagExclusion>TEST</controlTagExclusion>
+           </condition>
+           <externalMessage>Reached OD2</externalMessage>
+           <blockChanges>true</blockChanges>
+           <disableEntitlementAndChangesBlocked>true</disableEntitlementAndChangesBlocked>
+           <autoReevaluationInterval>
+               <unit>DAYS</unit><number>5</number>
+           </autoReevaluationInterval>
+       </state>
+       <state name="OD1">
+           <condition>
+               <timeSinceEarliestUnpaidInvoiceEqualsOrExceeds>
+                   <unit>DAYS</unit><number>30</number>
+               </timeSinceEarliestUnpaidInvoiceEqualsOrExceeds>
+               <controlTagExclusion>TEST</controlTagExclusion>
+           </condition>
+           <externalMessage>Reached OD1</externalMessage>
+           <blockChanges>true</blockChanges>
+           <disableEntitlementAndChangesBlocked>false</disableEntitlementAndChangesBlocked>
+           <autoReevaluationInterval>
+               <unit>DAYS</unit><number>5</number>
+           </autoReevaluationInterval>
+       </state>
+   </accountOverdueStates>
+</overdueConfig>
diff --git a/util/src/test/java/org/killbill/billing/mock/glue/MockTagModule.java b/util/src/test/java/org/killbill/billing/mock/glue/MockTagModule.java
index 6f77943..7c3e1a5 100644
--- a/util/src/test/java/org/killbill/billing/mock/glue/MockTagModule.java
+++ b/util/src/test/java/org/killbill/billing/mock/glue/MockTagModule.java
@@ -19,16 +19,27 @@
 package org.killbill.billing.mock.glue;
 
 import org.killbill.billing.platform.api.KillbillConfigSource;
+import org.killbill.billing.tag.TagInternalApi;
 import org.killbill.billing.util.glue.TagStoreModule;
+import org.killbill.billing.util.tag.DefaultTagInternalApi;
 import org.killbill.billing.util.tag.dao.MockTagDao;
 import org.killbill.billing.util.tag.dao.MockTagDefinitionDao;
 import org.killbill.billing.util.tag.dao.TagDao;
 import org.killbill.billing.util.tag.dao.TagDefinitionDao;
+import org.mockito.Mockito;
 
 public class MockTagModule extends TagStoreModule {
 
+    private final boolean mockInternalApi;
+
     public MockTagModule(final KillbillConfigSource configSource) {
         super(configSource);
+        this.mockInternalApi = false;
+    }
+
+    public MockTagModule(final KillbillConfigSource configSource, final boolean mockInternalApi) {
+        super(configSource);
+        this.mockInternalApi = mockInternalApi;
     }
 
     @Override
@@ -36,4 +47,13 @@ public class MockTagModule extends TagStoreModule {
         bind(TagDefinitionDao.class).to(MockTagDefinitionDao.class).asEagerSingleton();
         bind(TagDao.class).to(MockTagDao.class).asEagerSingleton();
     }
+
+    @Override
+    public void installInternalApi() {
+        if (mockInternalApi) {
+            bind(TagInternalApi.class).toInstance(Mockito.mock(TagInternalApi.class));
+        } else {
+            bind(TagInternalApi.class).to(DefaultTagInternalApi.class).asEagerSingleton();
+        }
+    }
 }