killbill-memoizeit

invoice: add admin endpoint to unpark all accounts As part

12/23/2016 1:26:55 AM

Details

diff --git a/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestInvoiceSystemDisabling.java b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestInvoiceSystemDisabling.java
index 5b5f1ca..cc57eae 100644
--- a/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestInvoiceSystemDisabling.java
+++ b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestInvoiceSystemDisabling.java
@@ -32,7 +32,6 @@ import org.killbill.billing.invoice.api.DryRunType;
 import org.killbill.billing.invoice.api.Invoice;
 import org.killbill.billing.invoice.api.InvoiceItemType;
 import org.testng.Assert;
-import org.testng.annotations.BeforeMethod;
 import org.testng.annotations.Test;
 
 import com.google.common.collect.ImmutableList;
@@ -41,16 +40,6 @@ import static org.testng.Assert.assertEquals;
 
 public class TestInvoiceSystemDisabling extends TestIntegrationBase {
 
-    @Override
-    @BeforeMethod(groups = "slow")
-    public void beforeMethod() throws Exception {
-        super.beforeMethod();
-        // Tests will delete the database entries, yet ParkedAccountsManager is injected once
-        busHandler.pushExpectedEvent(NextEvent.TAG_DEFINITION);
-        parkedAccountsManager.retrieveOrCreateParkTagDefinition(clock);
-        assertListenerStatus();
-    }
-
     @Test(groups = "slow")
     public void testInvoiceSystemDisablingBasic() throws Exception {
         // We take april as it has 30 days (easier to play with BCD)
@@ -61,7 +50,7 @@ public class TestInvoiceSystemDisabling extends TestIntegrationBase {
         final Account account = createAccountWithNonOsgiPaymentMethod(accountData);
         accountChecker.checkAccount(account.getId(), accountData, callContext);
 
-        Assert.assertFalse(parkedAccountsManager.isParked(account.getId(), internalCallContext));
+        Assert.assertFalse(parkedAccountsManager.isParked(internalCallContext));
 
         // Stop invoicing system
         invoiceConfig.setInvoicingSystemEnabled(false);
@@ -75,14 +64,14 @@ public class TestInvoiceSystemDisabling extends TestIntegrationBase {
                                                                                              NextEvent.BLOCK,
                                                                                              NextEvent.TAG);
 
-        Assert.assertTrue(parkedAccountsManager.isParked(account.getId(), internalCallContext));
+        Assert.assertTrue(parkedAccountsManager.isParked(internalCallContext));
         Collection<Invoice> invoices = invoiceUserApi.getInvoicesByAccount(account.getId(), false, callContext);
         assertEquals(invoices.size(), 0);
 
         // Move to end of trial =>  2012, 5, 1
         addDaysAndCheckForCompletion(30, NextEvent.PHASE);
 
-        Assert.assertTrue(parkedAccountsManager.isParked(account.getId(), internalCallContext));
+        Assert.assertTrue(parkedAccountsManager.isParked(internalCallContext));
         invoices = invoiceUserApi.getInvoicesByAccount(account.getId(), false, callContext);
         assertEquals(invoices.size(), 0);
 
@@ -94,7 +83,7 @@ public class TestInvoiceSystemDisabling extends TestIntegrationBase {
         invoiceChecker.checkInvoiceNoAudits(invoice, callContext, expected);
 
         // Still parked
-        Assert.assertTrue(parkedAccountsManager.isParked(account.getId(), internalCallContext));
+        Assert.assertTrue(parkedAccountsManager.isParked(internalCallContext));
         invoices = invoiceUserApi.getInvoicesByAccount(account.getId(), false, callContext);
         assertEquals(invoices.size(), 0);
 
@@ -104,7 +93,7 @@ public class TestInvoiceSystemDisabling extends TestIntegrationBase {
         assertListenerStatus();
 
         // Now unparked
-        Assert.assertFalse(parkedAccountsManager.isParked(account.getId(), internalCallContext));
+        Assert.assertFalse(parkedAccountsManager.isParked(internalCallContext));
         invoiceChecker.checkInvoice(invoice, callContext, expected);
         invoices = invoiceUserApi.getInvoicesByAccount(account.getId(), false, callContext);
         assertEquals(invoices.size(), 1);
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/InvoiceDispatcher.java b/invoice/src/main/java/org/killbill/billing/invoice/InvoiceDispatcher.java
index 45fb51c..51bff88 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/InvoiceDispatcher.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/InvoiceDispatcher.java
@@ -240,7 +240,7 @@ public class InvoiceDispatcher {
                                   final InternalCallContext context) throws InvoiceApiException {
         boolean parkedAccount = false;
         try {
-            parkedAccount = parkedAccountsManager.isParked(accountId, context);
+            parkedAccount = parkedAccountsManager.isParked(context);
             if (parkedAccount && !isApiCall) {
                 log.warn("Ignoring invoice generation process for accountId='{}', targetDate='{}', account is parked", accountId.toString(), targetDate);
                 return null;
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/ParkedAccountsManager.java b/invoice/src/main/java/org/killbill/billing/invoice/ParkedAccountsManager.java
index 11c6e21..73321ae 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/ParkedAccountsManager.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/ParkedAccountsManager.java
@@ -19,102 +19,51 @@ package org.killbill.billing.invoice;
 
 import java.util.UUID;
 
+import org.killbill.billing.ErrorCode;
 import org.killbill.billing.ObjectType;
 import org.killbill.billing.callcontext.InternalCallContext;
+import org.killbill.billing.tag.TagInternalApi;
 import org.killbill.billing.util.api.TagApiException;
 import org.killbill.billing.util.api.TagDefinitionApiException;
-import org.killbill.billing.util.api.TagUserApi;
-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.CallOrigin;
-import org.killbill.billing.util.callcontext.InternalCallContextFactory;
-import org.killbill.billing.util.callcontext.UserType;
-import org.killbill.billing.util.dao.NonEntityDao;
 import org.killbill.billing.util.tag.Tag;
-import org.killbill.billing.util.tag.dao.TagDefinitionDao;
-import org.killbill.billing.util.tag.dao.TagDefinitionModelDao;
-import org.killbill.clock.Clock;
 
-import com.google.common.annotations.VisibleForTesting;
 import com.google.common.base.Predicate;
 import com.google.common.collect.Iterables;
 import com.google.inject.Inject;
 
-public class ParkedAccountsManager {
+import static org.killbill.billing.util.tag.dao.SystemTags.PARK_TAG_DEFINITION_ID;
 
-    @VisibleForTesting
-    static final String PARK = "__PARK__";
+public class ParkedAccountsManager {
 
-    private final TagUserApi tagUserApi;
-    private final TagDefinitionDao tagDefinitionDao;
-    private final NonEntityDao nonEntityDao;
-    private final CacheControllerDispatcher cacheControllerDispatcher;
-    private /* final */ UUID tagDefinitionId;
+    private final TagInternalApi tagApi;
 
     @Inject
-    public ParkedAccountsManager(final TagUserApi tagUserApi,
-                                 final TagDefinitionDao tagDefinitionDao,
-                                 final NonEntityDao nonEntityDao,
-                                 final CacheControllerDispatcher cacheControllerDispatcher,
-                                 final Clock clock) throws TagDefinitionApiException {
-        this.tagUserApi = tagUserApi;
-        this.tagDefinitionDao = tagDefinitionDao;
-        this.nonEntityDao = nonEntityDao;
-        this.cacheControllerDispatcher = cacheControllerDispatcher;
-
-        retrieveOrCreateParkTagDefinition(clock);
+    public ParkedAccountsManager(final TagInternalApi tagApi) throws TagDefinitionApiException {
+        this.tagApi = tagApi;
     }
 
     // Idempotent
     public void parkAccount(final UUID accountId, final InternalCallContext internalCallContext) throws TagApiException {
-        final CallContext callContext = createCallContext(internalCallContext);
-        tagUserApi.addTag(accountId, ObjectType.ACCOUNT, tagDefinitionId, callContext);
+        try {
+            tagApi.addTag(accountId, ObjectType.ACCOUNT, PARK_TAG_DEFINITION_ID, internalCallContext);
+        } catch (final TagApiException e) {
+            if (ErrorCode.TAG_ALREADY_EXISTS.getCode() != e.getCode()) {
+                throw e;
+            }
+        }
     }
 
     public void unparkAccount(final UUID accountId, final InternalCallContext internalCallContext) throws TagApiException {
-        final CallContext callContext = createCallContext(internalCallContext);
-        tagUserApi.removeTag(accountId, ObjectType.ACCOUNT, tagDefinitionId, callContext);
+        tagApi.removeTag(accountId, ObjectType.ACCOUNT, PARK_TAG_DEFINITION_ID, internalCallContext);
     }
 
-    public boolean isParked(final UUID accountId, final InternalCallContext internalCallContext) throws TagApiException {
-        final CallContext callContext = createCallContext(internalCallContext);
-        return Iterables.<Tag>tryFind(tagUserApi.getTagsForAccount(accountId, false, callContext),
+    public boolean isParked(final InternalCallContext internalCallContext) throws TagApiException {
+        return Iterables.<Tag>tryFind(tagApi.getTagsForAccountType(ObjectType.ACCOUNT, false, internalCallContext),
                                       new Predicate<Tag>() {
                                           @Override
                                           public boolean apply(final Tag input) {
-                                              return tagDefinitionId.equals(input.getTagDefinitionId());
+                                              return PARK_TAG_DEFINITION_ID.equals(input.getTagDefinitionId());
                                           }
                                       }).orNull() != null;
     }
-
-    // TODO Consider creating a tag internal API to avoid this
-    private CallContext createCallContext(final InternalCallContext internalCallContext) {
-        final UUID tenantId = nonEntityDao.retrieveIdFromObject(internalCallContext.getTenantRecordId(),
-                                                                ObjectType.TENANT,
-                                                                cacheControllerDispatcher.getCacheController(CacheType.OBJECT_ID));
-        return internalCallContext.toCallContext(tenantId);
-    }
-
-    @VisibleForTesting
-    public void retrieveOrCreateParkTagDefinition(final Clock clock) throws TagDefinitionApiException {
-        final InternalCallContext callContext = new InternalCallContext(InternalCallContextFactory.INTERNAL_TENANT_RECORD_ID,
-                                                                        null,
-                                                                        null,
-                                                                        null,
-                                                                        UUID.randomUUID(),
-                                                                        ParkedAccountsManager.class.getName(),
-                                                                        CallOrigin.INTERNAL,
-                                                                        UserType.SYSTEM,
-                                                                        null,
-                                                                        null,
-                                                                        clock.getUTCNow(),
-                                                                        clock.getUTCNow());
-        // Need to use the DAO directly to bypass validations
-        TagDefinitionModelDao tagDefinitionModelDao = tagDefinitionDao.getByName(PARK, callContext);
-        if (tagDefinitionModelDao == null) {
-            tagDefinitionModelDao = tagDefinitionDao.create(PARK, "Accounts with invalid invoicing state", callContext);
-        }
-        this.tagDefinitionId = tagDefinitionModelDao.getId();
-    }
 }
diff --git a/invoice/src/test/java/org/killbill/billing/invoice/TestInvoiceDispatcher.java b/invoice/src/test/java/org/killbill/billing/invoice/TestInvoiceDispatcher.java
index f22a72b..36a536b 100644
--- a/invoice/src/test/java/org/killbill/billing/invoice/TestInvoiceDispatcher.java
+++ b/invoice/src/test/java/org/killbill/billing/invoice/TestInvoiceDispatcher.java
@@ -27,7 +27,6 @@ import org.joda.time.LocalDate;
 import org.killbill.billing.ErrorCode;
 import org.killbill.billing.account.api.Account;
 import org.killbill.billing.account.api.AccountApiException;
-import org.killbill.billing.callcontext.DefaultTenantContext;
 import org.killbill.billing.callcontext.InternalCallContext;
 import org.killbill.billing.catalog.MockPlan;
 import org.killbill.billing.catalog.MockPlanPhase;
@@ -56,7 +55,7 @@ import org.killbill.billing.subscription.api.SubscriptionBaseTransitionType;
 import org.killbill.billing.subscription.api.user.SubscriptionBaseApiException;
 import org.killbill.billing.util.api.TagDefinitionApiException;
 import org.killbill.billing.util.tag.Tag;
-import org.killbill.billing.util.tag.TagDefinition;
+import org.killbill.billing.util.tag.dao.SystemTags;
 import org.mockito.Mockito;
 import org.skife.jdbi.v2.Handle;
 import org.skife.jdbi.v2.tweak.HandleCallback;
@@ -80,8 +79,6 @@ public class TestInvoiceDispatcher extends InvoiceTestSuiteWithEmbeddedDB {
         account = invoiceUtil.createAccount(callContext);
         subscription = invoiceUtil.createSubscription();
         context = internalCallContextFactory.createInternalCallContext(account.getId(), callContext);
-        // Tests will delete the database entries, yet ParkedAccountsManager is injected once
-        parkedAccountsManager.retrieveOrCreateParkTagDefinition(clock);
     }
 
     @Test(groups = "slow")
@@ -151,10 +148,6 @@ public class TestInvoiceDispatcher extends InvoiceTestSuiteWithEmbeddedDB {
                                                                    internalCallContextFactory, invoiceNotifier, invoicePluginDispatcher, locker, busService.getBus(),
                                                                    null, invoiceConfig, clock, parkedAccountsManager);
 
-        // Verify the __PARK__ tag definition has been created
-        final TagDefinition tagDefinition = tagUserApi.getTagDefinitionForName(ParkedAccountsManager.PARK, new DefaultTenantContext(null));
-        Assert.assertNotNull(tagDefinition);
-
         // Verify initial tags state for account
         Assert.assertTrue(tagUserApi.getTagsForAccount(accountId, true, callContext).isEmpty());
 
@@ -224,7 +217,7 @@ public class TestInvoiceDispatcher extends InvoiceTestSuiteWithEmbeddedDB {
         // No dry-run: account is parked
         final List<Tag> tags = tagUserApi.getTagsForAccount(accountId, false, callContext);
         Assert.assertEquals(tags.size(), 1);
-        Assert.assertEquals(tags.get(0).getTagDefinitionId(), tagDefinition.getId());
+        Assert.assertEquals(tags.get(0).getTagDefinitionId(), SystemTags.PARK_TAG_DEFINITION_ID);
 
         // isApiCall=false
         final Invoice nullInvoice1 = dispatcher.processAccount(accountId, target, null, context);
diff --git a/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/AdminResource.java b/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/AdminResource.java
index c964e67..056ce07 100644
--- a/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/AdminResource.java
+++ b/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/AdminResource.java
@@ -17,13 +17,16 @@
 
 package org.killbill.billing.jaxrs.resources;
 
-import java.util.List;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.net.URI;
 import java.util.UUID;
 
 import javax.inject.Inject;
 import javax.servlet.http.HttpServletRequest;
 import javax.ws.rs.Consumes;
 import javax.ws.rs.DELETE;
+import javax.ws.rs.DefaultValue;
 import javax.ws.rs.HeaderParam;
 import javax.ws.rs.POST;
 import javax.ws.rs.PUT;
@@ -31,11 +34,16 @@ 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 org.killbill.billing.ErrorCode;
 import org.killbill.billing.ObjectType;
 import org.killbill.billing.account.api.AccountUserApi;
+import org.killbill.billing.invoice.api.InvoiceApiException;
+import org.killbill.billing.invoice.api.InvoiceUserApi;
 import org.killbill.billing.jaxrs.json.AdminPaymentJson;
 import org.killbill.billing.jaxrs.util.Context;
 import org.killbill.billing.jaxrs.util.JaxrsUriBuilder;
@@ -56,10 +64,15 @@ import org.killbill.billing.util.api.TagUserApi;
 import org.killbill.billing.util.cache.Cachable.CacheType;
 import org.killbill.billing.util.callcontext.CallContext;
 import org.killbill.billing.util.callcontext.TenantContext;
+import org.killbill.billing.util.entity.Pagination;
+import org.killbill.billing.util.tag.Tag;
+import org.killbill.billing.util.tag.dao.SystemTags;
 import org.killbill.clock.Clock;
 
+import com.fasterxml.jackson.core.JsonGenerator;
 import com.google.common.base.Predicate;
 import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.Iterables;
 import com.google.inject.Singleton;
 import io.swagger.annotations.Api;
@@ -77,20 +90,21 @@ import static javax.ws.rs.core.MediaType.APPLICATION_JSON;
 public class AdminResource extends JaxRsResourceBase {
 
     private final AdminPaymentApi adminPaymentApi;
+    private final InvoiceUserApi invoiceUserApi;
     private final TenantUserApi tenantApi;
     private final CacheManager cacheManager;
     private final RecordIdApi recordIdApi;
 
     @Inject
-    public AdminResource(final JaxrsUriBuilder uriBuilder, final TagUserApi tagUserApi, final CustomFieldUserApi customFieldUserApi, final AuditUserApi auditUserApi, final AccountUserApi accountUserApi, final PaymentApi paymentApi, final AdminPaymentApi adminPaymentApi, final CacheManager cacheManager, final TenantUserApi tenantApi, final RecordIdApi recordIdApi, final Clock clock, final Context context) {
+    public AdminResource(final JaxrsUriBuilder uriBuilder, final TagUserApi tagUserApi, final CustomFieldUserApi customFieldUserApi, final AuditUserApi auditUserApi, final AccountUserApi accountUserApi, final PaymentApi paymentApi, final AdminPaymentApi adminPaymentApi, final InvoiceUserApi invoiceUserApi, final CacheManager cacheManager, final TenantUserApi tenantApi, final RecordIdApi recordIdApi, final Clock clock, final Context context) {
         super(uriBuilder, tagUserApi, customFieldUserApi, auditUserApi, accountUserApi, paymentApi, null, clock, context);
         this.adminPaymentApi = adminPaymentApi;
+        this.invoiceUserApi = invoiceUserApi;
         this.tenantApi = tenantApi;
         this.recordIdApi = recordIdApi;
         this.cacheManager = cacheManager;
     }
 
-
     @PUT
     @Consumes(APPLICATION_JSON)
     @Produces(APPLICATION_JSON)
@@ -123,13 +137,71 @@ public class AdminResource extends JaxRsResourceBase {
         return Response.status(Status.OK).build();
     }
 
+    @POST
+    @Consumes(APPLICATION_JSON)
+    @Produces(APPLICATION_JSON)
+    @Path("/invoices")
+    @ApiOperation(value = "Trigger an invoice generation for all parked accounts")
+    @ApiResponses(value = {})
+    public Response triggerInvoiceGenerationForParkedAccounts(@QueryParam(QUERY_SEARCH_OFFSET) @DefaultValue("0") final Long offset,
+                                                              @QueryParam(QUERY_SEARCH_LIMIT) @DefaultValue("100") final Long limit,
+                                                              @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) {
+        final CallContext callContext = context.createContext(createdBy, reason, comment, request);
+
+        // TODO Consider adding a real invoice API post 0.18.x
+        final Pagination<Tag> tags = tagUserApi.searchTags(SystemTags.PARK_TAG_DEFINITION_NAME, offset, limit, callContext);
+
+        // Return the accounts still parked
+        final StreamingOutput json = new StreamingOutput() {
+            @Override
+            public void write(final OutputStream output) throws IOException, WebApplicationException {
+                final JsonGenerator generator = mapper.getFactory().createGenerator(output);
+                generator.configure(JsonGenerator.Feature.AUTO_CLOSE_TARGET, false);
+
+                generator.writeStartArray();
+                for (final Tag tag : tags) {
+                    final UUID accountId = tag.getObjectId();
+                    try {
+                        invoiceUserApi.triggerInvoiceGeneration(accountId, clock.getUTCToday(), null, callContext);
+                    } catch (final InvoiceApiException e) {
+                        if (e.getCode() == ErrorCode.UNEXPECTED_ERROR.getCode()) {
+                            generator.writeString(accountId.toString());
+                        }
+                        if (e.getCode() != ErrorCode.INVOICE_NOTHING_TO_DO.getCode()) {
+                            log.warn("Unable to trigger invoice generation for accountId='{}'", accountId);
+                        }
+                    }
+                }
+                generator.writeEndArray();
+                generator.close();
+            }
+        };
+
+        final URI nextPageUri = uriBuilder.nextPage(AdminResource.class,
+                                                    "triggerInvoiceGenerationForParkedAccounts",
+                                                    tags.getNextOffset(),
+                                                    limit,
+                                                    ImmutableMap.<String, String>of());
+        return Response.status(Status.OK)
+                       .entity(json)
+                       .header(HDR_PAGINATION_CURRENT_OFFSET, tags.getCurrentOffset())
+                       .header(HDR_PAGINATION_NEXT_OFFSET, tags.getNextOffset())
+                       .header(HDR_PAGINATION_TOTAL_NB_RECORDS, tags.getTotalNbRecords())
+                       .header(HDR_PAGINATION_MAX_NB_RECORDS, tags.getMaxNbRecords())
+                       .header(HDR_PAGINATION_NEXT_PAGE_URI, nextPageUri)
+                       .build();
+    }
+
     @DELETE
     @Path("/" + CACHE)
     @Produces(APPLICATION_JSON)
     @ApiOperation(value = "Invalidates the given Cache if specified, otherwise invalidates all caches")
     @ApiResponses(value = {@ApiResponse(code = 400, message = "Cache name does not exist or is not alive")})
     public Response invalidatesCache(@QueryParam("cacheName") final String cacheName,
-                                    @javax.ws.rs.core.Context final HttpServletRequest request) {
+                                     @javax.ws.rs.core.Context final HttpServletRequest request) {
         if (null != cacheName && !cacheName.isEmpty()) {
             final Ehcache cache = cacheManager.getEhcache(cacheName);
             // check if cache is null
@@ -139,8 +211,7 @@ public class AdminResource extends JaxRsResourceBase {
             }
             // Clear given cache
             cache.removeAll();
-        }
-        else {
+        } else {
             // if not given a specific cacheName, clear all
             cacheManager.clearAll();
         }
@@ -176,7 +247,7 @@ public class AdminResource extends JaxRsResourceBase {
     @ApiOperation(value = "Invalidates Caches per tenant level")
     @ApiResponses(value = {})
     public Response invalidatesCacheByTenant(@QueryParam("tenantApiKey") final String tenantApiKey,
-                                              @javax.ws.rs.core.Context final HttpServletRequest request) throws TenantApiException {
+                                             @javax.ws.rs.core.Context final HttpServletRequest request) throws TenantApiException {
 
         // creating Tenant Context from Request
         TenantContext tenantContext = context.createContext(request);
diff --git a/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestAdmin.java b/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestAdmin.java
index 288e8bd..59d24be 100644
--- a/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestAdmin.java
+++ b/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestAdmin.java
@@ -18,21 +18,40 @@
 package org.killbill.billing.jaxrs;
 
 import java.math.BigDecimal;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
 import java.util.UUID;
 
+import org.joda.time.DateTime;
+import org.killbill.billing.catalog.api.BillingPeriod;
+import org.killbill.billing.catalog.api.ProductCategory;
+import org.killbill.billing.client.JaxrsResource;
 import org.killbill.billing.client.KillBillClientException;
 import org.killbill.billing.client.KillBillHttpClient;
+import org.killbill.billing.client.RequestOptions;
 import org.killbill.billing.client.model.Account;
+import org.killbill.billing.client.model.Invoice;
 import org.killbill.billing.client.model.Payment;
 import org.killbill.billing.client.model.PaymentTransaction;
 import org.killbill.billing.jaxrs.json.AdminPaymentJson;
 import org.killbill.billing.payment.api.TransactionStatus;
+import org.killbill.billing.util.api.AuditLevel;
+import org.killbill.billing.util.jackson.ObjectMapper;
 import org.testng.Assert;
 import org.testng.annotations.Test;
 
+import com.ning.http.client.Response;
+
 import com.google.common.collect.HashMultimap;
+import com.google.common.collect.ImmutableMultimap;
 import com.google.common.collect.Multimap;
 
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertNotNull;
+
 public class TestAdmin extends TestJaxrsBase {
 
     @Test(groups = "slow")
@@ -77,6 +96,70 @@ public class TestAdmin extends TestJaxrsBase {
         doCapture(updatedPayment2, true);
     }
 
+    @Test(groups = "slow")
+    public void testAdminInvoiceEndpoint() throws Exception {
+        final DateTime initialDate = new DateTime(2012, 4, 25, 0, 3, 42, 0);
+        clock.setDeltaFromReality(initialDate.getMillis() - clock.getUTCNow().getMillis());
+
+        final Collection<UUID> accounts = new HashSet<UUID>();
+        for (int i = 0; i < 5; i++) {
+            final Account accountJson = createAccountWithDefaultPaymentMethod();
+            assertNotNull(accountJson);
+            accounts.add(accountJson.getAccountId());
+
+            createEntitlement(accountJson.getAccountId(),
+                              UUID.randomUUID().toString(),
+                              "Shotgun",
+                              ProductCategory.BASE,
+                              BillingPeriod.MONTHLY,
+                              true);
+            clock.addDays(2);
+            crappyWaitForLackOfProperSynchonization();
+
+            Assert.assertEquals(killBillClient.getInvoices(requestOptions).getPaginationMaxNbRecords(), i + 1);
+            final List<Invoice> invoices = killBillClient.getInvoicesForAccount(accountJson.getAccountId(), false, false, false, AuditLevel.NONE, requestOptions);
+            assertEquals(invoices.size(), 1);
+        }
+
+        // Trigger first non-trial invoice
+        clock.addDays(32);
+        crappyWaitForLackOfProperSynchonization();
+
+        Assert.assertEquals(killBillClient.getInvoices(requestOptions).getPaginationMaxNbRecords(), 10);
+        for (final UUID accountId : accounts) {
+            final List<Invoice> invoices = killBillClient.getInvoicesForAccount(accountId, false, false, false, AuditLevel.NONE, requestOptions);
+            assertEquals(invoices.size(), 2);
+        }
+
+        // Upload the config
+        final ObjectMapper mapper = new ObjectMapper();
+        final Map<String, String> perTenantProperties = new HashMap<String, String>();
+        perTenantProperties.put("org.killbill.invoice.enabled", "false");
+        final String perTenantConfig = mapper.writeValueAsString(perTenantProperties);
+        killBillClient.postConfigurationPropertiesForTenant(perTenantConfig, requestOptions);
+        crappyWaitForLackOfProperSynchonization();
+
+        // Verify the second invoice isn't generated
+        clock.addDays(32);
+        crappyWaitForLackOfProperSynchonization();
+
+        Assert.assertEquals(killBillClient.getInvoices(requestOptions).getPaginationMaxNbRecords(), 10);
+        for (final UUID accountId : accounts) {
+            final List<Invoice> invoices = killBillClient.getInvoicesForAccount(accountId, false, false, false, AuditLevel.NONE, requestOptions);
+            assertEquals(invoices.size(), 2);
+        }
+
+        // Fix one account
+        final Response response = triggerInvoiceGenerationForParkedAccounts(1);
+        Assert.assertEquals(response.getResponseBody(), "[]");
+        Assert.assertEquals(killBillClient.getInvoices(requestOptions).getPaginationMaxNbRecords(), 11);
+
+        // Fix all accounts
+        final Response response2 = triggerInvoiceGenerationForParkedAccounts(5);
+        Assert.assertEquals(response2.getResponseBody(), "[]");
+        Assert.assertEquals(killBillClient.getInvoices(requestOptions).getPaginationMaxNbRecords(), 15);
+    }
+
     private void doCapture(final Payment payment, final boolean expectException) throws KillBillClientException {
         // Payment object does not export state, this is purely internal, so to verify that we indeed changed to Failed, we can attempt
         // a capture, which should fail
@@ -100,7 +183,6 @@ public class TestAdmin extends TestJaxrsBase {
 
     }
 
-
     private void fixPaymentState(final Payment payment, final String lastSuccessPaymentState, final String currentPaymentStateName, final TransactionStatus transactionStatus) throws KillBillClientException {
         //
         // We do not expose the endpoint in the client API on purpose since this should only be accessed using special permission ADMIN_CAN_FIX_DATA
@@ -115,4 +197,15 @@ public class TestAdmin extends TestJaxrsBase {
         result.put(KillBillHttpClient.AUDIT_OPTION_COMMENT, comment);
         killBillHttpClient.doPut(uri, body, result);
     }
+
+    private Response triggerInvoiceGenerationForParkedAccounts(final int limit) throws KillBillClientException {
+        final String uri = "/1.0/kb/admin/invoices";
+
+        final RequestOptions requestOptions = RequestOptions.builder()
+                                                            .withQueryParams(ImmutableMultimap.<String, String>of(JaxrsResource.QUERY_SEARCH_LIMIT, String.valueOf(limit)))
+                                                            .withCreatedBy(createdBy)
+                                                            .withReason(reason)
+                                                            .withComment(comment).build();
+        return killBillHttpClient.doPost(uri, null, requestOptions);
+    }
 }
diff --git a/util/src/main/java/org/killbill/billing/util/tag/dao/DefaultTagDao.java b/util/src/main/java/org/killbill/billing/util/tag/dao/DefaultTagDao.java
index ed2d38c..1b4c9ec 100644
--- a/util/src/main/java/org/killbill/billing/util/tag/dao/DefaultTagDao.java
+++ b/util/src/main/java/org/killbill/billing/util/tag/dao/DefaultTagDao.java
@@ -166,13 +166,7 @@ public class DefaultTagDao extends EntityDaoBase<TagModelDao, Tag, TagApiExcepti
     }
 
     private TagDefinitionModelDao getTagDefinitionFromTransaction(final UUID tagDefinitionId, final EntitySqlDaoWrapperFactory entitySqlDaoWrapperFactory, final InternalTenantContext context) throws TagApiException {
-        TagDefinitionModelDao tagDefintion = null;
-        for (final ControlTagType t : ControlTagType.values()) {
-            if (t.getId().equals(tagDefinitionId)) {
-                tagDefintion = new TagDefinitionModelDao(t);
-                break;
-            }
-        }
+        TagDefinitionModelDao tagDefintion = SystemTags.lookup(tagDefinitionId);
         if (tagDefintion == null) {
             final TagDefinitionSqlDao transTagDefintionSqlDao = entitySqlDaoWrapperFactory.become(TagDefinitionSqlDao.class);
             tagDefintion = transTagDefintionSqlDao.getById(tagDefinitionId.toString(), context);
diff --git a/util/src/main/java/org/killbill/billing/util/tag/dao/DefaultTagDefinitionDao.java b/util/src/main/java/org/killbill/billing/util/tag/dao/DefaultTagDefinitionDao.java
index 46128ce..e406380 100644
--- a/util/src/main/java/org/killbill/billing/util/tag/dao/DefaultTagDefinitionDao.java
+++ b/util/src/main/java/org/killbill/billing/util/tag/dao/DefaultTagDefinitionDao.java
@@ -24,30 +24,28 @@ import java.util.LinkedList;
 import java.util.List;
 import java.util.UUID;
 
-import org.killbill.billing.util.callcontext.InternalCallContextFactory;
-import org.skife.jdbi.v2.IDBI;
-import org.skife.jdbi.v2.exceptions.TransactionFailedException;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
 import org.killbill.billing.BillingExceptionBase;
 import org.killbill.billing.ErrorCode;
-import org.killbill.bus.api.PersistentBus;
 import org.killbill.billing.callcontext.InternalCallContext;
 import org.killbill.billing.callcontext.InternalTenantContext;
-import org.killbill.clock.Clock;
 import org.killbill.billing.events.TagDefinitionInternalEvent;
 import org.killbill.billing.util.api.TagDefinitionApiException;
 import org.killbill.billing.util.audit.ChangeType;
 import org.killbill.billing.util.cache.CacheControllerDispatcher;
+import org.killbill.billing.util.callcontext.InternalCallContextFactory;
 import org.killbill.billing.util.dao.NonEntityDao;
 import org.killbill.billing.util.entity.dao.EntityDaoBase;
 import org.killbill.billing.util.entity.dao.EntitySqlDaoTransactionWrapper;
 import org.killbill.billing.util.entity.dao.EntitySqlDaoTransactionalJdbiWrapper;
 import org.killbill.billing.util.entity.dao.EntitySqlDaoWrapperFactory;
-import org.killbill.billing.util.tag.ControlTagType;
 import org.killbill.billing.util.tag.TagDefinition;
 import org.killbill.billing.util.tag.api.user.TagEventBuilder;
+import org.killbill.bus.api.PersistentBus;
+import org.killbill.clock.Clock;
+import org.skife.jdbi.v2.IDBI;
+import org.skife.jdbi.v2.exceptions.TransactionFailedException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 import com.google.common.base.Function;
 import com.google.common.collect.Collections2;
@@ -81,9 +79,7 @@ public class DefaultTagDefinitionDao extends EntityDaoBase<TagDefinitionModelDao
                 Iterators.addAll(definitionList, all);
 
                 // Add invoice tag definitions
-                for (final ControlTagType controlTag : ControlTagType.values()) {
-                    definitionList.add(new TagDefinitionModelDao(controlTag));
-                }
+                definitionList.addAll(SystemTags.all());
                 return definitionList;
             }
         });
@@ -94,12 +90,8 @@ public class DefaultTagDefinitionDao extends EntityDaoBase<TagDefinitionModelDao
         return transactionalSqlDao.execute(new EntitySqlDaoTransactionWrapper<TagDefinitionModelDao>() {
             @Override
             public TagDefinitionModelDao inTransaction(final EntitySqlDaoWrapperFactory entitySqlDaoWrapperFactory) throws Exception {
-                for (final ControlTagType controlTag : ControlTagType.values()) {
-                    if (controlTag.name().equals(definitionName)) {
-                        return new TagDefinitionModelDao(controlTag);
-                    }
-                }
-                return entitySqlDaoWrapperFactory.become(TagDefinitionSqlDao.class).getByName(definitionName, context);
+                final TagDefinitionModelDao tagDefinitionModelDao = SystemTags.lookup(definitionName);
+                return tagDefinitionModelDao != null ? tagDefinitionModelDao : entitySqlDaoWrapperFactory.become(TagDefinitionSqlDao.class).getByName(definitionName, context);
             }
         });
     }
@@ -109,12 +101,8 @@ public class DefaultTagDefinitionDao extends EntityDaoBase<TagDefinitionModelDao
         return transactionalSqlDao.execute(new EntitySqlDaoTransactionWrapper<TagDefinitionModelDao>() {
             @Override
             public TagDefinitionModelDao inTransaction(final EntitySqlDaoWrapperFactory entitySqlDaoWrapperFactory) throws Exception {
-                for (final ControlTagType controlTag : ControlTagType.values()) {
-                    if (controlTag.getId().equals(definitionId)) {
-                        return new TagDefinitionModelDao(controlTag);
-                    }
-                }
-                return entitySqlDaoWrapperFactory.become(TagDefinitionSqlDao.class).getById(definitionId.toString(), context);
+                final TagDefinitionModelDao tagDefinitionModelDao = SystemTags.lookup(definitionId);
+                return tagDefinitionModelDao != null ? tagDefinitionModelDao : entitySqlDaoWrapperFactory.become(TagDefinitionSqlDao.class).getById(definitionId.toString(), context);
             }
         });
     }
@@ -126,11 +114,9 @@ public class DefaultTagDefinitionDao extends EntityDaoBase<TagDefinitionModelDao
             public List<TagDefinitionModelDao> inTransaction(final EntitySqlDaoWrapperFactory entitySqlDaoWrapperFactory) throws Exception {
                 final List<TagDefinitionModelDao> result = new LinkedList<TagDefinitionModelDao>();
                 for (final UUID cur : definitionIds) {
-                    for (final ControlTagType controlTag : ControlTagType.values()) {
-                        if (controlTag.getId().equals(cur)) {
-                            result.add(new TagDefinitionModelDao(controlTag));
-                            break;
-                        }
+                    final TagDefinitionModelDao tagDefinitionModelDao = SystemTags.lookup(cur);
+                    if (tagDefinitionModelDao != null) {
+                        result.add(tagDefinitionModelDao);
                     }
                 }
                 if (definitionIds.size() > 0) {
diff --git a/util/src/main/java/org/killbill/billing/util/tag/dao/SystemTags.java b/util/src/main/java/org/killbill/billing/util/tag/dao/SystemTags.java
new file mode 100644
index 0000000..adede89
--- /dev/null
+++ b/util/src/main/java/org/killbill/billing/util/tag/dao/SystemTags.java
@@ -0,0 +1,77 @@
+/*
+ * 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.
+ */
+
+package org.killbill.billing.util.tag.dao;
+
+import java.util.Collection;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.UUID;
+
+import org.killbill.billing.util.tag.ControlTagType;
+
+import com.google.common.collect.ImmutableList;
+
+public class SystemTags {
+
+    // Invoice
+    public static final UUID PARK_TAG_DEFINITION_ID = new UUID(1, 1);
+    public static final String PARK_TAG_DEFINITION_NAME = "__PARK__";
+
+    // Note! TagSqlDao.sql.stg needs to be kept in sync (see userAndSystemTagDefinitions)
+    private static final List<TagDefinitionModelDao> SYSTEM_DEFINED_TAG_DEFINITIONS = ImmutableList.<TagDefinitionModelDao>of(new TagDefinitionModelDao(PARK_TAG_DEFINITION_ID, null, null, PARK_TAG_DEFINITION_NAME, "Accounts with invalid invoicing state"));
+
+    public static Collection<TagDefinitionModelDao> all() {
+        final Collection<TagDefinitionModelDao> all = new LinkedList<TagDefinitionModelDao>(SYSTEM_DEFINED_TAG_DEFINITIONS);
+        for (final ControlTagType controlTag : ControlTagType.values()) {
+            all.add(new TagDefinitionModelDao(controlTag));
+        }
+        return all;
+    }
+
+    public static TagDefinitionModelDao lookup(final String tagDefinitionName) {
+        for (final ControlTagType t : ControlTagType.values()) {
+            if (t.name().equals(tagDefinitionName)) {
+                return new TagDefinitionModelDao(t);
+            }
+        }
+
+        for (final TagDefinitionModelDao t : SYSTEM_DEFINED_TAG_DEFINITIONS) {
+            if (t.getName().equals(tagDefinitionName)) {
+                return t;
+            }
+        }
+
+        return null;
+    }
+
+    public static TagDefinitionModelDao lookup(final UUID tagDefinitionId) {
+        for (final ControlTagType t : ControlTagType.values()) {
+            if (t.getId().equals(tagDefinitionId)) {
+                return new TagDefinitionModelDao(t);
+            }
+        }
+
+        for (final TagDefinitionModelDao t : SYSTEM_DEFINED_TAG_DEFINITIONS) {
+            if (t.getId().equals(tagDefinitionId)) {
+                return t;
+            }
+        }
+
+        return null;
+    }
+}
diff --git a/util/src/main/resources/org/killbill/billing/util/tag/dao/TagSqlDao.sql.stg b/util/src/main/resources/org/killbill/billing/util/tag/dao/TagSqlDao.sql.stg
index 00c7bbe..3240d91 100644
--- a/util/src/main/resources/org/killbill/billing/util/tag/dao/TagSqlDao.sql.stg
+++ b/util/src/main/resources/org/killbill/billing/util/tag/dao/TagSqlDao.sql.stg
@@ -100,6 +100,11 @@ userAndSystemTagDefinitions() ::= <<
     \'00000000-0000-0000-0000-000000000007\' id
   , \'PARTNER\' as name
   , \'Indicates that this is a partner account\' description
+  union
+  select
+    \'00000000-0000-0001-0000-000000000001\' id
+  , \'__PARK__\' as name
+  , \'Accounts with invalid invoicing state\' description
 >>
 
 searchQuery(tagAlias, tagDefinitionAlias) ::= <<
diff --git a/util/src/test/java/org/killbill/billing/util/tag/dao/TestDefaultTagDao.java b/util/src/test/java/org/killbill/billing/util/tag/dao/TestDefaultTagDao.java
index fd16051..b110370 100644
--- a/util/src/test/java/org/killbill/billing/util/tag/dao/TestDefaultTagDao.java
+++ b/util/src/test/java/org/killbill/billing/util/tag/dao/TestDefaultTagDao.java
@@ -69,7 +69,7 @@ public class TestDefaultTagDao extends UtilTestSuiteWithEmbeddedDB {
         assertEquals(result.size(), 4);
 
         result = tagDefinitionDao.getTagDefinitions(internalCallContext);
-        assertEquals(result.size(), 3 + ControlTagType.values().length);
+        assertEquals(result.size(), 3 + SystemTags.all().size());
     }
 
     @Test(groups = "slow")