diff --git a/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/InvoiceItemResource.java b/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/InvoiceItemResource.java
new file mode 100644
index 0000000..7235277
--- /dev/null
+++ b/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/InvoiceItemResource.java
@@ -0,0 +1,202 @@
+/*
+ * Copyright 2014-2018 Groupon, Inc
+ * Copyright 2014-2018 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.jaxrs.resources;
+
+import java.util.List;
+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.GET;
+import javax.ws.rs.HeaderParam;
+import javax.ws.rs.POST;
+import javax.ws.rs.PUT;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+import javax.ws.rs.Produces;
+import javax.ws.rs.QueryParam;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.UriInfo;
+
+import org.killbill.billing.ObjectType;
+import org.killbill.billing.account.api.Account;
+import org.killbill.billing.account.api.AccountApiException;
+import org.killbill.billing.account.api.AccountUserApi;
+import org.killbill.billing.entitlement.api.SubscriptionApi;
+import org.killbill.billing.jaxrs.json.CustomFieldJson;
+import org.killbill.billing.jaxrs.json.TagJson;
+import org.killbill.billing.jaxrs.util.Context;
+import org.killbill.billing.jaxrs.util.JaxrsUriBuilder;
+import org.killbill.billing.payment.api.PaymentApi;
+import org.killbill.billing.util.api.AuditUserApi;
+import org.killbill.billing.util.api.CustomFieldApiException;
+import org.killbill.billing.util.api.CustomFieldUserApi;
+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.callcontext.TenantContext;
+import org.killbill.clock.Clock;
+import org.killbill.commons.metrics.TimedResource;
+
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import io.swagger.annotations.ApiResponse;
+import io.swagger.annotations.ApiResponses;
+
+import static javax.ws.rs.core.MediaType.APPLICATION_JSON;
+
+@Path(JaxrsResource.INVOICES_ITEMS_PATH)
+@Api(value = JaxrsResource.INVOICES_ITEMS_PATH, description = "Operations on invoice items")
+public class InvoiceItemResource extends JaxRsResourceBase {
+ private static final String ID_PARAM_NAME = "invoiceItemId";
+
+ @Inject
+ public InvoiceItemResource(final JaxrsUriBuilder uriBuilder, final TagUserApi tagUserApi, final CustomFieldUserApi customFieldUserApi,
+ final AuditUserApi auditUserApi, final AccountUserApi accountUserApi, final PaymentApi paymentApi,
+ final SubscriptionApi subscriptionApi, final Clock clock, final Context context) {
+ super(uriBuilder, tagUserApi, customFieldUserApi, auditUserApi, accountUserApi, paymentApi, subscriptionApi, clock, context);
+ }
+
+ @TimedResource
+ @GET
+ @Path("/{invoiceItemId:" + UUID_PATTERN + "}/" + CUSTOM_FIELDS)
+ @Produces(APPLICATION_JSON)
+ @ApiOperation(value = "Retrieve invoice item custom fields", response = CustomFieldJson.class, responseContainer = "List")
+ @ApiResponses(value = {@ApiResponse(code = 400, message = "Invalid invoice item id supplied")})
+ public Response getCustomFields(@PathParam(ID_PARAM_NAME) final String id,
+ @QueryParam(QUERY_AUDIT) @DefaultValue("NONE") final AuditMode auditMode,
+ @javax.ws.rs.core.Context final HttpServletRequest request) {
+ return super.getCustomFields(UUID.fromString(id), auditMode, context.createTenantContextNoAccountId(request));
+ }
+
+ @TimedResource
+ @POST
+ @Path("/{invoiceItemId:" + UUID_PATTERN + "}/" + CUSTOM_FIELDS)
+ @Consumes(APPLICATION_JSON)
+ @Produces(APPLICATION_JSON)
+ @ApiOperation(value = "Add custom fields to invoice item")
+ @ApiResponses(value = {@ApiResponse(code = 400, message = "Invalid invoice item id supplied")})
+ public Response createCustomFields(@PathParam(ID_PARAM_NAME) final String id,
+ final List<CustomFieldJson> customFields,
+ @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 CustomFieldApiException {
+ return super.createCustomFields(UUID.fromString(id), customFields,
+ context.createCallContextNoAccountId(createdBy, reason, comment, request), uriInfo, request);
+ }
+
+
+ @TimedResource
+ @PUT
+ @Path("/{invoiceItemId:" + UUID_PATTERN + "}/" + CUSTOM_FIELDS)
+ @Consumes(APPLICATION_JSON)
+ @Produces(APPLICATION_JSON)
+ @ApiOperation(value = "Modify custom fields to invoice item")
+ @ApiResponses(value = {@ApiResponse(code = 400, message = "Invalid invoice item id supplied")})
+ public Response modifyCustomFields(@PathParam(ID_PARAM_NAME) final String id,
+ final List<CustomFieldJson> customFields,
+ @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) throws CustomFieldApiException {
+ return super.modifyCustomFields(UUID.fromString(id), customFields,
+ context.createCallContextNoAccountId(createdBy, reason, comment, request));
+ }
+
+
+ @TimedResource
+ @DELETE
+ @Path("/{invoiceItemId:" + UUID_PATTERN + "}/" + CUSTOM_FIELDS)
+ @Consumes(APPLICATION_JSON)
+ @Produces(APPLICATION_JSON)
+ @ApiOperation(value = "Remove custom fields from invoice item")
+ @ApiResponses(value = {@ApiResponse(code = 400, message = "Invalid invoice item id supplied")})
+ public Response deleteCustomFields(@PathParam(ID_PARAM_NAME) final String id,
+ @QueryParam(QUERY_CUSTOM_FIELDS) final String customFieldList,
+ @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) throws CustomFieldApiException {
+ return super.deleteCustomFields(UUID.fromString(id), customFieldList,
+ context.createCallContextNoAccountId(createdBy, reason, comment, request));
+ }
+
+ @TimedResource
+ @GET
+ @Path("/{invoiceItemId:" + UUID_PATTERN + "}/" + TAGS)
+ @Produces(APPLICATION_JSON)
+ @ApiOperation(value = "Retrieve invoice item tags", response = TagJson.class, responseContainer = "List")
+ @ApiResponses(value = {@ApiResponse(code = 400, message = "Invalid invoice item id supplied"),
+ @ApiResponse(code = 404, message = "Account not found")})
+ public Response getTags(@PathParam(ID_PARAM_NAME) final String id,
+ @QueryParam(QUERY_ACCOUNT_ID) final String accountIdStr,
+ @QueryParam(QUERY_AUDIT) @DefaultValue("NONE") final AuditMode auditMode,
+ @QueryParam(QUERY_TAGS_INCLUDED_DELETED) @DefaultValue("false") final Boolean includedDeleted,
+ @javax.ws.rs.core.Context final HttpServletRequest request) throws TagDefinitionApiException, AccountApiException {
+ final UUID accountId = UUID.fromString(accountIdStr);
+ final TenantContext tenantContext = context.createTenantContextWithAccountId(accountId, request);
+ final Account account = accountUserApi.getAccountById(accountId, tenantContext);
+
+ return super.getTags(account.getId(), UUID.fromString(id), auditMode, includedDeleted, tenantContext);
+ }
+
+ @TimedResource
+ @POST
+ @Path("/{invoiceItemId:" + UUID_PATTERN + "}/" + TAGS)
+ @Consumes(APPLICATION_JSON)
+ @Produces(APPLICATION_JSON)
+ @ApiOperation(value = "Add tags to invoice item")
+ @ApiResponses(value = {@ApiResponse(code = 400, message = "Invalid invoice item id supplied")})
+ public Response createTags(@PathParam(ID_PARAM_NAME) final String id,
+ @QueryParam(QUERY_TAGS) final String tagList,
+ @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 UriInfo uriInfo,
+ @javax.ws.rs.core.Context final HttpServletRequest request) throws TagApiException {
+ return super.createTags(UUID.fromString(id), tagList, uriInfo,
+ context.createCallContextNoAccountId(createdBy, reason, comment, request), request);
+ }
+
+ @TimedResource
+ @DELETE
+ @Path("/{invoiceItemId:" + UUID_PATTERN + "}/" + TAGS)
+ @Consumes(APPLICATION_JSON)
+ @Produces(APPLICATION_JSON)
+ @ApiOperation(value = "Remove tags from invoice item")
+ @ApiResponses(value = {@ApiResponse(code = 400, message = "Invalid invoice item id supplied")})
+ public Response deleteTags(@PathParam(ID_PARAM_NAME) final String id,
+ @QueryParam(QUERY_TAGS) final String tagList,
+ @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) throws TagApiException {
+ return super.deleteTags(UUID.fromString(id), tagList,
+ context.createCallContextNoAccountId(createdBy, reason, comment, request));
+ }
+
+ @Override
+ protected ObjectType getObjectType() {
+ return ObjectType.INVOICE_ITEM;
+ }
+}
diff --git a/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestInvoiceItem.java b/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestInvoiceItem.java
new file mode 100644
index 0000000..a846d1b
--- /dev/null
+++ b/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestInvoiceItem.java
@@ -0,0 +1,141 @@
+/*
+ * Copyright 2014-2018 Groupon, Inc
+ * Copyright 2014-2018 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.jaxrs;
+
+import java.util.Collection;
+import java.util.LinkedList;
+import java.util.List;
+
+import org.killbill.billing.GuicyKillbillTestSuite;
+import org.killbill.billing.ObjectType;
+import org.killbill.billing.client.RequestOptions;
+import org.killbill.billing.client.model.Account;
+import org.killbill.billing.client.model.AuditLog;
+import org.killbill.billing.client.model.CustomField;
+import org.killbill.billing.client.model.InvoiceItem;
+import org.killbill.billing.client.model.Invoices;
+import org.killbill.billing.client.model.Tag;
+import org.killbill.billing.client.model.TagDefinition;
+import org.killbill.billing.jaxrs.resources.JaxrsResource;
+import org.killbill.billing.util.api.AuditLevel;
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import com.google.common.collect.HashMultimap;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Multimap;
+
+public class TestInvoiceItem extends TestJaxrsBase {
+
+ @Test(groups = "slow", description = "Add tags to invoice item")
+ public void testTags() throws Exception {
+ final Account accountJson = createAccountNoPMBundleAndSubscriptionAndWaitForFirstInvoice();
+ final Invoices invoicesJson = killBillClient.getInvoicesForAccount(accountJson.getAccountId(), requestOptions);
+
+ Assert.assertNotNull(invoicesJson);
+ Assert.assertEquals(invoicesJson.size(), 2);
+
+ final List<InvoiceItem> invoiceItems = invoicesJson.get(0).getItems();
+
+ Assert.assertNotNull(invoiceItems);
+
+ // Create tag definition
+ final TagDefinition input = new TagDefinition(null, false, "tagtest", "invoice item tag test", ImmutableList.<ObjectType>of(ObjectType.INVOICE_ITEM));
+
+ final TagDefinition objFromJson = killBillClient.createTagDefinition(input, requestOptions);
+ Assert.assertNotNull(objFromJson);
+ Assert.assertEquals(objFromJson.getName(), input.getName());
+ Assert.assertEquals(objFromJson.getDescription(), input.getDescription());
+
+ // Add a tag
+ final Multimap<String, String> followQueryParams = HashMultimap.create();
+ followQueryParams.put(JaxrsResource.QUERY_ACCOUNT_ID, accountJson.getAccountId().toString());
+ final RequestOptions followRequestOptions = requestOptions.extend().withQueryParamsForFollow(followQueryParams).build();
+ killBillClient.createInvoiceItemTag(invoiceItems.get(0).getInvoiceItemId(),objFromJson.getId(), followRequestOptions);
+
+ // Add account id to request
+ final Multimap<String, String> queryParams = HashMultimap.create();
+ queryParams.put(JaxrsResource.QUERY_ACCOUNT_ID, accountJson.getAccountId().toString());
+ final RequestOptions addedRequestOptions = requestOptions.extend().withQueryParams(queryParams).build();
+
+ // Retrieves all tags
+ final List<Tag> tags1 = killBillClient.getInvoiceItemTags(invoiceItems.get(0).getInvoiceItemId(), AuditLevel.FULL, addedRequestOptions);
+ Assert.assertEquals(tags1.size(), 1);
+ Assert.assertEquals(tags1.get(0).getTagDefinitionId(), objFromJson.getId());
+
+ // Verify adding the same tag a second time doesn't do anything
+ killBillClient.createInvoiceItemTag(invoiceItems.get(0).getInvoiceItemId(), objFromJson.getId(), followRequestOptions);
+
+ // Retrieves all tags again
+ final List<Tag> tags2 = killBillClient.getInvoiceItemTags(invoiceItems.get(0).getInvoiceItemId(), AuditLevel.FULL, addedRequestOptions);
+ Assert.assertEquals(tags2, tags1);
+
+ // Verify audit logs
+ Assert.assertEquals(tags2.get(0).getAuditLogs().size(), 1);
+ final AuditLog auditLogJson = tags2.get(0).getAuditLogs().get(0);
+ Assert.assertEquals(auditLogJson.getChangeType(), "INSERT");
+ Assert.assertEquals(auditLogJson.getChangedBy(), createdBy);
+ Assert.assertEquals(auditLogJson.getReasonCode(), reason);
+ Assert.assertEquals(auditLogJson.getComments(), comment);
+ Assert.assertNotNull(auditLogJson.getChangeDate());
+ Assert.assertNotNull(auditLogJson.getUserToken());
+
+ // remove it
+ killBillClient.deleteInvoiceItemTag(invoiceItems.get(0).getInvoiceItemId(), objFromJson.getId(), requestOptions);
+ final List<Tag> tags3 = killBillClient.getInvoiceItemTags(invoiceItems.get(0).getInvoiceItemId(), AuditLevel.FULL, addedRequestOptions);
+ Assert.assertEquals(tags3.size(), 0);
+
+ killBillClient.deleteTagDefinition(objFromJson.getId(),requestOptions);
+ List<TagDefinition> objsFromJson = killBillClient.getTagDefinitions(requestOptions);
+ Assert.assertNotNull(objsFromJson);
+ Boolean isFound = false;
+ for (TagDefinition tagDefinition : objsFromJson){
+ isFound |= tagDefinition.getId().equals(objFromJson.getId());
+ }
+ Assert.assertFalse(isFound);
+ }
+
+ @Test(groups = "slow", description = "Add custom fields to invoice item")
+ public void testCustomFields() throws Exception {
+ final Account accountJson = createAccountNoPMBundleAndSubscriptionAndWaitForFirstInvoice();
+ final Invoices invoicesJson = killBillClient.getInvoicesForAccount(accountJson.getAccountId(), requestOptions);
+
+ Assert.assertNotNull(invoicesJson);
+ Assert.assertEquals(invoicesJson.size(), 2);
+
+ final List<InvoiceItem> invoiceItems = invoicesJson.get(0).getItems();
+
+ Assert.assertNotNull(invoiceItems);
+
+ final Collection<CustomField> customFields = new LinkedList<CustomField>();
+ customFields.add(new CustomField(null, invoiceItems.get(0).getInvoiceItemId(), ObjectType.INVOICE_ITEM, "1", "value1", null));
+ customFields.add(new CustomField(null, invoiceItems.get(0).getInvoiceItemId(), ObjectType.INVOICE_ITEM, "2", "value2", null));
+ customFields.add(new CustomField(null, invoiceItems.get(0).getInvoiceItemId(), ObjectType.INVOICE_ITEM, "3", "value3", null));
+
+ killBillClient.createInvoiceItemCustomField(invoiceItems.get(0).getInvoiceItemId(), customFields, requestOptions);
+
+ final List<CustomField> invoiceItemCustomFields = killBillClient.getInvoiceItemCustomFields(invoiceItems.get(0).getInvoiceItemId(), requestOptions);
+ Assert.assertEquals(invoiceItemCustomFields.size(), 3);
+
+ // Delete all custom fields for account
+ killBillClient.deleteInvoiceItemCustomFields(invoiceItems.get(0).getInvoiceItemId(), requestOptions);
+
+ final List<CustomField> remainingCustomFields = killBillClient.getInvoiceItemCustomFields(invoiceItems.get(0).getInvoiceItemId(), requestOptions);
+ Assert.assertEquals(remainingCustomFields.size(), 0);
+ }
+}