Please wait. This can take some minutes ...
Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance.
Project price only 1 $
You can buy this project and download/modify it how often you want.
org.killbill.billing.jaxrs.resources.AccountResource Maven / Gradle / Ivy
/*
* Copyright 2010-2013 Ning, Inc.
* 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.math.BigDecimal;
import java.net.URI;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.stream.Collectors;
import javax.inject.Inject;
import javax.inject.Singleton;
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.Response.Status;
import javax.ws.rs.core.UriInfo;
import org.joda.time.DateTime;
import org.joda.time.LocalDate;
import org.killbill.billing.ErrorCode;
import org.killbill.billing.ObjectType;
import org.killbill.billing.OrderingType;
import org.killbill.billing.account.api.Account;
import org.killbill.billing.account.api.AccountApiException;
import org.killbill.billing.account.api.AccountData;
import org.killbill.billing.account.api.AccountEmail;
import org.killbill.billing.account.api.AccountUserApi;
import org.killbill.billing.catalog.api.BillingActionPolicy;
import org.killbill.billing.catalog.api.CatalogApiException;
import org.killbill.billing.catalog.api.Currency;
import org.killbill.billing.catalog.api.ProductCategory;
import org.killbill.billing.entitlement.api.BlockingState;
import org.killbill.billing.entitlement.api.BlockingStateType;
import org.killbill.billing.entitlement.api.Entitlement.EntitlementActionPolicy;
import org.killbill.billing.entitlement.api.EntitlementApiException;
import org.killbill.billing.entitlement.api.Subscription;
import org.killbill.billing.entitlement.api.SubscriptionApi;
import org.killbill.billing.entitlement.api.SubscriptionApiException;
import org.killbill.billing.entitlement.api.SubscriptionBundle;
import org.killbill.billing.invoice.api.Invoice;
import org.killbill.billing.invoice.api.InvoiceApiException;
import org.killbill.billing.invoice.api.InvoiceItem;
import org.killbill.billing.invoice.api.InvoiceItemType;
import org.killbill.billing.invoice.api.InvoicePayment;
import org.killbill.billing.invoice.api.InvoiceUserApi;
import org.killbill.billing.jaxrs.JaxrsExecutors;
import org.killbill.billing.jaxrs.json.AccountEmailJson;
import org.killbill.billing.jaxrs.json.AccountJson;
import org.killbill.billing.jaxrs.json.AccountTimelineJson;
import org.killbill.billing.jaxrs.json.AuditLogJson;
import org.killbill.billing.jaxrs.json.BlockingStateJson;
import org.killbill.billing.jaxrs.json.BundleJson;
import org.killbill.billing.jaxrs.json.CustomFieldJson;
import org.killbill.billing.jaxrs.json.InvoiceJson;
import org.killbill.billing.jaxrs.json.InvoicePaymentJson;
import org.killbill.billing.jaxrs.json.OverdueStateJson;
import org.killbill.billing.jaxrs.json.PaymentJson;
import org.killbill.billing.jaxrs.json.PaymentMethodJson;
import org.killbill.billing.jaxrs.json.PaymentTransactionJson;
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.overdue.api.OverdueApi;
import org.killbill.billing.overdue.api.OverdueApiException;
import org.killbill.billing.overdue.api.OverdueState;
import org.killbill.billing.overdue.config.api.OverdueException;
import org.killbill.billing.payment.api.InvoicePaymentApi;
import org.killbill.billing.payment.api.Payment;
import org.killbill.billing.payment.api.PaymentApi;
import org.killbill.billing.payment.api.PaymentApiException;
import org.killbill.billing.payment.api.PaymentMethod;
import org.killbill.billing.payment.api.PaymentOptions;
import org.killbill.billing.payment.api.PaymentTransaction;
import org.killbill.billing.payment.api.PluginProperty;
import org.killbill.billing.payment.api.TransactionStatus;
import org.killbill.billing.payment.api.TransactionType;
import org.killbill.commons.utils.Preconditions;
import org.killbill.billing.util.UUIDs;
import org.killbill.billing.util.api.AuditLevel;
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.RecordIdApi;
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.audit.AccountAuditLogs;
import org.killbill.billing.util.audit.AuditLogWithHistory;
import org.killbill.billing.util.callcontext.CallContext;
import org.killbill.billing.util.callcontext.TenantContext;
import org.killbill.commons.utils.collect.Iterables;
import org.killbill.billing.util.config.definition.JaxrsConfig;
import org.killbill.billing.util.customfield.CustomField;
import org.killbill.billing.util.entity.Pagination;
import org.killbill.billing.util.tag.ControlTagType;
import org.killbill.billing.util.tag.Tag;
import org.killbill.clock.Clock;
import org.killbill.commons.metrics.api.annotation.MetricTag;
import org.killbill.commons.metrics.api.annotation.TimedResource;
import org.killbill.notificationq.api.NotificationQueue;
import org.killbill.notificationq.api.NotificationQueueService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import io.swagger.annotations.ApiResponse;
import io.swagger.annotations.ApiResponses;
import static javax.ws.rs.core.MediaType.APPLICATION_JSON;
@Singleton
@Path(JaxrsResource.ACCOUNTS_PATH)
@Api(value = JaxrsResource.ACCOUNTS_PATH, description = "Operations on accounts", tags = "Account")
public class AccountResource extends JaxRsResourceBase {
private static final String ID_PARAM_NAME = "accountId";
private final SubscriptionApi subscriptionApi;
private final InvoiceUserApi invoiceApi;
private final InvoicePaymentApi invoicePaymentApi;
private final OverdueApi overdueApi;
private final JaxrsExecutors jaxrsExecutors;
private final JaxrsConfig jaxrsConfig;
private final RecordIdApi recordIdApi;
private final NotificationQueueService notificationQueueService;
@Inject
public AccountResource(final JaxrsUriBuilder uriBuilder,
final AccountUserApi accountApi,
final InvoiceUserApi invoiceApi,
final InvoicePaymentApi invoicePaymentApi,
final PaymentApi paymentApi,
final TagUserApi tagUserApi,
final AuditUserApi auditUserApi,
final CustomFieldUserApi customFieldUserApi,
final SubscriptionApi subscriptionApi,
final OverdueApi overdueApi,
final Clock clock,
final JaxrsExecutors jaxrsExecutors,
final JaxrsConfig jaxrsConfig,
final Context context,
final RecordIdApi recordIdApi,
final NotificationQueueService notificationQueueService) {
super(uriBuilder, tagUserApi, customFieldUserApi, auditUserApi, accountApi, paymentApi, invoicePaymentApi, subscriptionApi, clock, context);
this.subscriptionApi = subscriptionApi;
this.invoiceApi = invoiceApi;
this.invoicePaymentApi = invoicePaymentApi;
this.overdueApi = overdueApi;
this.jaxrsExecutors = jaxrsExecutors;
this.jaxrsConfig = jaxrsConfig;
this.recordIdApi = recordIdApi;
this.notificationQueueService = notificationQueueService;
}
@TimedResource
@GET
@Path("/{accountId:" + UUID_PATTERN + "}")
@Produces(APPLICATION_JSON)
@ApiOperation(value = "Retrieve an account by id", response = AccountJson.class)
@ApiResponses(value = {@ApiResponse(code = 400, message = "Invalid account id supplied"),
@ApiResponse(code = 404, message = "Account not found")})
public Response getAccount(@PathParam("accountId") final UUID accountId,
@QueryParam(QUERY_ACCOUNT_WITH_BALANCE) @DefaultValue("false") final Boolean accountWithBalance,
@QueryParam(QUERY_ACCOUNT_WITH_BALANCE_AND_CBA) @DefaultValue("false") final Boolean accountWithBalanceAndCBA,
@QueryParam(QUERY_AUDIT) @DefaultValue("NONE") final AuditMode auditMode,
@javax.ws.rs.core.Context final HttpServletRequest request) throws AccountApiException {
final TenantContext tenantContext = context.createTenantContextWithAccountId(accountId, request);
final Account account = accountUserApi.getAccountById(accountId, tenantContext);
final AccountAuditLogs accountAuditLogs = auditUserApi.getAccountAuditLogs(account.getId(), auditMode.getLevel(), tenantContext);
final AccountJson accountJson = getAccount(account, accountWithBalance, accountWithBalanceAndCBA, accountAuditLogs, tenantContext);
return Response.status(Status.OK).entity(accountJson).build();
}
@TimedResource
@GET
@Path("/" + PAGINATION)
@Produces(APPLICATION_JSON)
@ApiOperation(value = "List accounts", response = AccountJson.class, responseContainer = "List")
@ApiResponses(value = {})
public Response getAccounts(@QueryParam(QUERY_SEARCH_OFFSET) @DefaultValue("0") final Long offset,
@QueryParam(QUERY_SEARCH_LIMIT) @DefaultValue("100") final Long limit,
@QueryParam(QUERY_ACCOUNT_WITH_BALANCE) @DefaultValue("false") final Boolean accountWithBalance,
@QueryParam(QUERY_ACCOUNT_WITH_BALANCE_AND_CBA) @DefaultValue("false") final Boolean accountWithBalanceAndCBA,
@QueryParam(QUERY_AUDIT) @DefaultValue("NONE") final AuditMode auditMode,
@javax.ws.rs.core.Context final HttpServletRequest request) throws AccountApiException {
final TenantContext tenantContext = context.createTenantContextNoAccountId(request);
final Pagination accounts = accountUserApi.getAccounts(offset, limit, tenantContext);
final URI nextPageUri = uriBuilder.nextPage(AccountResource.class,
"getAccounts",
accounts.getNextOffset(),
limit,
Map.of(QUERY_ACCOUNT_WITH_BALANCE, accountWithBalance.toString(),
QUERY_ACCOUNT_WITH_BALANCE_AND_CBA, accountWithBalanceAndCBA.toString(),
QUERY_AUDIT, auditMode.getLevel().toString()),
Collections.emptyMap());
return buildStreamingPaginationResponse(accounts,
account -> {
final AccountAuditLogs accountAuditLogs = auditUserApi.getAccountAuditLogs(account.getId(), auditMode.getLevel(), tenantContext);
return getAccount(account, accountWithBalance, accountWithBalanceAndCBA, accountAuditLogs, tenantContext);
},
nextPageUri
);
}
@TimedResource
@GET
@Path("/" + SEARCH + "/{searchKey:" + ANYTHING_PATTERN + "}")
@Produces(APPLICATION_JSON)
@ApiOperation(value = "Search accounts", response = AccountJson.class, responseContainer = "List")
@ApiResponses(value = {})
public Response searchAccounts(@PathParam("searchKey") final String searchKey,
@QueryParam(QUERY_SEARCH_OFFSET) @DefaultValue("0") final Long offset,
@QueryParam(QUERY_SEARCH_LIMIT) @DefaultValue("100") final Long limit,
@QueryParam(QUERY_ACCOUNT_WITH_BALANCE) @DefaultValue("false") final Boolean accountWithBalance,
@QueryParam(QUERY_ACCOUNT_WITH_BALANCE_AND_CBA) @DefaultValue("false") final Boolean accountWithBalanceAndCBA,
@QueryParam(QUERY_AUDIT) @DefaultValue("NONE") final AuditMode auditMode,
@javax.ws.rs.core.Context final HttpServletRequest request) throws AccountApiException {
final TenantContext tenantContext = context.createTenantContextNoAccountId(request);
final Pagination accounts = accountUserApi.searchAccounts(searchKey, offset, limit, tenantContext);
final URI nextPageUri = uriBuilder.nextPage(AccountResource.class,
"searchAccounts",
accounts.getNextOffset(),
limit,
Map.of(QUERY_ACCOUNT_WITH_BALANCE, accountWithBalance.toString(),
QUERY_ACCOUNT_WITH_BALANCE_AND_CBA, accountWithBalanceAndCBA.toString(),
QUERY_AUDIT, auditMode.getLevel().toString()),
Map.of("searchKey", searchKey));
return buildStreamingPaginationResponse(accounts,
account -> {
final AccountAuditLogs accountAuditLogs = auditUserApi.getAccountAuditLogs(account.getId(), auditMode.getLevel(), tenantContext);
return getAccount(account, accountWithBalance, accountWithBalanceAndCBA, accountAuditLogs, tenantContext);
},
nextPageUri
);
}
@TimedResource
@GET
@Path("/{accountId:" + UUID_PATTERN + "}/" + BUNDLES)
@Produces(APPLICATION_JSON)
@ApiOperation(value = "Retrieve bundles for account", response = BundleJson.class, responseContainer = "List")
@ApiResponses(value = {@ApiResponse(code = 400, message = "Invalid account id supplied"),
@ApiResponse(code = 404, message = "Account not found")})
public Response getAccountBundles(@PathParam("accountId") final UUID accountId,
@QueryParam(QUERY_EXTERNAL_KEY) final String externalKey,
@QueryParam(QUERY_BUNDLES_FILTER) final String bundlesFilter,
@QueryParam(QUERY_AUDIT) @DefaultValue("NONE") final AuditMode auditMode,
@javax.ws.rs.core.Context final HttpServletRequest request) throws AccountApiException, SubscriptionApiException, CatalogApiException {
final TenantContext tenantContext = context.createTenantContextWithAccountId(accountId, request);
final Account account = accountUserApi.getAccountById(accountId, tenantContext);
final List bundles = (externalKey != null) ?
subscriptionApi.getSubscriptionBundlesForAccountIdAndExternalKey(accountId, externalKey, tenantContext) :
subscriptionApi.getSubscriptionBundlesForAccountId(accountId, tenantContext);
final AccountAuditLogs accountAuditLogs = auditUserApi.getAccountAuditLogs(account.getId(), auditMode.getLevel(), tenantContext);
final boolean filter = (null != bundlesFilter && !bundlesFilter.isEmpty());
final List subscriptionBundles = filter ? filterBundles(bundles, Arrays.asList(bundlesFilter.split(","))) : bundles;
final Collection result = new LinkedList();
for (final SubscriptionBundle subscriptionBundle : subscriptionBundles) {
result.add(new BundleJson(subscriptionBundle, account.getCurrency(), accountAuditLogs));
}
return Response.status(Status.OK).entity(result).build();
}
@TimedResource
@GET
@Path("/{accountId:" + UUID_PATTERN + "}/" + BUNDLES + "/" + PAGINATION)
@Produces(APPLICATION_JSON)
@ApiOperation(value = "Retrieve paginated bundles for account", response = BundleJson.class, responseContainer = "List")
@ApiResponses(value = {@ApiResponse(code = 400, message = "Invalid account id supplied"),
@ApiResponse(code = 404, message = "Account not found")})
public Response getAccountBundlesPaginated(@PathParam("accountId") final UUID accountId,
@QueryParam(QUERY_SEARCH_OFFSET) @DefaultValue("0") final Long offset,
@QueryParam(QUERY_SEARCH_LIMIT) @DefaultValue("100") final Long limit,
@QueryParam(QUERY_AUDIT) @DefaultValue("NONE") final AuditMode auditMode,
@javax.ws.rs.core.Context final HttpServletRequest request) throws AccountApiException, SubscriptionApiException, CatalogApiException {
final TenantContext tenantContext = context.createTenantContextWithAccountId(accountId, request);
final Pagination bundles = subscriptionApi.getSubscriptionBundlesForAccountId(accountId, offset, limit, tenantContext);
final Map queryParams = new HashMap<>();
if (auditMode != null && auditMode.getLevel() != null) {
queryParams.put(QUERY_AUDIT, auditMode.getLevel().toString());
}
final URI nextPageUri = uriBuilder.nextPage(AccountResource.class,
"getAccountBundlesPaginated",
bundles.getNextOffset(),
limit,
queryParams,
Map.of("accountId", String.valueOf(accountId)));
return buildStreamingPaginationResponse(bundles,
bundle -> {
final AccountAuditLogs accountAuditLogs = auditUserApi.getAccountAuditLogs(accountId, auditMode.getLevel(), tenantContext);
try {
return new BundleJson(bundle, null, accountAuditLogs);
} catch (final CatalogApiException e) {
throw new RuntimeException(e);
}
},
nextPageUri
);
}
private List filterBundles(final List subscriptionBundlesForAccountId, final List bundlesFilter) {
List result = new ArrayList();
for (SubscriptionBundle subscriptionBundle : subscriptionBundlesForAccountId) {
if (bundlesFilter.contains(subscriptionBundle.getId().toString())) {
result.add(subscriptionBundle);
}
}
return result;
}
@TimedResource
@GET
@Produces(APPLICATION_JSON)
@ApiOperation(value = "Retrieve an account by external key", response = AccountJson.class)
@ApiResponses(value = {@ApiResponse(code = 404, message = "Account not found")})
public Response getAccountByKey(@ApiParam(required = true) @QueryParam(QUERY_EXTERNAL_KEY) final String externalKey,
@QueryParam(QUERY_ACCOUNT_WITH_BALANCE) @DefaultValue("false") final Boolean accountWithBalance,
@QueryParam(QUERY_ACCOUNT_WITH_BALANCE_AND_CBA) @DefaultValue("false") final Boolean accountWithBalanceAndCBA,
@QueryParam(QUERY_AUDIT) @DefaultValue("NONE") final AuditMode auditMode,
@javax.ws.rs.core.Context final HttpServletRequest request) throws AccountApiException {
final TenantContext tenantContext = context.createTenantContextNoAccountId(request);
final Account account = accountUserApi.getAccountByKey(externalKey, tenantContext);
final AccountAuditLogs accountAuditLogs = auditUserApi.getAccountAuditLogs(account.getId(), auditMode.getLevel(), tenantContext);
final AccountJson accountJson = getAccount(account, accountWithBalance, accountWithBalanceAndCBA, accountAuditLogs, tenantContext);
return Response.status(Status.OK).entity(accountJson).build();
}
private AccountJson getAccount(final Account account, final Boolean accountWithBalance, final Boolean accountWithBalanceAndCBA,
final AccountAuditLogs auditLogs, final TenantContext tenantContext) {
if (accountWithBalanceAndCBA) {
final BigDecimal accountBalance = invoiceApi.getAccountBalance(account.getId(), tenantContext);
final BigDecimal accountCBA = invoiceApi.getAccountCBA(account.getId(), tenantContext);
return new AccountJson(account, accountBalance, accountCBA, auditLogs);
} else if (accountWithBalance) {
final BigDecimal accountBalance = invoiceApi.getAccountBalance(account.getId(), tenantContext);
return new AccountJson(account, accountBalance, null, auditLogs);
} else {
return new AccountJson(account, null, null, auditLogs);
}
}
@TimedResource
@POST
@Consumes(APPLICATION_JSON)
@Produces(APPLICATION_JSON)
@ApiOperation(value = "Create account", response = AccountJson.class)
@ApiResponses(value = {@ApiResponse(code = 201, message = "Account created successfully"),
@ApiResponse(code = 400, message = "Invalid account data supplied")})
public Response createAccount(final AccountJson json,
@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 AccountApiException {
verifyNonNullOrEmpty(json, "AccountJson body should be specified");
final AccountData data = json.toAccount(null);
final Account account = accountUserApi.createAccount(data, context.createCallContextNoAccountId(createdBy, reason, comment, request));
return uriBuilder.buildResponse(uriInfo, AccountResource.class, "getAccount", account.getId(), request);
}
@TimedResource
@PUT
@Consumes(APPLICATION_JSON)
@Produces(APPLICATION_JSON)
@Path("/{accountId:" + UUID_PATTERN + "}")
@ApiOperation(value = "Update account")
@ApiResponses(value = {@ApiResponse(code = 204, message = "Successful operation"),
@ApiResponse(code = 400, message = "Invalid account data supplied")})
public Response updateAccount(@PathParam("accountId") final UUID accountId,
final AccountJson json,
@QueryParam(QUERY_ACCOUNT_TREAT_NULL_AS_RESET) @DefaultValue("false") final Boolean treatNullValueAsReset,
@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 AccountApiException {
verifyNonNullOrEmpty(json, "AccountJson body should be specified");
final Account data = json.toAccount(accountId);
if (treatNullValueAsReset) {
accountUserApi.updateAccount(data, context.createCallContextWithAccountId(accountId, createdBy, reason, comment, request));
} else {
accountUserApi.updateAccount(accountId, data, context.createCallContextWithAccountId(accountId, createdBy, reason, comment, request));
}
return Response.status(Status.NO_CONTENT).build();
}
@TimedResource
@DELETE
@Path("/{accountId:" + UUID_PATTERN + "}")
@Produces(APPLICATION_JSON)
@ApiOperation(value = "Close account")
@ApiResponses(value = {@ApiResponse(code = 204, message = "Successful operation"),
@ApiResponse(code = 400, message = "Invalid account id supplied")})
public Response closeAccount(@PathParam("accountId") final UUID accountId,
@QueryParam(QUERY_CANCEL_ALL_SUBSCRIPTIONS) @DefaultValue("false") final Boolean cancelAllSubscriptions,
@QueryParam(QUERY_WRITE_OFF_UNPAID_INVOICES) @DefaultValue("false") final Boolean writeOffUnpaidInvoices,
@QueryParam(QUERY_ITEM_ADJUST_UNPAID_INVOICES) @DefaultValue("false") final Boolean itemAdjustUnpaidInvoices,
@QueryParam(QUERY_REMOVE_FUTURE_NOTIFICATIONS) @DefaultValue("true") final Boolean removeFutureNotifications,
@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 SubscriptionApiException, AccountApiException, EntitlementApiException, InvoiceApiException, TagApiException {
final CallContext callContext = context.createCallContextWithAccountId(accountId, createdBy, reason, comment, request);
tagUserApi.addTag(accountId, ObjectType.ACCOUNT, ControlTagType.AUTO_INVOICING_OFF.getId(), callContext);
if (cancelAllSubscriptions) {
final List bundles = subscriptionApi.getSubscriptionBundlesForAccountId(accountId, callContext);
final Iterable toBeCancelled = bundles.stream()
.map(SubscriptionBundle::getSubscriptions)
.flatMap(Collection::stream)
.filter(input -> input.getLastActiveProductCategory() != ProductCategory.ADD_ON &&
input.getBillingEndDate() == null)
.collect(Collectors.toUnmodifiableList());
for (final Subscription cur : toBeCancelled) {
cur.cancelEntitlementWithPolicyOverrideBillingPolicy(EntitlementActionPolicy.IMMEDIATE, BillingActionPolicy.END_OF_TERM, Collections.emptyList(), callContext);
}
}
final Collection unpaidInvoices = writeOffUnpaidInvoices || itemAdjustUnpaidInvoices ? invoiceApi.getUnpaidInvoicesByAccountId(accountId, null, null, callContext) : Collections.emptyList();
if (writeOffUnpaidInvoices) {
for (final Invoice cur : unpaidInvoices) {
invoiceApi.tagInvoiceAsWrittenOff(cur.getId(), callContext);
}
} else if (itemAdjustUnpaidInvoices) {
final List ADJUSTABLE_TYPES = List.of(InvoiceItemType.EXTERNAL_CHARGE,
InvoiceItemType.FIXED,
InvoiceItemType.RECURRING,
InvoiceItemType.TAX,
InvoiceItemType.USAGE,
InvoiceItemType.PARENT_SUMMARY);
final String description = comment != null ? comment : "Close Account";
for (final Invoice invoice : unpaidInvoices) {
for (final InvoiceItem item : invoice.getInvoiceItems()) {
if (ADJUSTABLE_TYPES.contains(item.getInvoiceItemType())) {
invoiceApi.insertInvoiceItemAdjustment(accountId, invoice.getId(), item.getId(), clock.getUTCToday(), description, null, null, callContext);
}
}
}
}
final BlockingStateJson blockingState = new BlockingStateJson(accountId, "CLOSE_ACCOUNT", "account-service", true, false, false, null, BlockingStateType.ACCOUNT, null);
addBlockingState(blockingState, accountId, accountId, BlockingStateType.ACCOUNT, null, Collections.emptyList(), createdBy, reason, comment, request, null);
if (removeFutureNotifications) {
final Long tenantRecordId = recordIdApi.getRecordId(callContext.getTenantId(), ObjectType.TENANT, callContext);
final Long accountRecordId = accountId == null ? null : recordIdApi.getRecordId(accountId, ObjectType.ACCOUNT, callContext);
for (final NotificationQueue notificationQueue : notificationQueueService.getNotificationQueues()) {
log.debug("Removing future notifications for queueName={}", notificationQueue.getFullQName());
notificationQueue.removeFutureNotificationsForSearchKeys(accountRecordId, tenantRecordId);
}
}
return Response.status(Status.NO_CONTENT).build();
}
@TimedResource
@GET
@Path("/{accountId:" + UUID_PATTERN + "}/" + TIMELINE)
@Produces(APPLICATION_JSON)
@ApiOperation(value = "Retrieve account timeline", response = AccountTimelineJson.class)
@ApiResponses(value = {@ApiResponse(code = 400, message = "Invalid account id supplied"),
@ApiResponse(code = 404, message = "Account not found")})
public Response getAccountTimeline(@PathParam("accountId") final UUID accountId,
@QueryParam(QUERY_PARALLEL) @DefaultValue("false") final Boolean parallel,
@QueryParam(QUERY_AUDIT) @DefaultValue("NONE") final AuditMode auditMode,
@javax.ws.rs.core.Context final HttpServletRequest request) throws AccountApiException, PaymentApiException, SubscriptionApiException, InvoiceApiException, CatalogApiException {
final TenantContext tenantContext = context.createTenantContextWithAccountId(accountId, request);
final Account account = accountUserApi.getAccountById(accountId, tenantContext);
final Callable> bundlesCallable = new Callable>() {
@Override
public List call() throws Exception {
return subscriptionApi.getSubscriptionBundlesForAccountId(accountId, tenantContext);
}
};
final Callable> invoicesCallable = new Callable>() {
@Override
public List call() throws Exception {
return invoiceApi.getInvoicesByAccount(accountId, false, false, true, tenantContext);
}
};
final Callable> invoicePaymentsCallable = new Callable>() {
@Override
public List call() throws Exception {
return invoicePaymentApi.getInvoicePaymentsByAccount(accountId, tenantContext);
}
};
final Callable> paymentsCallable = new Callable>() {
@Override
public List call() throws Exception {
return paymentApi.getAccountPayments(accountId, false, false, Collections.emptyList(), tenantContext);
}
};
final Callable auditsCallable = new Callable() {
@Override
public AccountAuditLogs call() throws Exception {
return auditUserApi.getAccountAuditLogs(accountId, auditMode.getLevel(), tenantContext);
}
};
final AccountTimelineJson json;
List invoices = null;
List bundles = null;
List invoicePayments = null;
List payments = null;
AccountAuditLogs accountAuditLogs = null;
if (parallel) {
final ExecutorService executor = jaxrsExecutors.getJaxrsExecutorService();
final Future> futureBundlesCallable = executor.submit(bundlesCallable);
final Future> futureInvoicesCallable = executor.submit(invoicesCallable);
final Future> futureInvoicePaymentsCallable = executor.submit(invoicePaymentsCallable);
final Future> futurePaymentsCallable = executor.submit(paymentsCallable);
final Future futureAuditsCallable = executor.submit(auditsCallable);
final List toBeCancelled = List.of(futureBundlesCallable, futureInvoicesCallable, futureInvoicePaymentsCallable, futurePaymentsCallable, futureAuditsCallable);
final int timeoutMsec = 100;
final long ini = System.currentTimeMillis();
do {
bundles = (bundles == null) ? waitOnFutureAndHandleTimeout("bundles", futureBundlesCallable, timeoutMsec, toBeCancelled) : bundles;
invoices = (invoices == null) ? waitOnFutureAndHandleTimeout("invoices", futureInvoicesCallable, timeoutMsec, toBeCancelled) : invoices;
invoicePayments = (invoicePayments == null) ? waitOnFutureAndHandleTimeout("invoicePayments", futureInvoicePaymentsCallable, timeoutMsec, toBeCancelled) : invoicePayments;
payments = (payments == null) ? waitOnFutureAndHandleTimeout("payments", futurePaymentsCallable, timeoutMsec, toBeCancelled) : payments;
accountAuditLogs = (accountAuditLogs == null) ? waitOnFutureAndHandleTimeout("accountAuditLogs", futureAuditsCallable, timeoutMsec, toBeCancelled) : accountAuditLogs;
} while ((System.currentTimeMillis() - ini < jaxrsConfig.getJaxrsTimeout().getMillis()) &&
(bundles == null || invoices == null || invoicePayments == null || payments == null || accountAuditLogs == null));
if (bundles == null || invoices == null || invoicePayments == null || payments == null || accountAuditLogs == null) {
Response.status(Status.SERVICE_UNAVAILABLE).build();
}
} else {
invoices = runCallable("invoices", invoicesCallable);
payments = runCallable("payments", paymentsCallable);
bundles = runCallable("bundles", bundlesCallable);
accountAuditLogs = runCallable("accountAuditLogs", auditsCallable);
invoicePayments = runCallable("invoicePayments", invoicePaymentsCallable);
}
json = new AccountTimelineJson(account, invoices, payments, invoicePayments, bundles, accountAuditLogs);
return Response.status(Status.OK).entity(json).build();
}
private T waitOnFutureAndHandleTimeout(final String logSuffix, final Future future, final long timeoutMsec, final Iterable toBeCancelled) throws PaymentApiException, AccountApiException, InvoiceApiException, SubscriptionApiException {
try {
return waitOnFutureAndHandleTimeout(future, timeoutMsec);
} catch (final InterruptedException e) {
log.warn("InterruptedException while retrieving {}", logSuffix, e);
handleCallableException(e, toBeCancelled);
} catch (final ExecutionException e) {
log.warn("ExecutionException while retrieving {}", logSuffix, e);
handleCallableException(e.getCause(), toBeCancelled);
}
// Never reached
return null;
}
private T waitOnFutureAndHandleTimeout(final Future future, final long timeoutMsec) throws ExecutionException, InterruptedException {
try {
return future.get(timeoutMsec, TimeUnit.MILLISECONDS);
} catch (final TimeoutException e) {
return null;
}
}
private T runCallable(final String logSuffix, final Callable callable) throws PaymentApiException, AccountApiException, InvoiceApiException, SubscriptionApiException {
try {
return callable.call();
} catch (final Exception e) {
log.warn("InterruptedException while retrieving {}", logSuffix, e);
handleCallableException(e);
}
// Never reached
return null;
}
private void handleCallableException(final Throwable causeOrException, final Iterable toBeCancelled) throws AccountApiException, SubscriptionApiException, PaymentApiException, InvoiceApiException {
for (final Future f : toBeCancelled) {
f.cancel(true);
}
handleCallableException(causeOrException);
}
private void handleCallableException(final Throwable causeOrException) throws AccountApiException, SubscriptionApiException, PaymentApiException, InvoiceApiException {
if (causeOrException instanceof AccountApiException) {
throw (AccountApiException) causeOrException;
} else if (causeOrException instanceof SubscriptionApiException) {
throw (SubscriptionApiException) causeOrException;
} else if (causeOrException instanceof InvoiceApiException) {
throw (InvoiceApiException) causeOrException;
} else if (causeOrException instanceof PaymentApiException) {
throw (PaymentApiException) causeOrException;
} else {
if (causeOrException instanceof InterruptedException) {
Thread.currentThread().interrupt();
}
throw new RuntimeException(causeOrException.getMessage(), causeOrException);
}
}
/*
* ************************** INVOICE CBA REBALANCING ********************************
*/
@TimedResource
@PUT
@Path("/{accountId:" + UUID_PATTERN + "}/" + CBA_REBALANCING)
@Consumes(APPLICATION_JSON)
@Produces(APPLICATION_JSON)
@ApiOperation(value = "Rebalance account CBA")
@ApiResponses(value = {@ApiResponse(code = 204, message = "Successful operation"),
@ApiResponse(code = 400, message = "Invalid account id supplied")})
public Response rebalanceExistingCBAOnAccount(@PathParam("accountId") final UUID accountId,
@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 AccountApiException {
final CallContext callContext = context.createCallContextWithAccountId(accountId, createdBy, reason, comment, request);
invoiceApi.consumeExistingCBAOnAccountWithUnpaidInvoices(accountId, callContext);
return Response.status(Status.NO_CONTENT).build();
}
/*
* ************************** INVOICES ********************************
*/
@TimedResource
@GET
@Path("/{accountId:" + UUID_PATTERN + "}/" + INVOICES)
@Produces(APPLICATION_JSON)
@ApiOperation(value = "Retrieve account invoices", response = InvoiceJson.class, responseContainer = "List")
@ApiResponses(value = {@ApiResponse(code = 400, message = "Invalid account id supplied"),
@ApiResponse(code = 404, message = "Account not found")})
public Response getInvoicesForAccount(@PathParam("accountId") final UUID accountId,
@QueryParam(QUERY_START_DATE) final String startDateStr,
@QueryParam(QUERY_END_DATE) final String endDateStr,
@QueryParam(QUERY_WITH_MIGRATION_INVOICES) @DefaultValue("false") final boolean withMigrationInvoices,
@QueryParam(QUERY_UNPAID_INVOICES_ONLY) @DefaultValue("false") final boolean unpaidInvoicesOnly,
@QueryParam(QUERY_INCLUDE_VOIDED_INVOICES) @DefaultValue("false") final boolean includeVoidedInvoices,
@QueryParam(QUERY_INCLUDE_INVOICE_COMPONENTS) @DefaultValue("false") final boolean includeInvoiceComponents,
@QueryParam(QUERY_INVOICES_FILTER) final String invoicesFilter,
@QueryParam(QUERY_AUDIT) @DefaultValue("NONE") final AuditMode auditMode,
@javax.ws.rs.core.Context final HttpServletRequest request) throws AccountApiException {
Preconditions.checkState(!unpaidInvoicesOnly || !withMigrationInvoices, "We don't support fetching unpaid invoices incl. migration");
Preconditions.checkState(startDateStr == null || !withMigrationInvoices, "We don't support fetching migration invoices and specifying a start date");
Preconditions.checkState(!unpaidInvoicesOnly || !includeInvoiceComponents, "We don't support fetching unpaid invoices without invoice components");
final TenantContext tenantContext = context.createTenantContextWithAccountId(accountId, request);
final LocalDate startDate = startDateStr != null ? LOCAL_DATE_FORMATTER.parseLocalDate(startDateStr) : null;
final LocalDate endDate = endDateStr != null ? LOCAL_DATE_FORMATTER.parseLocalDate(endDateStr) : null;
final List invoices;
if (unpaidInvoicesOnly) {
invoices = new ArrayList(invoiceApi.getUnpaidInvoicesByAccountId(accountId, startDate, endDate, tenantContext));
} else {
invoices = startDate != null || endDate != null ?
invoiceApi.getInvoicesByAccount(accountId, startDate, endDate, includeVoidedInvoices, includeInvoiceComponents, tenantContext) :
invoiceApi.getInvoicesByAccount(accountId, withMigrationInvoices, includeVoidedInvoices, includeInvoiceComponents, tenantContext);
}
final AccountAuditLogs accountAuditLogs = auditUserApi.getAccountAuditLogs(accountId, auditMode.getLevel(), tenantContext);
// The filter, if any comes in addition to other param to limit the response
final Set filterInvoiceIds = (null != invoicesFilter && !invoicesFilter.isEmpty()) ? Set.of(invoicesFilter.split(",")) : Collections.emptySet();
final List result = new LinkedList<>();
for (final Invoice invoice : invoices) {
if (filterInvoiceIds.isEmpty() || filterInvoiceIds.contains(invoice.getId().toString())) {
result.add(new InvoiceJson(invoice, null, accountAuditLogs));
}
}
return Response.status(Status.OK).entity(result).build();
}
@TimedResource
@GET
@Path("/{accountId:" + UUID_PATTERN + "}/" + INVOICES + "/" + PAGINATION)
@Produces(APPLICATION_JSON)
@ApiOperation(value = "Retrieve paginated invoices for account", response = InvoiceJson.class, responseContainer = "List")
@ApiResponses(value = {@ApiResponse(code = 400, message = "Invalid account id supplied"),
@ApiResponse(code = 404, message = "Account not found")})
public Response getInvoicesForAccountPaginated(@PathParam("accountId") final UUID accountId,
@QueryParam(QUERY_SEARCH_OFFSET) @DefaultValue("0") final Long offset,
@QueryParam(QUERY_SEARCH_LIMIT) @DefaultValue("100") final Long limit,
@QueryParam(QUERY_AUDIT) @DefaultValue("NONE") final AuditMode auditMode,
@javax.ws.rs.core.Context final HttpServletRequest request) throws AccountApiException {
final TenantContext tenantContext = context.createTenantContextWithAccountId(accountId, request);
final Pagination invoices = invoiceApi.getInvoicesByAccount(accountId, offset, limit, tenantContext);
final Map queryParams = new HashMap<>();
if (auditMode != null && auditMode.getLevel() != null) {
queryParams.put(QUERY_AUDIT, auditMode.getLevel().toString());
}
final URI nextPageUri = uriBuilder.nextPage(AccountResource.class,
"getInvoicesForAccountPaginated",
invoices.getNextOffset(),
limit,
queryParams,
Map.of("accountId", String.valueOf(accountId)));
return buildStreamingPaginationResponse(invoices,
invoice -> {
final AccountAuditLogs accountAuditLogs = auditUserApi.getAccountAuditLogs(accountId, auditMode.getLevel(), tenantContext);
return new InvoiceJson(invoice, null, accountAuditLogs);
},
nextPageUri
);
}
/*
* ************************** PAYMENTS ********************************
*/
// STEPH should refactor code since very similar to @Path("/{accountId:" + UUID_PATTERN + "}/" + PAYMENTS)
@TimedResource
@GET
@Path("/{accountId:" + UUID_PATTERN + "}/" + INVOICE_PAYMENTS)
@Produces(APPLICATION_JSON)
@ApiOperation(value = "Retrieve account invoice payments", response = InvoicePaymentJson.class, responseContainer = "List")
@ApiResponses(value = {@ApiResponse(code = 400, message = "Invalid account id supplied"),
@ApiResponse(code = 404, message = "Account not found")})
public Response getInvoicePayments(@PathParam("accountId") final UUID accountId,
@QueryParam(QUERY_WITH_PLUGIN_INFO) @DefaultValue("false") final Boolean withPluginInfo,
@QueryParam(QUERY_WITH_ATTEMPTS) @DefaultValue("false") final Boolean withAttempts,
@QueryParam(QUERY_PLUGIN_PROPERTY) final List pluginPropertiesString,
@QueryParam(QUERY_AUDIT) @DefaultValue("NONE") final AuditMode auditMode,
@javax.ws.rs.core.Context final HttpServletRequest request) throws PaymentApiException, AccountApiException {
final Iterable pluginProperties = extractPluginProperties(pluginPropertiesString);
final TenantContext tenantContext = context.createTenantContextWithAccountId(accountId, request);
final Account account = accountUserApi.getAccountById(accountId, tenantContext);
final List payments = paymentApi.getAccountPayments(account.getId(), withPluginInfo, withAttempts, pluginProperties, tenantContext);
final List invoicePayments = invoicePaymentApi.getInvoicePaymentsByAccount(accountId, tenantContext);
final AccountAuditLogs accountAuditLogs = auditUserApi.getAccountAuditLogs(accountId, auditMode.getLevel(), tenantContext);
final List result = new ArrayList(payments.size());
for (final Payment payment : payments) {
final UUID invoiceId = getInvoiceId(invoicePayments, payment);
result.add(new InvoicePaymentJson(payment, invoiceId, accountAuditLogs));
}
return Response.status(Status.OK).entity(result).build();
}
@TimedResource
@POST
@Produces(APPLICATION_JSON)
@Consumes(APPLICATION_JSON)
@Path("/{accountId:" + UUID_PATTERN + "}/" + INVOICE_PAYMENTS)
@ApiOperation(value = "Trigger a payment for all unpaid invoices", response = InvoiceJson.class, responseContainer = "List")
@ApiResponses(value = {@ApiResponse(code = 201, message = "Successful operation"),
@ApiResponse(code = 204, message = "Nothing to pay"),
@ApiResponse(code = 404, message = "Invalid account id supplied")})
public Response payAllInvoices(@PathParam("accountId") final UUID accountId,
@QueryParam(QUERY_PAYMENT_METHOD_ID) final UUID inputPaymentMethodId,
@QueryParam(QUERY_PAYMENT_EXTERNAL) @DefaultValue("false") final Boolean externalPayment,
@QueryParam(QUERY_PAYMENT_AMOUNT) final BigDecimal paymentAmount,
@QueryParam(QUERY_TARGET_DATE) final String targetDate,
@QueryParam(QUERY_PLUGIN_PROPERTY) final List pluginPropertiesString,
@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 AccountApiException, PaymentApiException, InvoiceApiException {
final Iterable pluginProperties = extractPluginProperties(pluginPropertiesString);
final CallContext callContext = context.createCallContextWithAccountId(accountId, createdBy, reason, comment, request);
final Account account = accountUserApi.getAccountById(accountId, callContext);
final LocalDate inputDate = targetDate == null ? clock.getUTCToday() : toLocalDate(targetDate);
final Collection unpaidInvoices = invoiceApi.getUnpaidInvoicesByAccountId(account.getId(), null, inputDate, callContext);
BigDecimal remainingRequestPayment = paymentAmount;
if (remainingRequestPayment == null) {
remainingRequestPayment = BigDecimal.ZERO;
for (final Invoice invoice : unpaidInvoices) {
remainingRequestPayment = remainingRequestPayment.add(invoice.getBalance());
}
}
LocalDate filterMinDate = null;
LocalDate filterMaxDate = null;
StringBuilder filterInvoiceIds = null;
for (final Invoice invoice : unpaidInvoices) {
final BigDecimal amountToPay = (remainingRequestPayment.compareTo(invoice.getBalance()) >= 0) ?
invoice.getBalance() : remainingRequestPayment;
if (amountToPay.compareTo(BigDecimal.ZERO) > 0) {
final UUID paymentMethodId = externalPayment ?
null :
(inputPaymentMethodId != null ? inputPaymentMethodId : account.getPaymentMethodId());
createPurchaseForInvoice(account, invoice.getId(), amountToPay, paymentMethodId, externalPayment, null, null, pluginProperties, callContext);
if (filterMinDate == null || filterMinDate.isAfter(invoice.getTargetDate())) {
filterMinDate = invoice.getTargetDate();
}
if (filterMaxDate == null || filterMaxDate.isBefore(invoice.getTargetDate())) {
filterMaxDate = invoice.getTargetDate();
}
if (filterInvoiceIds == null) {
filterInvoiceIds = new StringBuilder();
} else {
filterInvoiceIds.append(",");
}
filterInvoiceIds.append(invoice.getId());
}
remainingRequestPayment = remainingRequestPayment.subtract(amountToPay);
if (remainingRequestPayment.compareTo(BigDecimal.ZERO) == 0) {
break;
}
}
//
// If the amount requested is greater than what had to be paid and if this an for an external payment (check, ..)
// then we apply some credit on the account.
//
final BigDecimal creditAmount = remainingRequestPayment;
if (externalPayment && remainingRequestPayment.compareTo(BigDecimal.ZERO) > 0) {
invoiceApi.insertCredits(account.getId(), clock.getUTCToday(), List.of(createCreditItem(account.getId(), creditAmount, account.getCurrency())), true, pluginProperties, callContext);
}
final Map queryParams = new HashMap<>();
if (filterMinDate != null) {
queryParams.put(QUERY_START_DATE, filterMinDate.toString());
}
if (filterMaxDate != null) {
queryParams.put(QUERY_END_DATE, filterMaxDate.toString());
}
if (filterInvoiceIds != null) {
queryParams.put(QUERY_INVOICES_FILTER, filterInvoiceIds.toString());
}
if (queryParams.size() > 0) {
return uriBuilder.buildResponse(uriInfo, AccountResource.class, "getInvoicesForAccount", account.getId(), queryParams, request);
} else {
return Response.status(Status.NO_CONTENT).build();
}
}
@TimedResource
@POST
@Path("/{accountId:" + UUID_PATTERN + "}/" + PAYMENT_METHODS)
@Consumes(APPLICATION_JSON)
@Produces(APPLICATION_JSON)
@ApiOperation(value = "Add a payment method", response = PaymentMethodJson.class)
@ApiResponses(value = {@ApiResponse(code = 201, message = "Payment method created"),
@ApiResponse(code = 400, message = "Invalid account id supplied"),
@ApiResponse(code = 404, message = "Account not found")})
public Response createPaymentMethod(@PathParam("accountId") final UUID accountId,
final PaymentMethodJson json,
@QueryParam(QUERY_PAYMENT_METHOD_IS_DEFAULT) @DefaultValue("false") final Boolean isDefault,
@QueryParam(QUERY_PAY_ALL_UNPAID_INVOICES) @DefaultValue("false") final Boolean payAllUnpaidInvoices,
@QueryParam(QUERY_PAYMENT_CONTROL_PLUGIN_NAME) final List paymentControlPluginNames,
@QueryParam(QUERY_PLUGIN_PROPERTY) final List pluginPropertiesString,
@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 AccountApiException, PaymentApiException {
verifyNonNullOrEmpty(json, "PaymentMethodJson body should be specified");
verifyNonNullOrEmpty(json.getPluginName(), "PaymentMethodJson pluginName should be specified");
final Iterable pluginProperties = extractPluginProperties(pluginPropertiesString);
final CallContext callContext = context.createCallContextWithAccountId(accountId, createdBy, reason, comment, request);
final PaymentMethod data = json.toPaymentMethod(accountId);
final Account account = accountUserApi.getAccountById(data.getAccountId(), callContext);
final boolean hasDefaultPaymentMethod = account.getPaymentMethodId() != null || isDefault;
final Collection unpaidInvoices = payAllUnpaidInvoices ? invoiceApi.getUnpaidInvoicesByAccountId(account.getId(), null, clock.getUTCToday(), callContext) :
Collections.emptyList();
if (payAllUnpaidInvoices && unpaidInvoices.size() > 0 && !hasDefaultPaymentMethod) {
return Response.status(Status.BAD_REQUEST).build();
}
final PaymentOptions paymentOptions = createControlPluginApiPaymentOptions(paymentControlPluginNames);
final UUID paymentMethodId = paymentApi.addPaymentMethodWithPaymentControl(account, data.getExternalKey(), data.getPluginName(), isDefault, data.getPluginDetail(), pluginProperties, paymentOptions, callContext);
if (payAllUnpaidInvoices && unpaidInvoices.size() > 0) {
for (final Invoice invoice : unpaidInvoices) {
createPurchaseForInvoice(account, invoice.getId(), invoice.getBalance(), paymentMethodId, false, null, null, pluginProperties, callContext);
}
}
return uriBuilder.buildResponse(uriInfo, PaymentMethodResource.class, "getPaymentMethod", paymentMethodId, request);
}
@TimedResource
@GET
@Path("/{accountId:" + UUID_PATTERN + "}/" + PAYMENT_METHODS)
@Produces(APPLICATION_JSON)
@ApiOperation(value = "Retrieve account payment methods", response = PaymentMethodJson.class, responseContainer = "List")
@ApiResponses(value = {@ApiResponse(code = 400, message = "Invalid account id supplied"),
@ApiResponse(code = 404, message = "Account not found")})
public Response getPaymentMethodsForAccount(@PathParam("accountId") final UUID accountId,
@QueryParam(QUERY_WITH_PLUGIN_INFO) @DefaultValue("false") final Boolean withPluginInfo,
@QueryParam(QUERY_INCLUDED_DELETED) @DefaultValue("false") final Boolean includedDeleted,
@QueryParam(QUERY_PLUGIN_PROPERTY) final List pluginPropertiesString,
@QueryParam(QUERY_AUDIT) @DefaultValue("NONE") final AuditMode auditMode,
@javax.ws.rs.core.Context final HttpServletRequest request) throws AccountApiException, PaymentApiException {
final Iterable pluginProperties = extractPluginProperties(pluginPropertiesString);
final TenantContext tenantContext = context.createTenantContextWithAccountId(accountId, request);
final Account account = accountUserApi.getAccountById(accountId, tenantContext);
final List methods = paymentApi.getAccountPaymentMethods(account.getId(), includedDeleted, withPluginInfo, pluginProperties, tenantContext);
final AccountAuditLogs accountAuditLogs = auditUserApi.getAccountAuditLogs(account.getId(), auditMode.getLevel(), tenantContext);
final List json = methods.stream()
.map(input -> PaymentMethodJson.toPaymentMethodJson(account, input, accountAuditLogs))
.collect(Collectors.toUnmodifiableList());
return Response.status(Status.OK).entity(json).build();
}
@TimedResource
@PUT
@Path("/{accountId:" + UUID_PATTERN + "}/" + PAYMENT_METHODS + "/refresh")
@Produces(APPLICATION_JSON)
@ApiOperation(value = "Refresh account payment methods")
@ApiResponses(value = {@ApiResponse(code = 204, message = "Successful operation"),
@ApiResponse(code = 400, message = "Invalid account id supplied"),
@ApiResponse(code = 404, message = "Account not found")})
public Response refreshPaymentMethods(@PathParam("accountId") final UUID accountId,
@QueryParam(QUERY_PAYMENT_PLUGIN_NAME) final String pluginName,
@QueryParam(QUERY_PLUGIN_PROPERTY) final List pluginPropertiesString,
@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 AccountApiException, PaymentApiException {
final Iterable pluginProperties = extractPluginProperties(pluginPropertiesString);
final CallContext callContext = context.createCallContextWithAccountId(accountId, createdBy, reason, comment, request);
final Account account = accountUserApi.getAccountById(accountId, callContext);
if (pluginName != null && !pluginName.isEmpty()) {
paymentApi.refreshPaymentMethods(account, pluginName, pluginProperties, callContext);
} else {
paymentApi.refreshPaymentMethods(account, pluginProperties, callContext);
}
return Response.status(Status.NO_CONTENT).build();
}
@TimedResource
@PUT
@Consumes(APPLICATION_JSON)
@Produces(APPLICATION_JSON)
@Path("/{accountId:" + UUID_PATTERN + "}/" + PAYMENT_METHODS + "/{paymentMethodId:" + UUID_PATTERN + "}/" + PAYMENT_METHODS_DEFAULT_PATH_POSTFIX)
@ApiOperation(value = "Set the default payment method")
@ApiResponses(value = {@ApiResponse(code = 204, message = "Successful operation"),
@ApiResponse(code = 400, message = "Invalid account id or payment method id supplied"),
@ApiResponse(code = 404, message = "Account not found")})
public Response setDefaultPaymentMethod(@PathParam("accountId") final UUID accountId,
@PathParam("paymentMethodId") final UUID paymentMethodId,
@QueryParam(QUERY_PAY_ALL_UNPAID_INVOICES) @DefaultValue("false") final Boolean payAllUnpaidInvoices,
@QueryParam(QUERY_PLUGIN_PROPERTY) final List pluginPropertiesString,
@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 AccountApiException, PaymentApiException {
final Iterable pluginProperties = extractPluginProperties(pluginPropertiesString);
final CallContext callContext = context.createCallContextWithAccountId(accountId, createdBy, reason, comment, request);
final Account account = accountUserApi.getAccountById(accountId, callContext);
paymentApi.setDefaultPaymentMethod(account, paymentMethodId, pluginProperties, callContext);
if (payAllUnpaidInvoices) {
final Collection unpaidInvoices = invoiceApi.getUnpaidInvoicesByAccountId(account.getId(), null, clock.getUTCToday(), callContext);
for (final Invoice invoice : unpaidInvoices) {
createPurchaseForInvoice(account, invoice.getId(), invoice.getBalance(), paymentMethodId, false, null, null, pluginProperties, callContext);
}
}
return Response.status(Status.NO_CONTENT).build();
}
/*
* ************************* PAYMENTS *****************************
*/
@TimedResource
@GET
@Path("/{accountId:" + UUID_PATTERN + "}/" + PAYMENTS)
@Produces(APPLICATION_JSON)
@ApiOperation(value = "Retrieve account payments", response = PaymentJson.class, responseContainer = "List")
@ApiResponses(value = {@ApiResponse(code = 400, message = "Invalid account id supplied")})
public Response getPaymentsForAccount(@PathParam("accountId") final UUID accountId,
@QueryParam(QUERY_WITH_ATTEMPTS) @DefaultValue("false") final Boolean withAttempts,
@QueryParam(QUERY_WITH_PLUGIN_INFO) @DefaultValue("false") final Boolean withPluginInfo,
@QueryParam(QUERY_PLUGIN_PROPERTY) final List pluginPropertiesString,
@QueryParam(QUERY_AUDIT) @DefaultValue("NONE") final AuditMode auditMode,
@javax.ws.rs.core.Context final HttpServletRequest request) throws PaymentApiException {
final Iterable pluginProperties = extractPluginProperties(pluginPropertiesString);
final TenantContext tenantContext = context.createTenantContextWithAccountId(accountId, request);
final List payments = paymentApi.getAccountPayments(accountId, withPluginInfo, withAttempts, pluginProperties, tenantContext);
final AccountAuditLogs accountAuditLogs = auditUserApi.getAccountAuditLogs(accountId, auditMode.getLevel(), tenantContext);
final List result = payments.stream()
.map(payment -> new PaymentJson(payment, accountAuditLogs))
.collect(Collectors.toUnmodifiableList());
return Response.status(Response.Status.OK).entity(result).build();
}
@TimedResource(name = "processPayment")
@POST
@Path("/" + PAYMENTS)
@Consumes(APPLICATION_JSON)
@Produces(APPLICATION_JSON)
@ApiOperation(value = "Trigger a payment using the account external key (authorization, purchase or credit)", response = PaymentJson.class)
@ApiResponses(value = {@ApiResponse(code = 201, message = "Payment transaction created successfully"),
@ApiResponse(code = 400, message = "Invalid account external key supplied"),
@ApiResponse(code = 404, message = "Account not found"),
@ApiResponse(code = 402, message = "Transaction declined by gateway"),
@ApiResponse(code = 422, message = "Payment is aborted by a control plugin"),
@ApiResponse(code = 502, message = "Failed to submit payment transaction"),
@ApiResponse(code = 503, message = "Payment in unknown status, failed to receive gateway response"),
@ApiResponse(code = 504, message = "Payment operation timeout")})
public Response processPaymentByExternalKey(@MetricTag(tag = "type", property = "transactionType") final PaymentTransactionJson json,
@ApiParam(required = true) @QueryParam(QUERY_EXTERNAL_KEY) final String externalKey,
@QueryParam(QUERY_PAYMENT_METHOD_ID) final UUID paymentMethodId,
@QueryParam(QUERY_PAYMENT_CONTROL_PLUGIN_NAME) final List paymentControlPluginNames,
@QueryParam(QUERY_PLUGIN_PROPERTY) final List pluginPropertiesString,
@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 PaymentApiException, AccountApiException {
final CallContext callContext = context.createCallContextNoAccountId(createdBy, reason, comment, request);
final Account account = accountUserApi.getAccountByKey(externalKey, callContext);
return processPayment(json, account, paymentMethodId, paymentControlPluginNames, pluginPropertiesString, uriInfo, callContext, request);
}
@TimedResource(name = "processPayment")
@POST
@Path("/{accountId:" + UUID_PATTERN + "}/" + PAYMENTS)
@Consumes(APPLICATION_JSON)
@Produces(APPLICATION_JSON)
@ApiOperation(value = "Trigger a payment (authorization, purchase or credit)", response = PaymentJson.class)
@ApiResponses(value = {@ApiResponse(code = 201, message = "Payment transaction created successfully"),
@ApiResponse(code = 400, message = "Invalid account id supplied"),
@ApiResponse(code = 404, message = "Account not found"),
@ApiResponse(code = 402, message = "Transaction declined by gateway"),
@ApiResponse(code = 422, message = "Payment is aborted by a control plugin"),
@ApiResponse(code = 502, message = "Failed to submit payment transaction"),
@ApiResponse(code = 503, message = "Payment in unknown status, failed to receive gateway response"),
@ApiResponse(code = 504, message = "Payment operation timeout")})
public Response processPayment(@PathParam("accountId") final UUID accountId,
@MetricTag(tag = "type", property = "transactionType") final PaymentTransactionJson json,
@QueryParam(QUERY_PAYMENT_METHOD_ID) final UUID inputPaymentMethodId,
@QueryParam(QUERY_PAYMENT_CONTROL_PLUGIN_NAME) final List paymentControlPluginNames,
@QueryParam(QUERY_PLUGIN_PROPERTY) final List pluginPropertiesString,
@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 PaymentApiException, AccountApiException {
final CallContext callContext = context.createCallContextWithAccountId(accountId, createdBy, reason, comment, request);
final Account account = accountUserApi.getAccountById(accountId, callContext);
return processPayment(json, account, inputPaymentMethodId, paymentControlPluginNames, pluginPropertiesString, uriInfo, callContext, request);
}
private Response processPayment(final PaymentTransactionJson json,
final Account account,
final UUID inputPaymentMethodId,
final List paymentControlPluginNames,
final List pluginPropertiesString,
final UriInfo uriInfo,
final CallContext callContext,
final HttpServletRequest request) throws PaymentApiException {
verifyNonNullOrEmpty(json, "PaymentTransactionJson body should be specified");
verifyNonNullOrEmpty(json.getTransactionType(), "PaymentTransactionJson transactionType needs to be set",
json.getAmount(), "PaymentTransactionJson amount needs to be set");
final Iterable pluginProperties = extractPluginProperties(pluginPropertiesString);
final Currency currency = json.getCurrency() == null ? account.getCurrency() : json.getCurrency();
final UUID paymentId = json.getPaymentId();
//
// If paymentId was specified, it means we are attempting a payment completion. The preferred way is to use the PaymentResource
// (PUT /1.0/kb/payments/{paymentId}/completeTransaction), but for backward compatibility we still allow the call to proceed
// as long as the request/existing state is healthy (i.e there is a matching PENDING transaction)
//
final UUID paymentMethodId;
if (paymentId != null) {
final Payment initialPayment = paymentApi.getPayment(paymentId, false, false, pluginProperties, callContext);
final PaymentTransaction pendingOrSuccessTransaction = lookupPendingOrSuccessTransaction(initialPayment,
json.getTransactionId(),
json.getTransactionExternalKey(),
json.getTransactionType());
// If transaction was already completed, return early (See #626)
if (pendingOrSuccessTransaction.getTransactionStatus() == TransactionStatus.SUCCESS) {
return uriBuilder.buildResponse(uriInfo, PaymentResource.class, "getPayment", pendingOrSuccessTransaction.getPaymentId(), request);
}
paymentMethodId = initialPayment.getPaymentMethodId();
} else {
paymentMethodId = inputPaymentMethodId == null ? account.getPaymentMethodId() : inputPaymentMethodId;
}
validatePaymentMethodForAccount(account.getId(), paymentMethodId, callContext);
final TransactionType transactionType = json.getTransactionType();
final PaymentOptions paymentOptions = createControlPluginApiPaymentOptions(paymentControlPluginNames);
final Payment result;
switch (transactionType) {
case AUTHORIZE:
result = paymentApi.createAuthorizationWithPaymentControl(account, paymentMethodId, paymentId, json.getAmount(), currency, json.getEffectiveDate(),
json.getPaymentExternalKey(), json.getTransactionExternalKey(),
pluginProperties, paymentOptions, callContext);
break;
case PURCHASE:
result = paymentApi.createPurchaseWithPaymentControl(account, paymentMethodId, paymentId, json.getAmount(), currency, json.getEffectiveDate(),
json.getPaymentExternalKey(), json.getTransactionExternalKey(),
pluginProperties, paymentOptions, callContext);
break;
case CREDIT:
result = paymentApi.createCreditWithPaymentControl(account, paymentMethodId, paymentId, json.getAmount(), currency, json.getEffectiveDate(),
json.getPaymentExternalKey(), json.getTransactionExternalKey(),
pluginProperties, paymentOptions, callContext);
break;
default:
return Response.status(Status.PRECONDITION_FAILED).entity("TransactionType " + transactionType + " is not allowed for an account").build();
}
return createPaymentResponse(uriInfo, result, transactionType, json.getTransactionExternalKey(), request);
}
/*
* ************************** OVERDUE ********************************
*/
@TimedResource
@GET
@Path("/{accountId:" + UUID_PATTERN + "}/" + OVERDUE)
@Produces(APPLICATION_JSON)
@ApiOperation(value = "Retrieve overdue state for account", response = OverdueStateJson.class)
@ApiResponses(value = {@ApiResponse(code = 400, message = "Invalid account id supplied"),
@ApiResponse(code = 404, message = "Account not found")})
public Response getOverdueAccount(@PathParam("accountId") final UUID accountId,
@javax.ws.rs.core.Context final HttpServletRequest request) throws AccountApiException, OverdueException, OverdueApiException {
final TenantContext tenantContext = context.createTenantContextWithAccountId(accountId, request);
final Account account = accountUserApi.getAccountById(accountId, tenantContext);
final OverdueState overdueState = overdueApi.getOverdueStateFor(account.getId(), tenantContext);
return Response.status(Status.OK).entity(new OverdueStateJson(overdueState)).build();
}
/*
* ************************* BLOCKING STATE *****************************
*/
@TimedResource
@GET
@Path("/{accountId:" + UUID_PATTERN + "}/" + BLOCK)
@Produces(APPLICATION_JSON)
@ApiOperation(value = "Retrieve blocking states for account", response = BlockingStateJson.class, responseContainer = "List")
@ApiResponses(value = {@ApiResponse(code = 400, message = "Invalid account id supplied")})
public Response getBlockingStates(@PathParam(ID_PARAM_NAME) final UUID accountId,
@QueryParam(QUERY_BLOCKING_STATE_TYPES) final List typeFilter,
@QueryParam(QUERY_BLOCKING_STATE_SVCS) final List svcsFilter,
@QueryParam(QUERY_AUDIT) @DefaultValue("NONE") final AuditMode auditMode,
@javax.ws.rs.core.Context final HttpServletRequest request) throws EntitlementApiException {
final TenantContext tenantContext = this.context.createTenantContextWithAccountId(accountId, request);
final Iterable blockingStates = subscriptionApi.getBlockingStates(accountId, typeFilter, svcsFilter, OrderingType.ASCENDING, SubscriptionApi.ALL_EVENTS, tenantContext);
final AccountAuditLogs accountAuditLogs = auditUserApi.getAccountAuditLogs(accountId, auditMode.getLevel(), tenantContext);
final List result = Iterables.toStream(blockingStates)
.map(input -> new BlockingStateJson(input, accountAuditLogs))
.collect(Collectors.toUnmodifiableList());
return Response.status(Status.OK).entity(result).build();
}
@TimedResource
@GET
@Path("/" + BLOCK + "/{blockingId:" + UUID_PATTERN + "}/" + AUDIT_LOG_WITH_HISTORY)
@Produces(APPLICATION_JSON)
@ApiOperation(value = "Retrieve blocking state audit logs with history by id", response = AuditLogJson.class, responseContainer = "List")
@ApiResponses(value = {@ApiResponse(code = 404, message = "Blocking state not found")})
public Response getBlockingStateAuditLogsWithHistory(@PathParam("blockingId") final UUID blockingId,
@javax.ws.rs.core.Context final HttpServletRequest request) {
final TenantContext tenantContext = context.createTenantContextNoAccountId(request);
final List auditLogWithHistory = subscriptionApi.getBlockingStateAuditLogsWithHistoryForId(blockingId, AuditLevel.FULL, tenantContext);
return Response.status(Status.OK).entity(getAuditLogsWithHistory(auditLogWithHistory)).build();
}
@TimedResource
@POST
@Path("/{accountId:" + UUID_PATTERN + "}/" + BLOCK)
@Consumes(APPLICATION_JSON)
@ApiOperation(value = "Block an account", response = BlockingStateJson.class, responseContainer = "List")
@ApiResponses(value = {@ApiResponse(code = 201, message = "Blocking state created successfully"),
@ApiResponse(code = 400, message = "Invalid account id supplied"),
@ApiResponse(code = 404, message = "Account not found")})
public Response addAccountBlockingState(@PathParam(ID_PARAM_NAME) final UUID id,
final BlockingStateJson json,
@QueryParam(QUERY_REQUESTED_DT) final String requestedDate,
@QueryParam(QUERY_PLUGIN_PROPERTY) final List pluginPropertiesString,
@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 SubscriptionApiException, EntitlementApiException, AccountApiException {
return addBlockingState(json, id, id, BlockingStateType.ACCOUNT, requestedDate, pluginPropertiesString, createdBy, reason, comment, request, uriInfo);
}
/*
* ************************* CUSTOM FIELDS *****************************
*/
@TimedResource
@GET
@Path("/{accountId:" + UUID_PATTERN + "}/" + CUSTOM_FIELDS)
@Produces(APPLICATION_JSON)
@ApiOperation(value = "Retrieve account custom fields", response = CustomFieldJson.class, responseContainer = "List", nickname = "getAccountCustomFields")
@ApiResponses(value = {@ApiResponse(code = 400, message = "Invalid account id supplied")})
public Response getCustomFields(@PathParam(ID_PARAM_NAME) final UUID accountId,
@QueryParam(QUERY_AUDIT) @DefaultValue("NONE") final AuditMode auditMode,
@javax.ws.rs.core.Context final HttpServletRequest request) {
return super.getCustomFields(accountId, auditMode, context.createTenantContextWithAccountId(accountId, request));
}
@TimedResource
@GET
@Path("/{accountId:" + UUID_PATTERN + "}/" + ALL_CUSTOM_FIELDS)
@Produces(APPLICATION_JSON)
@ApiOperation(value = "Retrieve account customFields", response = CustomFieldJson.class, responseContainer = "List")
@ApiResponses(value = {@ApiResponse(code = 400, message = "Invalid account id supplied"),
@ApiResponse(code = 404, message = "Account not found")})
public Response getAllCustomFields(@PathParam(ID_PARAM_NAME) final UUID accountId,
@QueryParam(QUERY_OBJECT_TYPE) final ObjectType objectType,
@QueryParam(QUERY_AUDIT) @DefaultValue("NONE") final AuditMode auditMode,
@javax.ws.rs.core.Context final HttpServletRequest request) {
final TenantContext tenantContext = context.createTenantContextWithAccountId(accountId, request);
final List customFields = objectType != null ?
customFieldUserApi.getCustomFieldsForAccountType(accountId, objectType, tenantContext) :
customFieldUserApi.getCustomFieldsForAccount(accountId, tenantContext);
return createCustomFieldResponse(customFields, auditMode, tenantContext);
}
@TimedResource
@POST
@Path("/{accountId:" + UUID_PATTERN + "}/" + CUSTOM_FIELDS)
@Consumes(APPLICATION_JSON)
@Produces(APPLICATION_JSON)
@ApiOperation(value = "Add custom fields to account", response = CustomField.class, responseContainer = "List")
@ApiResponses(value = {@ApiResponse(code = 201, message = "Custom field created successfully"),
@ApiResponse(code = 400, message = "Invalid account id supplied")})
public Response createAccountCustomFields(@PathParam(ID_PARAM_NAME) final UUID accountId,
final List 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(accountId, customFields, context.createCallContextWithAccountId(accountId, createdBy, reason,
comment, request), uriInfo, request);
}
@TimedResource
@PUT
@Path("/{accountId:" + UUID_PATTERN + "}/" + CUSTOM_FIELDS)
@Consumes(APPLICATION_JSON)
@Produces(APPLICATION_JSON)
@ApiOperation(value = "Modify custom fields to account")
@ApiResponses(value = {@ApiResponse(code = 204, message = "Successful operation"),
@ApiResponse(code = 400, message = "Invalid account id supplied")})
public Response modifyAccountCustomFields(@PathParam(ID_PARAM_NAME) final UUID accountId,
final List 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(accountId, customFields, context.createCallContextWithAccountId(accountId, createdBy, reason,
comment, request));
}
@TimedResource
@DELETE
@Path("/{accountId:" + UUID_PATTERN + "}/" + CUSTOM_FIELDS)
@Consumes(APPLICATION_JSON)
@Produces(APPLICATION_JSON)
@ApiOperation(value = "Remove custom fields from account")
@ApiResponses(value = {@ApiResponse(code = 204, message = "Successful operation"),
@ApiResponse(code = 400, message = "Invalid account id supplied")})
public Response deleteAccountCustomFields(@PathParam(ID_PARAM_NAME) final UUID accountId,
@QueryParam(QUERY_CUSTOM_FIELD) final List 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(accountId, customFieldList,
context.createCallContextWithAccountId(accountId, createdBy, reason, comment, request));
}
/*
* ************************* TAGS *****************************
*/
@TimedResource
@GET
@Path("/{accountId:" + UUID_PATTERN + "}/" + TAGS)
@Produces(APPLICATION_JSON)
@ApiOperation(value = "Retrieve account tags", response = TagJson.class, responseContainer = "List", nickname = "getAccountTags")
@ApiResponses(value = {@ApiResponse(code = 400, message = "Invalid account id supplied"),
@ApiResponse(code = 404, message = "Account not found")})
public Response getTags(@PathParam(ID_PARAM_NAME) final UUID accountId,
@QueryParam(QUERY_INCLUDED_DELETED) @DefaultValue("false") final Boolean includedDeleted,
@QueryParam(QUERY_AUDIT) @DefaultValue("NONE") final AuditMode auditMode,
@javax.ws.rs.core.Context final HttpServletRequest request) throws TagDefinitionApiException {
return super.getTags(accountId, accountId, auditMode, includedDeleted, context.createTenantContextWithAccountId(accountId, request));
}
@TimedResource
@GET
@Path("/{accountId:" + UUID_PATTERN + "}/" + ALL_TAGS)
@Produces(APPLICATION_JSON)
@ApiOperation(value = "Retrieve account tags", response = TagJson.class, responseContainer = "List")
@ApiResponses(value = {@ApiResponse(code = 400, message = "Invalid account id supplied"),
@ApiResponse(code = 404, message = "Account not found")})
public Response getAllTags(@PathParam(ID_PARAM_NAME) final UUID accountId,
@QueryParam(QUERY_OBJECT_TYPE) final ObjectType objectType,
@QueryParam(QUERY_INCLUDED_DELETED) @DefaultValue("false") final Boolean includedDeleted,
@QueryParam(QUERY_AUDIT) @DefaultValue("NONE") final AuditMode auditMode,
@javax.ws.rs.core.Context final HttpServletRequest request) throws TagDefinitionApiException {
final TenantContext tenantContext = context.createTenantContextWithAccountId(accountId, request);
final List tags = objectType != null ?
tagUserApi.getTagsForAccountType(accountId, objectType, includedDeleted, tenantContext) :
tagUserApi.getTagsForAccount(accountId, includedDeleted, tenantContext);
return createTagResponse(accountId, tags, auditMode, tenantContext);
}
@TimedResource
@POST
@Path("/{accountId:" + UUID_PATTERN + "}/" + TAGS)
@Consumes(APPLICATION_JSON)
@Produces(APPLICATION_JSON)
@ApiOperation(value = "Add tags to account", response = TagJson.class, responseContainer = "List")
@ApiResponses(value = {@ApiResponse(code = 201, message = "Tag created successfully"),
@ApiResponse(code = 400, message = "Invalid account id supplied")})
public Response createAccountTags(@PathParam(ID_PARAM_NAME) final UUID accountId,
final List 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(accountId, tagList, uriInfo,
context.createCallContextWithAccountId(accountId, createdBy, reason, comment, request), request);
}
@TimedResource
@DELETE
@Path("/{accountId:" + UUID_PATTERN + "}/" + TAGS)
@Consumes(APPLICATION_JSON)
@Produces(APPLICATION_JSON)
@ApiOperation(value = "Remove tags from account")
@ApiResponses(value = {@ApiResponse(code = 204, message = "Successful operation"),
@ApiResponse(code = 400, message = "Invalid account id supplied or account does not have a default payment method (AUTO_PAY_OFF tag only)")})
public Response deleteAccountTags(@PathParam(ID_PARAM_NAME) final UUID accountId,
@QueryParam(QUERY_TAG) final List 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, AccountApiException {
final CallContext callContext = context.createCallContextWithAccountId(accountId, createdBy, reason, comment, request);
// Look if there is an AUTO_PAY_OFF for that account and check if the account has a default paymentMethod
// If not we can't remove the AUTO_PAY_OFF tag
boolean isTagAutoPayOff = false;
for (final UUID cur : tagList) {
if (cur.equals(ControlTagType.AUTO_PAY_OFF.getId())) {
isTagAutoPayOff = true;
break;
}
}
if (isTagAutoPayOff) {
final Account account = accountUserApi.getAccountById(accountId, callContext);
if (account.getPaymentMethodId() == null) {
throw new TagApiException(ErrorCode.TAG_CANNOT_BE_REMOVED, ControlTagType.AUTO_PAY_OFF, " the account does not have a default payment method");
}
}
return super.deleteTags(accountId, tagList, callContext);
}
/*
* ************************* EMAILS *****************************
*/
@TimedResource
@GET
@Path("/{accountId:" + UUID_PATTERN + "}/" + EMAILS)
@Produces(APPLICATION_JSON)
@ApiOperation(value = "Retrieve an account emails", response = AccountEmailJson.class, responseContainer = "List")
@ApiResponses(value = {@ApiResponse(code = 400, message = "Invalid account id supplied")})
public Response getEmails(@PathParam(ID_PARAM_NAME) final UUID accountId,
@javax.ws.rs.core.Context final HttpServletRequest request) {
final List emails = accountUserApi.getEmails(accountId, context.createTenantContextWithAccountId(accountId, request));
final List emailsJson = new ArrayList();
for (final AccountEmail email : emails) {
emailsJson.add(new AccountEmailJson(email.getAccountId(), email.getEmail()));
}
return Response.status(Status.OK).entity(emailsJson).build();
}
@TimedResource
@POST
@Path("/{accountId:" + UUID_PATTERN + "}/" + EMAILS)
@Consumes(APPLICATION_JSON)
@Produces(APPLICATION_JSON)
@ApiOperation(value = "Add account email", response = AccountEmailJson.class, responseContainer = "List")
@ApiResponses(value = {@ApiResponse(code = 201, message = "Email created successfully"),
@ApiResponse(code = 400, message = "Invalid account id supplied"),
@ApiResponse(code = 404, message = "Account not found")})
public Response addEmail(@PathParam(ID_PARAM_NAME) final UUID accountId,
final AccountEmailJson json,
@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 AccountApiException {
verifyNonNullOrEmpty(json, "AccountEmailJson body should be specified");
verifyNonNullOrEmpty(json.getEmail(), "AccountEmailJson email needs to be set");
final CallContext callContext = context.createCallContextWithAccountId(accountId, createdBy, reason, comment, request);
// Make sure the account exist or we will confuse the history and auditing code
accountUserApi.getAccountById(accountId, callContext);
// Make sure the email doesn't exist
final AccountEmail existingEmail = accountUserApi.getEmails(accountId, callContext).stream()
.filter(input -> input.getEmail().equals(json.getEmail()))
.findFirst().orElse(null);
if (existingEmail == null) {
accountUserApi.addEmail(accountId, json.toAccountEmail(UUIDs.randomUUID()), callContext);
}
return uriBuilder.buildResponse(uriInfo, AccountResource.class, "getEmails", json.getAccountId(), request);
}
@TimedResource
@DELETE
@Path("/{accountId:" + UUID_PATTERN + "}/" + EMAILS + "/{email}")
@Produces(APPLICATION_JSON)
@ApiOperation(value = "Delete email from account")
@ApiResponses(value = {@ApiResponse(code = 204, message = "Successful operation"),
@ApiResponse(code = 400, message = "Invalid account id supplied")})
public Response removeEmail(@PathParam(ID_PARAM_NAME) final UUID accountId,
@PathParam("email") final String email,
@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 List emails = accountUserApi.getEmails(accountId, context.createTenantContextWithAccountId(accountId, request));
for (final AccountEmail cur : emails) {
if (cur.getEmail().equals(email)) {
final AccountEmailJson accountEmailJson = new AccountEmailJson(accountId, email);
final AccountEmail accountEmail = accountEmailJson.toAccountEmail(cur.getId());
accountUserApi.removeEmail(accountId, accountEmail, context.createCallContextWithAccountId(accountId, createdBy, reason, comment, request));
}
}
return Response.status(Status.NO_CONTENT).build();
}
@TimedResource
@GET
@Path("/{accountId:" + UUID_PATTERN + "}/" + EMAILS + "/{accountEmailId:" + UUID_PATTERN + "}/" + AUDIT_LOG_WITH_HISTORY)
@Produces(APPLICATION_JSON)
@ApiOperation(value = "Retrieve account email audit logs with history by id", response = AuditLogJson.class, responseContainer = "List")
@ApiResponses(value = {@ApiResponse(code = 404, message = "Account not found")})
public Response getAccountEmailAuditLogsWithHistory(@PathParam("accountId") final UUID accountId,
@PathParam("accountEmailId") final UUID accountEmailId,
@javax.ws.rs.core.Context final HttpServletRequest request) throws AccountApiException {
final TenantContext tenantContext = context.createTenantContextWithAccountId(accountId, request);
final List auditLogWithHistory = accountUserApi.getEmailAuditLogsWithHistoryForId(accountEmailId, AuditLevel.FULL, tenantContext);
return Response.status(Status.OK).entity(getAuditLogsWithHistory(auditLogWithHistory)).build();
}
@Override
protected ObjectType getObjectType() {
return ObjectType.ACCOUNT;
}
// -------------------------------------
// Parent and child accounts
// -------------------------------------
@TimedResource
@GET
@Path("/{accountId:" + UUID_PATTERN + "}/" + CHILDREN)
@Produces(APPLICATION_JSON)
@ApiOperation(value = "List children accounts", response = AccountJson.class, responseContainer = "List")
@ApiResponses(value = {@ApiResponse(code = 400, message = "Invalid parent account id supplied"),
@ApiResponse(code = 404, message = "Parent Account not found")})
public Response getChildrenAccounts(@PathParam("accountId") final UUID parentAccountId,
@QueryParam(QUERY_ACCOUNT_WITH_BALANCE) @DefaultValue("false") final Boolean accountWithBalance,
@QueryParam(QUERY_ACCOUNT_WITH_BALANCE_AND_CBA) @DefaultValue("false") final Boolean accountWithBalanceAndCBA,
@QueryParam(QUERY_AUDIT) @DefaultValue("NONE") final AuditMode auditMode,
@javax.ws.rs.core.Context final HttpServletRequest request) throws AccountApiException {
final TenantContext tenantContext = context.createTenantContextWithAccountId(parentAccountId, request);
final List accounts = accountUserApi.getChildrenAccounts(parentAccountId, tenantContext);
final List accountJson = new ArrayList();
for (final Account account : accounts) {
final AccountAuditLogs accountAuditLogs = auditUserApi.getAccountAuditLogs(account.getId(), auditMode.getLevel(), tenantContext);
accountJson.add(getAccount(account, accountWithBalance, accountWithBalanceAndCBA, accountAuditLogs, tenantContext));
}
return Response.status(Status.OK).entity(accountJson).build();
}
@TimedResource
@PUT
@Path("/{childAccountId:" + UUID_PATTERN + "}/" + TRANSFER_CREDIT)
@Consumes(APPLICATION_JSON)
@Produces(APPLICATION_JSON)
@ApiOperation(value = "Move a given child credit to the parent level")
@ApiResponses(value = {@ApiResponse(code = 204, message = "Successful operation"),
@ApiResponse(code = 400, message = "Account does not have credit"),
@ApiResponse(code = 404, message = "Account not found")})
public Response transferChildCreditToParent(@PathParam("childAccountId") final UUID childAccountId,
@HeaderParam(HDR_CREATED_BY) final String createdBy,
@HeaderParam(HDR_REASON) final String reason,
@HeaderParam(HDR_COMMENT) final String comment,
@javax.ws.rs.core.Context final HttpServletRequest request,
@javax.ws.rs.core.Context final UriInfo uriInfo) throws InvoiceApiException {
final CallContext callContext = context.createCallContextWithAccountId(childAccountId, createdBy, reason, comment, request);
invoiceApi.transferChildCreditToParent(childAccountId, callContext);
return Response.status(Status.NO_CONTENT).build();
}
/*
* ************************* AUDIT LOGS *****************************
*/
@TimedResource
@GET
@Path("/{accountId:" + UUID_PATTERN + "}/" + AUDIT_LOG)
@Produces(APPLICATION_JSON)
@ApiOperation(value = "Retrieve audit logs by account id", response = AuditLogJson.class, responseContainer = "List")
@ApiResponses(value = {@ApiResponse(code = 404, message = "Account not found")})
public Response getAccountAuditLogs(@PathParam("accountId") final UUID accountId,
@javax.ws.rs.core.Context final HttpServletRequest request) throws AccountApiException {
final TenantContext tenantContext = context.createTenantContextWithAccountId(accountId, request);
final AccountAuditLogs accountAuditLogs = auditUserApi.getAccountAuditLogs(accountId, AuditLevel.FULL, tenantContext);
return Response.status(Status.OK).entity(getAuditLogs(accountAuditLogs)).build();
}
@TimedResource
@GET
@Path("/{accountId:" + UUID_PATTERN + "}/" + AUDIT_LOG_WITH_HISTORY)
@Produces(APPLICATION_JSON)
@ApiOperation(value = "Retrieve account audit logs with history by account id", response = AuditLogJson.class, responseContainer = "List")
@ApiResponses(value = {@ApiResponse(code = 404, message = "Account not found")})
public Response getAccountAuditLogsWithHistory(@PathParam("accountId") final UUID accountId,
@javax.ws.rs.core.Context final HttpServletRequest request) throws AccountApiException {
final TenantContext tenantContext = context.createTenantContextWithAccountId(accountId, request);
final List auditLogWithHistory = accountUserApi.getAuditLogsWithHistoryForId(accountId, AuditLevel.FULL, tenantContext);
return Response.status(Status.OK).entity(getAuditLogsWithHistory(auditLogWithHistory)).build();
}
private List getAuditLogs(final AccountAuditLogs accountAuditLogs) {
if (accountAuditLogs.getAuditLogs() == null) {
return null;
}
return accountAuditLogs.getAuditLogs().stream().map(AuditLogJson::new).collect(Collectors.toUnmodifiableList());
}
private InvoiceItem createCreditItem(final UUID accountId, final BigDecimal creditAmount, final Currency currency) {
return new InvoiceItem() {
@Override
public InvoiceItemType getInvoiceItemType() {
return InvoiceItemType.CREDIT_ADJ;
}
@Override
public UUID getInvoiceId() {
return null;
}
@Override
public UUID getAccountId() {
return accountId;
}
@Override
public UUID getChildAccountId() {
return null;
}
@Override
public LocalDate getStartDate() {
return null;
}
@Override
public LocalDate getEndDate() {
return null;
}
@Override
public BigDecimal getAmount() {
return creditAmount;
}
@Override
public Currency getCurrency() {
return currency;
}
@Override
public String getDescription() {
return "pay all invoices";
}
@Override
public UUID getBundleId() {
return null;
}
@Override
public UUID getSubscriptionId() {
return null;
}
@Override
public String getProductName() {
return null;
}
@Override
public String getPrettyProductName() {
return null;
}
@Override
public String getPlanName() {
return null;
}
@Override
public String getPrettyPlanName() {
return null;
}
@Override
public String getPhaseName() {
return null;
}
@Override
public String getPrettyPhaseName() {
return null;
}
@Override
public String getUsageName() {
return null;
}
@Override
public String getPrettyUsageName() {
return null;
}
@Override
public BigDecimal getRate() {
return null;
}
@Override
public UUID getLinkedItemId() {
return null;
}
@Override
public BigDecimal getQuantity() {
return null;
}
@Override
public String getItemDetails() {
return null;
}
@Override
public DateTime getCatalogEffectiveDate() {
return null;
}
@Override
public boolean matches(final Object o) {
return false;
}
@Override
public UUID getId() {
return null;
}
@Override
public DateTime getCreatedDate() {
return null;
}
@Override
public DateTime getUpdatedDate() {
return null;
}
};
}
}