Create API to list Quota credits (#9590)

Co-authored-by: Bernardo De Marco Gonçalves <bernardomg2004@gmail.com>
This commit is contained in:
Fabricio Duarte 2025-01-16 11:19:32 -03:00 committed by GitHub
parent 0c13ded943
commit 449d3c7cb1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 572 additions and 85 deletions

View File

@ -1192,6 +1192,14 @@ public class ApiConstants {
"numeric value will be applied; if the result is neither a boolean nor a numeric value, the tariff will not be applied. If the rule is not informed, the tariff " +
"value will be applied.";
public static final String PARAMETER_DESCRIPTION_START_DATE_POSSIBLE_FORMATS = "The recommended format is \"yyyy-MM-dd'T'HH:mm:ssZ\" (e.g.: \"2023-01-01T12:00:00+0100\"); " +
"however, the following formats are also accepted: \"yyyy-MM-dd HH:mm:ss\" (e.g.: \"2023-01-01 12:00:00\") and \"yyyy-MM-dd\" (e.g.: \"2023-01-01\" - if the time is not " +
"added, it will be interpreted as \"00:00:00\"). If the recommended format is not used, the date will be considered in the server timezone.";
public static final String PARAMETER_DESCRIPTION_END_DATE_POSSIBLE_FORMATS = "The recommended format is \"yyyy-MM-dd'T'HH:mm:ssZ\" (e.g.: \"2023-01-01T12:00:00+0100\"); " +
"however, the following formats are also accepted: \"yyyy-MM-dd HH:mm:ss\" (e.g.: \"2023-01-01 12:00:00\") and \"yyyy-MM-dd\" (e.g.: \"2023-01-01\" - if the time is not " +
"added, it will be interpreted as \"23:59:59\"). If the recommended format is not used, the date will be considered in the server timezone.";
/**
* This enum specifies IO Drivers, each option controls specific policies on I/O.
* Qemu guests support "threads" and "native" options Since 0.8.8 ; "io_uring" is supported Since 6.3.0 (QEMU 5.0).
@ -1214,14 +1222,6 @@ public class ApiConstants {
}
}
public static final String PARAMETER_DESCRIPTION_START_DATE_POSSIBLE_FORMATS = "The recommended format is \"yyyy-MM-dd'T'HH:mm:ssZ\" (e.g.: \"2023-01-01T12:00:00+0100\"); " +
"however, the following formats are also accepted: \"yyyy-MM-dd HH:mm:ss\" (e.g.: \"2023-01-01 12:00:00\") and \"yyyy-MM-dd\" (e.g.: \"2023-01-01\" - if the time is not " +
"added, it will be interpreted as \"00:00:00\"). If the recommended format is not used, the date will be considered in the server timezone.";
public static final String PARAMETER_DESCRIPTION_END_DATE_POSSIBLE_FORMATS = "The recommended format is \"yyyy-MM-dd'T'HH:mm:ssZ\" (e.g.: \"2023-01-01T12:00:00+0100\"); " +
"however, the following formats are also accepted: \"yyyy-MM-dd HH:mm:ss\" (e.g.: \"2023-01-01 12:00:00\") and \"yyyy-MM-dd\" (e.g.: \"2023-01-01\" - if the time is not " +
"added, it will be interpreted as \"23:59:59\"). If the recommended format is not used, the date will be considered in the server timezone.";
public enum BootType {
UEFI, BIOS;

View File

@ -42,5 +42,7 @@ public interface DomainDao extends GenericDao<DomainVO, Long> {
List<Long> getDomainChildrenIds(String path);
List<Long> getDomainAndChildrenIds(long domainId);
boolean domainIdListContainsAccessibleDomain(String domainIdList, Account caller, Long domainId);
}

View File

@ -19,6 +19,7 @@ package com.cloud.domain.dao;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
@ -238,6 +239,15 @@ public class DomainDaoImpl extends GenericDaoBase<DomainVO, Long> implements Dom
return customSearch(sc, null);
}
@Override
public List<Long> getDomainAndChildrenIds(long domainId) {
DomainVO domain = findById(domainId);
if (domain != null) {
return getDomainChildrenIds(domain.getPath());
}
return new ArrayList<>();
}
@Override
public boolean isChildDomain(Long parentId, Long childId) {
if ((parentId == null) || (childId == null)) {

View File

@ -24,3 +24,10 @@ CALL `cloud`.`IDEMPOTENT_ADD_COLUMN`('cloud.console_session', 'console_endpoint_
-- Add client_address column to cloud.console_session table
CALL `cloud`.`IDEMPOTENT_ADD_COLUMN`('cloud.console_session', 'client_address', 'VARCHAR(45)');
-- Allow default roles to use quotaCreditsList
INSERT INTO `cloud`.`role_permissions` (uuid, role_id, rule, permission, sort_order)
SELECT uuid(), role_id, 'quotaCreditsList', permission, sort_order
FROM `cloud`.`role_permissions` rp
WHERE rp.rule = 'quotaStatement'
AND NOT EXISTS(SELECT 1 FROM cloud.role_permissions rp_ WHERE rp.role_id = rp_.role_id AND rp_.rule = 'quotaCreditsList');

View File

@ -25,7 +25,7 @@ import com.cloud.utils.db.GenericDao;
public interface QuotaCreditsDao extends GenericDao<QuotaCreditsVO, Long> {
List<QuotaCreditsVO> findCredits(long accountId, long domainId, Date startDate, Date endDate);
List<QuotaCreditsVO> findCredits(Long accountId, Long domainId, Date startDate, Date endDate, boolean recursive);
QuotaCreditsVO saveCredits(QuotaCreditsVO credits);

View File

@ -16,19 +16,20 @@
//under the License.
package org.apache.cloudstack.quota.dao;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import javax.inject.Inject;
import com.cloud.domain.dao.DomainDao;
import com.cloud.utils.db.Filter;
import com.cloud.utils.db.SearchBuilder;
import org.apache.cloudstack.quota.vo.QuotaBalanceVO;
import org.apache.cloudstack.quota.vo.QuotaCreditsVO;
import org.apache.commons.lang3.ObjectUtils;
import org.springframework.stereotype.Component;
import com.cloud.utils.db.Filter;
import com.cloud.utils.db.GenericDaoBase;
import com.cloud.utils.db.QueryBuilder;
import com.cloud.utils.db.SearchCriteria;
import com.cloud.utils.db.Transaction;
import com.cloud.utils.db.TransactionCallback;
@ -39,25 +40,36 @@ import com.cloud.utils.db.TransactionStatus;
public class QuotaCreditsDaoImpl extends GenericDaoBase<QuotaCreditsVO, Long> implements QuotaCreditsDao {
@Inject
QuotaBalanceDao _quotaBalanceDao;
DomainDao domainDao;
@Inject
QuotaBalanceDao quotaBalanceDao;
private SearchBuilder<QuotaCreditsVO> quotaCreditsVoSearch;
public QuotaCreditsDaoImpl() {
quotaCreditsVoSearch = createSearchBuilder();
quotaCreditsVoSearch.and("updatedOn", quotaCreditsVoSearch.entity().getUpdatedOn(), SearchCriteria.Op.BETWEEN);
quotaCreditsVoSearch.and("accountId", quotaCreditsVoSearch.entity().getAccountId(), SearchCriteria.Op.EQ);
quotaCreditsVoSearch.and("domainId", quotaCreditsVoSearch.entity().getDomainId(), SearchCriteria.Op.IN);
quotaCreditsVoSearch.done();
}
@Override
public List<QuotaCreditsVO> findCredits(final long accountId, final long domainId, final Date startDate, final Date endDate) {
return Transaction.execute(TransactionLegacy.USAGE_DB, new TransactionCallback<List<QuotaCreditsVO>>() {
@Override
public List<QuotaCreditsVO> doInTransaction(final TransactionStatus status) {
if ((startDate != null) && (endDate != null) && startDate.before(endDate)) {
Filter filter = new Filter(QuotaCreditsVO.class, "updatedOn", true, 0L, Long.MAX_VALUE);
QueryBuilder<QuotaCreditsVO> qb = QueryBuilder.create(QuotaCreditsVO.class);
qb.and(qb.entity().getAccountId(), SearchCriteria.Op.EQ, accountId);
qb.and(qb.entity().getDomainId(), SearchCriteria.Op.EQ, domainId);
qb.and(qb.entity().getUpdatedOn(), SearchCriteria.Op.BETWEEN, startDate, endDate);
return search(qb.create(), filter);
} else {
return Collections.<QuotaCreditsVO> emptyList();
}
}
});
public List<QuotaCreditsVO> findCredits(Long accountId, Long domainId, Date startDate, Date endDate, boolean recursive) {
SearchCriteria<QuotaCreditsVO> sc = quotaCreditsVoSearch.create();
Filter filter = new Filter(QuotaCreditsVO.class, "updatedOn", true, 0L, Long.MAX_VALUE);
sc.setParametersIfNotNull("accountId", accountId);
if (domainId != null) {
List<Long> domainIds = recursive ? domainDao.getDomainAndChildrenIds(domainId) : List.of(domainId);
sc.setParameters("domainId", domainIds.toArray());
}
if (ObjectUtils.allNotNull(startDate, endDate)) {
sc.setParameters("updatedOn", startDate, endDate);
}
return Transaction.execute(TransactionLegacy.USAGE_DB, (TransactionCallback<List<QuotaCreditsVO>>) status -> search(sc, filter));
}
@Override
@ -68,7 +80,7 @@ public class QuotaCreditsDaoImpl extends GenericDaoBase<QuotaCreditsVO, Long> im
persist(credits);
// make an entry in the balance table
QuotaBalanceVO bal = new QuotaBalanceVO(credits);
_quotaBalanceDao.persist(bal);
quotaBalanceDao.persist(bal);
return credits;
}
});

View File

@ -17,6 +17,7 @@
package org.apache.cloudstack.quota.vo;
import org.apache.cloudstack.api.InternalIdentity;
import org.apache.cloudstack.utils.reflectiontostringbuilderutils.ReflectionToStringBuilderUtils;
import javax.persistence.Column;
import javax.persistence.Entity;
@ -113,4 +114,9 @@ public class QuotaCreditsVO implements InternalIdentity {
public long getId() {
return this.id;
}
@Override
public String toString() {
return ReflectionToStringBuilderUtils.reflectOnlySelectedFields(this, "id", "accountId", "domainId", "credit");
}
}

View File

@ -0,0 +1,122 @@
//Licensed to the Apache Software Foundation (ASF) under one
//or more contributor license agreements. See the NOTICE file
//distributed with this work for additional information
//regarding copyright ownership. The ASF 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.apache.cloudstack.api.command;
import com.cloud.utils.Pair;
import org.apache.cloudstack.api.ACL;
import org.apache.cloudstack.api.APICommand;
import org.apache.cloudstack.api.ApiConstants;
import org.apache.cloudstack.api.BaseCmd;
import org.apache.cloudstack.api.Parameter;
import org.apache.cloudstack.api.response.AccountResponse;
import org.apache.cloudstack.api.response.DomainResponse;
import org.apache.cloudstack.api.response.ListResponse;
import org.apache.cloudstack.api.response.QuotaCreditsResponse;
import org.apache.cloudstack.api.response.QuotaResponseBuilder;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.time.DateUtils;
import java.util.Calendar;
import java.util.Date;
import java.util.List;
import javax.inject.Inject;
@APICommand(name = "quotaCreditsList", responseObject = QuotaCreditsResponse.class, description = "Lists quota credits of an account.", since = "4.21.0",
requestHasSensitiveInfo = false, responseHasSensitiveInfo = false)
public class QuotaCreditsListCmd extends BaseCmd {
@Inject
QuotaResponseBuilder quotaResponseBuilder;
@ACL
@Parameter(name = ApiConstants.ACCOUNT_ID, type = CommandType.UUID, entityType = AccountResponse.class, description = "ID of the account for which the credit statement will be generated.")
private Long accountId;
@ACL
@Parameter(name = ApiConstants.DOMAIN_ID, type = CommandType.UUID, entityType = DomainResponse.class, description = "ID of the domain for which credit statement will be generated. " +
"Available only for administrators.")
private Long domainId;
@Parameter(name = ApiConstants.END_DATE, type = CommandType.DATE, description = "End date of the credit statement. If not provided, the current date will be " +
"considered as the end date. " + ApiConstants.PARAMETER_DESCRIPTION_END_DATE_POSSIBLE_FORMATS)
private Date endDate;
@Parameter(name = ApiConstants.START_DATE, type = CommandType.DATE, description = "Start date of the credit statement. If not provided, the first day of the current month " +
"will be considered as the start date. " + ApiConstants.PARAMETER_DESCRIPTION_START_DATE_POSSIBLE_FORMATS)
private Date startDate;
@Parameter(name = ApiConstants.IS_RECURSIVE, type = CommandType.BOOLEAN, description = "Whether to generate the credit statement for the provided domain and its children. " +
"Defaults to false.")
private Boolean recursive = false;
public Long getAccountId() {
return accountId;
}
public void setAccountId(Long accountId) {
this.accountId = accountId;
}
public Long getDomainId() {
return domainId;
}
public void setDomainId(Long domainId) {
this.domainId = domainId;
}
public Date getEndDate() {
return ObjectUtils.defaultIfNull(endDate, new Date());
}
public void setEndDate(Date endDate) {
this.endDate = endDate;
}
public Date getStartDate() {
return ObjectUtils.defaultIfNull(startDate, DateUtils.truncate(new Date(), Calendar.MONTH));
}
public void setStartDate(Date startDate) {
this.startDate = startDate;
}
public Boolean getRecursive() {
return recursive;
}
public void setRecursive(Boolean recursive) {
this.recursive = recursive;
}
@Override
public void execute() {
Pair<List<QuotaCreditsResponse>, Integer> responses = quotaResponseBuilder.createQuotaCreditsListResponse(this);
ListResponse<QuotaCreditsResponse> response = new ListResponse<>();
response.setResponses(responses.first(), responses.second());
response.setResponseName(getCommandName());
setResponseObject(response);
}
@Override
public long getEntityOwnerId() {
return -1;
}
}

View File

@ -122,8 +122,8 @@ public class QuotaBalanceResponse extends BaseResponse {
public void addCredits(QuotaBalanceVO credit) {
QuotaCreditsResponse cr = new QuotaCreditsResponse();
cr.setCredits(credit.getCreditBalance());
cr.setUpdatedOn(credit.getUpdatedOn() == null ? null : new Date(credit.getUpdatedOn().getTime()));
cr.setCredit(credit.getCreditBalance());
cr.setCreditedOn(credit.getUpdatedOn());
credits.add(0, cr);
}

View File

@ -20,65 +20,62 @@ import com.cloud.serializer.Param;
import com.google.gson.annotations.SerializedName;
import org.apache.cloudstack.api.BaseResponse;
import org.apache.cloudstack.quota.vo.QuotaCreditsVO;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.Date;
public class QuotaCreditsResponse extends BaseResponse {
@SerializedName("credits")
@Param(description = "the credit deposited")
private BigDecimal credits;
@SerializedName("credit")
@Param(description = "The credit deposited.")
private BigDecimal credit;
@SerializedName("updated_by")
@Param(description = "the user name of the admin who updated the credits")
private String updatedBy;
@SerializedName("creditoruserid")
@Param(description = "ID of the creditor user.")
private String creditorUserId;
@SerializedName("updated_on")
@Param(description = "the account name of the admin who updated the credits")
private Date updatedOn;
@SerializedName("creditorusername")
@Param(description = "Username of the creditor user.")
private String creditorUsername;
@SerializedName("creditedon")
@Param(description = "When the credit was added.")
private Date creditedOn;
@SerializedName("currency")
@Param(description = "currency")
@Param(description = "Credit's currency.")
private String currency;
public QuotaCreditsResponse() {
super();
public BigDecimal getCredit() {
return credit;
}
public QuotaCreditsResponse(QuotaCreditsVO result, String updatedBy) {
super();
if (result != null) {
setCredits(result.getCredit());
setUpdatedBy(updatedBy);
setUpdatedOn(new Date());
}
public void setCredit(BigDecimal credit) {
this.credit = credit;
}
public BigDecimal getCredits() {
return credits;
public String getCreditorUserId() {
return creditorUserId;
}
public void setCredits(BigDecimal credits) {
this.credits = credits.setScale(2, RoundingMode.HALF_EVEN);
public void setCreditorUserId(String creditorUserId) {
this.creditorUserId = creditorUserId;
}
public String getUpdatedBy() {
return updatedBy;
public String getCreditorUsername() {
return creditorUsername;
}
public void setUpdatedBy(String updatedBy) {
this.updatedBy = updatedBy;
public void setCreditorUsername(String creditorUsername) {
this.creditorUsername = creditorUsername;
}
public Date getUpdatedOn() {
return updatedOn;
public Date getCreditedOn() {
return creditedOn;
}
public void setUpdatedOn(Date updatedOn) {
this.updatedOn = updatedOn;
public void setCreditedOn(Date creditedOn) {
this.creditedOn = creditedOn;
}
public String getCurrency() {

View File

@ -19,6 +19,7 @@ package org.apache.cloudstack.api.response;
import com.cloud.user.User;
import org.apache.cloudstack.api.command.QuotaBalanceCmd;
import org.apache.cloudstack.api.command.QuotaConfigureEmailCmd;
import org.apache.cloudstack.api.command.QuotaCreditsListCmd;
import org.apache.cloudstack.api.command.QuotaEmailTemplateListCmd;
import org.apache.cloudstack.api.command.QuotaEmailTemplateUpdateCmd;
import org.apache.cloudstack.api.command.QuotaPresetVariablesListCmd;
@ -90,5 +91,7 @@ public interface QuotaResponseBuilder {
List<QuotaConfigureEmailResponse> listEmailConfiguration(long accountId);
Pair<List<QuotaCreditsResponse>, Integer> createQuotaCreditsListResponse(QuotaCreditsListCmd cmd);
QuotaValidateActivationRuleResponse validateActivationRule(QuotaValidateActivationRuleCmd cmd);
}

View File

@ -42,12 +42,16 @@ import java.util.stream.Collectors;
import javax.inject.Inject;
import com.cloud.exception.PermissionDeniedException;
import com.cloud.user.User;
import com.cloud.user.UserVO;
import com.cloud.utils.DateUtil;
import com.cloud.utils.exception.CloudRuntimeException;
import org.apache.cloudstack.api.ApiErrorCode;
import org.apache.cloudstack.api.ServerApiException;
import org.apache.cloudstack.api.command.QuotaBalanceCmd;
import org.apache.cloudstack.api.command.QuotaConfigureEmailCmd;
import org.apache.cloudstack.api.command.QuotaCreditsListCmd;
import org.apache.cloudstack.api.command.QuotaEmailTemplateListCmd;
import org.apache.cloudstack.api.command.QuotaEmailTemplateUpdateCmd;
import org.apache.cloudstack.api.command.QuotaPresetVariablesListCmd;
@ -99,7 +103,6 @@ import com.cloud.exception.InvalidParameterValueException;
import com.cloud.user.Account;
import com.cloud.user.AccountManager;
import com.cloud.user.AccountVO;
import com.cloud.user.User;
import com.cloud.user.dao.AccountDao;
import com.cloud.user.dao.UserDao;
import com.cloud.utils.Pair;
@ -116,7 +119,7 @@ public class QuotaResponseBuilderImpl implements QuotaResponseBuilder {
@Inject
private QuotaBalanceDao _quotaBalanceDao;
@Inject
private QuotaCreditsDao _quotaCreditsDao;
private QuotaCreditsDao quotaCreditsDao;
@Inject
private QuotaUsageDao _quotaUsageDao;
@Inject
@ -548,28 +551,32 @@ public class QuotaResponseBuilderImpl implements QuotaResponseBuilder {
@Override
public QuotaCreditsResponse addQuotaCredits(Long accountId, Long domainId, Double amount, Long updatedBy, Boolean enforce) {
Date despositedOn = new Date();
QuotaBalanceVO qb = _quotaBalanceDao.findLaterBalanceEntry(accountId, domainId, despositedOn);
Date depositedOn = new Date();
QuotaBalanceVO qb = _quotaBalanceDao.findLaterBalanceEntry(accountId, domainId, depositedOn);
if (qb != null) {
throw new InvalidParameterValueException(String.format("Incorrect deposit date [%s], as there are balance entries after this date.",
despositedOn));
depositedOn));
}
QuotaCreditsVO credits = new QuotaCreditsVO(accountId, domainId, new BigDecimal(amount), updatedBy);
credits.setUpdatedOn(despositedOn);
QuotaCreditsVO result = _quotaCreditsDao.saveCredits(credits);
credits.setUpdatedOn(depositedOn);
QuotaCreditsVO result = quotaCreditsDao.saveCredits(credits);
if (result == null) {
logger.error("Unable to add credits to account ID [{}].", accountId);
throw new CloudRuntimeException("Unable to add credits to account.");
}
final AccountVO account = _accountDao.findById(accountId);
if (account == null) {
throw new InvalidParameterValueException("Account does not exist with account id " + accountId);
}
final boolean lockAccountEnforcement = "true".equalsIgnoreCase(QuotaConfig.QuotaEnableEnforcement.value());
final BigDecimal currentAccountBalance = _quotaBalanceDao.lastQuotaBalance(accountId, domainId, startOfNextDay(new Date(despositedOn.getTime())));
final BigDecimal currentAccountBalance = _quotaBalanceDao.lastQuotaBalance(accountId, domainId, startOfNextDay(new Date(depositedOn.getTime())));
logger.debug("Depositing [{}] credits on adjusted date [{}]; current balance is [{}].", amount,
DateUtil.displayDateInTimezone(QuotaManagerImpl.getUsageAggregationTimeZone(), despositedOn), currentAccountBalance);
DateUtil.displayDateInTimezone(QuotaManagerImpl.getUsageAggregationTimeZone(), depositedOn), currentAccountBalance);
// update quota account with the balance
_quotaService.saveQuotaAccount(account, currentAccountBalance, despositedOn);
_quotaService.saveQuotaAccount(account, currentAccountBalance, depositedOn);
if (lockAccountEnforcement) {
if (currentAccountBalance.compareTo(new BigDecimal(0)) >= 0) {
if (account.getState() == Account.State.LOCKED) {
@ -584,14 +591,8 @@ public class QuotaResponseBuilderImpl implements QuotaResponseBuilder {
}
}
String creditor = String.valueOf(Account.ACCOUNT_ID_SYSTEM);
User creditorUser = _userDao.getUser(updatedBy);
if (creditorUser != null) {
creditor = creditorUser.getUsername();
}
QuotaCreditsResponse response = new QuotaCreditsResponse(result, creditor);
response.setCurrency(QuotaConfig.QuotaCurrencySymbol.value());
return response;
UserVO creditor = getCreditorForQuotaCredits(result);
return createQuotaCreditsResponse(result, creditor);
}
private QuotaEmailTemplateResponse createQuotaEmailResponse(QuotaEmailTemplatesVO template) {
@ -938,6 +939,91 @@ public class QuotaResponseBuilderImpl implements QuotaResponseBuilder {
return quotaConfigureEmailResponse;
}
@Override
public Pair<List<QuotaCreditsResponse>, Integer> createQuotaCreditsListResponse(QuotaCreditsListCmd cmd) {
List<QuotaCreditsVO> credits = getCreditsForQuotaCreditsList(cmd);
List<QuotaCreditsResponse> creditResponses = new ArrayList<>();
Map<Long, UserVO> userMap = new HashMap<>();
for (QuotaCreditsVO credit : credits) {
UserVO creditor = getCreditorForQuotaCreditsList(credit, userMap);
QuotaCreditsResponse response = createQuotaCreditsResponse(credit, creditor);
creditResponses.add(response);
}
return new Pair<>(creditResponses, creditResponses.size());
}
protected List<QuotaCreditsVO> getCreditsForQuotaCreditsList(QuotaCreditsListCmd cmd) {
Long accountId = cmd.getAccountId();
Long domainId = cmd.getDomainId();
Date startDate = cmd.getStartDate();
Date endDate = cmd.getEndDate();
boolean isRecursive = cmd.getRecursive();
if (ObjectUtils.allNull(accountId, domainId)) {
throw new InvalidParameterValueException("Please provide either account ID or domain ID.");
}
if (startDate.after(endDate)) {
throw new InvalidParameterValueException("The start date must be before the end date.");
}
Account caller = CallContext.current().getCallingAccount();
if (domainId != null && _accountMgr.isNormalUser(caller.getAccountId())) {
throw new PermissionDeniedException("Regular users are not allowed to generate domain statements.");
}
return quotaCreditsDao.findCredits(accountId, domainId, startDate, endDate, isRecursive);
}
/**
* Returns the creditor user of a <code>QuotaCreditsVO</code>. If <code>userMap</code> contains the user, returns the
* user from the map; otherwise, obtains the user from the database and adds it to the map.
*/
protected UserVO getCreditorForQuotaCreditsList(QuotaCreditsVO credit, Map<Long, UserVO> userMap) {
Long creditorUserId = credit.getUpdatedBy();
UserVO userVo = userMap.get(creditorUserId);
if (userVo != null) {
return userVo;
}
userVo = getCreditorForQuotaCredits(credit);
userMap.put(creditorUserId, userVo);
return userVo;
}
/**
* Returns the creditor user of a <code>QuotaCreditsVO</code> by obtaining it from the database.
*/
protected UserVO getCreditorForQuotaCredits(QuotaCreditsVO credit) {
Long creditorUserId = credit.getUpdatedBy();
UserVO userVo = _userDao.findByIdIncludingRemoved(creditorUserId);
if (userVo == null) {
logger.error("Could not find creditor user with ID [{}] for credit [{}].", creditorUserId, credit.toString());
throw new CloudRuntimeException("Could not find creditor user.");
}
return userVo;
}
protected QuotaCreditsResponse createQuotaCreditsResponse(QuotaCreditsVO credit, UserVO creditor) {
QuotaCreditsResponse response = new QuotaCreditsResponse();
if (credit != null) {
response.setCredit(credit.getCredit());
response.setCreditedOn(credit.getUpdatedOn());
response.setCurrency(QuotaConfig.QuotaCurrencySymbol.value());
}
if (creditor != null) {
response.setCreditorUserId(creditor.getUuid());
response.setCreditorUsername(creditor.getUsername());
}
response.setObjectName("credit");
return response;
}
@Override
public QuotaValidateActivationRuleResponse validateActivationRule(QuotaValidateActivationRuleCmd cmd) {
String message;

View File

@ -29,6 +29,7 @@ import javax.naming.ConfigurationException;
import org.apache.cloudstack.api.command.QuotaBalanceCmd;
import org.apache.cloudstack.api.command.QuotaConfigureEmailCmd;
import org.apache.cloudstack.api.command.QuotaCreditsCmd;
import org.apache.cloudstack.api.command.QuotaCreditsListCmd;
import org.apache.cloudstack.api.command.QuotaEmailTemplateListCmd;
import org.apache.cloudstack.api.command.QuotaEmailTemplateUpdateCmd;
import org.apache.cloudstack.api.command.QuotaEnabledCmd;
@ -115,6 +116,7 @@ public class QuotaServiceImpl extends ManagerBase implements QuotaService, Confi
cmdList.add(QuotaTariffListCmd.class);
cmdList.add(QuotaTariffUpdateCmd.class);
cmdList.add(QuotaCreditsCmd.class);
cmdList.add(QuotaCreditsListCmd.class);
cmdList.add(QuotaEmailTemplateListCmd.class);
cmdList.add(QuotaEmailTemplateUpdateCmd.class);
cmdList.add(QuotaTariffCreateCmd.class);

View File

@ -0,0 +1,79 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF 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.apache.cloudstack.api.command;
import java.util.Calendar;
import java.util.Date;
import org.apache.commons.lang3.time.DateUtils;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.MockedConstruction;
import org.mockito.Mockito;
import org.mockito.Spy;
import org.mockito.junit.MockitoJUnitRunner;
@RunWith(MockitoJUnitRunner.class)
public class QuotaCreditsListCmdTest {
@Spy
QuotaCreditsListCmd quotaCreditsListCmdSpy;
@Test
public void getEndDateTestReturnsNewDateWhenEndDateIsNull() {
quotaCreditsListCmdSpy.setEndDate(null);
try (MockedConstruction<Date> dateMockedConstruction = Mockito.mockConstruction(Date.class)) {
Date result = quotaCreditsListCmdSpy.getEndDate();
Assert.assertEquals(1, dateMockedConstruction.constructed().size());
Assert.assertEquals(dateMockedConstruction.constructed().get(0), result);
}
}
@Test
public void getEndDateTestReturnsEndDateWhenItIsNotNull() {
Date expected = new Date();
quotaCreditsListCmdSpy.setEndDate(expected);
Date result = quotaCreditsListCmdSpy.getEndDate();
Assert.assertEquals(expected, result);
}
@Test
public void getStartDateTestReturnsFirstDayOfTheCurrentMonthWhenStartDateIsNull() {
quotaCreditsListCmdSpy.setStartDate(null);
Date expected = DateUtils.truncate(new Date(), Calendar.MONTH);
Date result = quotaCreditsListCmdSpy.getStartDate();
Assert.assertEquals(expected, result);
}
@Test
public void getStartDateTestReturnsStartDateWhenItIsNotNull() {
Date expected = new Date();
quotaCreditsListCmdSpy.setStartDate(expected);
Date result = quotaCreditsListCmdSpy.getStartDate();
Assert.assertEquals(expected, result);
}
}

View File

@ -24,6 +24,7 @@ import java.time.ZoneId;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
@ -32,12 +33,18 @@ import java.util.function.Consumer;
import com.cloud.domain.DomainVO;
import com.cloud.domain.dao.DomainDao;
import com.cloud.exception.PermissionDeniedException;
import com.cloud.user.AccountManager;
import com.cloud.user.UserVO;
import com.cloud.utils.Pair;
import com.cloud.utils.exception.CloudRuntimeException;
import org.apache.cloudstack.api.ServerApiException;
import org.apache.cloudstack.api.command.QuotaConfigureEmailCmd;
import org.apache.cloudstack.api.command.QuotaCreditsListCmd;
import org.apache.cloudstack.api.command.QuotaEmailTemplateListCmd;
import org.apache.cloudstack.api.command.QuotaEmailTemplateUpdateCmd;
import org.apache.cloudstack.api.command.QuotaValidateActivationRuleCmd;
import org.apache.cloudstack.context.CallContext;
import org.apache.cloudstack.discovery.ApiDiscoveryService;
import org.apache.cloudstack.framework.config.ConfigKey;
import org.apache.cloudstack.jsinterpreter.JsInterpreterHelper;
@ -66,6 +73,7 @@ import org.apache.cloudstack.utils.jsinterpreter.JsInterpreter;
import org.apache.commons.lang3.time.DateUtils;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
@ -81,6 +89,7 @@ import com.cloud.user.dao.UserDao;
import com.cloud.user.User;
import junit.framework.TestCase;
import org.mockito.Spy;
import org.mockito.junit.MockitoJUnitRunner;
@RunWith(MockitoJUnitRunner.class)
@ -135,7 +144,8 @@ public class QuotaResponseBuilderImplTest extends TestCase {
QuotaEmailConfigurationDao quotaEmailConfigurationDaoMock;
@InjectMocks
QuotaResponseBuilderImpl quotaResponseBuilderSpy = Mockito.spy(QuotaResponseBuilderImpl.class);
@Spy
QuotaResponseBuilderImpl quotaResponseBuilderSpy;
Date date = new Date();
@ -154,6 +164,26 @@ public class QuotaResponseBuilderImplTest extends TestCase {
@Mock
QuotaEmailTemplatesVO quotaEmailTemplatesVoMock;
@Mock
QuotaCreditsVO quotaCreditsVoMock;
@Mock
UserVO userVoMock;
@Mock
AccountManager accountManagerMock;
@Mock
Account callerAccountMock;
@Mock
User callerUserMock;
@Before
public void setup() {
CallContext.register(callerUserMock, callerAccountMock);
}
private void overrideDefaultQuotaEnabledConfigValue(final Object value) throws IllegalAccessException, NoSuchFieldException {
Field f = ConfigKey.class.getDeclaredField("_defaultValue");
f.setAccessible(true);
@ -221,13 +251,14 @@ public class QuotaResponseBuilderImplTest extends TestCase {
Mockito.when(quotaCreditsDaoMock.saveCredits(Mockito.any(QuotaCreditsVO.class))).thenReturn(credit);
Mockito.when(quotaBalanceDaoMock.lastQuotaBalance(Mockito.anyLong(), Mockito.anyLong(), Mockito.any(Date.class))).thenReturn(new BigDecimal(111));
Mockito.doReturn(userVoMock).when(quotaResponseBuilderSpy).getCreditorForQuotaCredits(credit);
AccountVO account = new AccountVO();
account.setState(Account.State.LOCKED);
Mockito.when(accountDaoMock.findById(Mockito.anyLong())).thenReturn(account);
QuotaCreditsResponse resp = quotaResponseBuilderSpy.addQuotaCredits(accountId, domainId, amount, updatedBy, true);
assertTrue(resp.getCredits().compareTo(credit.getCredit()) == 0);
assertTrue(resp.getCredit().compareTo(credit.getCredit()) == 0);
}
@Test
@ -658,6 +689,136 @@ public class QuotaResponseBuilderImplTest extends TestCase {
assertFalse(quotaResponseBuilderSpy.isUserAllowedToSeeActivationRules(userMock));
}
@Test
public void createQuotaCreditsListResponseTestReturnsObject() {
List<QuotaCreditsVO> credits = new ArrayList<>();
credits.add(new QuotaCreditsVO());
QuotaCreditsResponse expectedQuotaCreditsResponse = new QuotaCreditsResponse();
Mockito.doReturn(credits).when(quotaResponseBuilderSpy).getCreditsForQuotaCreditsList(Mockito.any());
Mockito.doReturn(userVoMock).when(quotaResponseBuilderSpy).getCreditorForQuotaCreditsList(Mockito.any(), Mockito.any());
Mockito.doReturn(expectedQuotaCreditsResponse).when(quotaResponseBuilderSpy).createQuotaCreditsResponse(credits.get(0), userVoMock);
Pair<List<QuotaCreditsResponse>, Integer> result = quotaResponseBuilderSpy.createQuotaCreditsListResponse(createQuotaCreditsListCmdForTests());
Assert.assertEquals(expectedQuotaCreditsResponse, result.first().get(0));
Assert.assertEquals(1, (int) result.second());
}
private QuotaCreditsListCmd createQuotaCreditsListCmdForTests() {
Mockito.doReturn(false).when(accountManagerMock).isNormalUser(Mockito.anyLong());
QuotaCreditsListCmd cmd = new QuotaCreditsListCmd();
cmd.setAccountId(1L);
cmd.setDomainId(2L);
return cmd;
}
@Test(expected = InvalidParameterValueException.class)
public void getCreditsForQuotaCreditsListTestThrowsInvalidParameterValueExceptionWhenBothAccountIdAndDomainIdAreNull() {
QuotaCreditsListCmd cmd = new QuotaCreditsListCmd();
quotaResponseBuilderSpy.getCreditsForQuotaCreditsList(cmd);
}
@Test(expected = InvalidParameterValueException.class)
public void getCreditsForQuotaCreditsListTestThrowsInvalidParameterValueExceptionWhenStartDateIsAfterEndDate() {
QuotaCreditsListCmd cmd = createQuotaCreditsListCmdForTests();
cmd.setStartDate(new Date());
cmd.setEndDate(DateUtils.addDays(new Date(), -1));
quotaResponseBuilderSpy.getCreditsForQuotaCreditsList(cmd);
}
@Test(expected = PermissionDeniedException.class)
public void getCreditsForQuotaCreditsListTestThrowsPermissionDeniedExceptionWhenDomainIdIsProvidedAndCallerIsNormalUser() {
QuotaCreditsListCmd cmd = createQuotaCreditsListCmdForTests();
Mockito.doReturn(true).when(accountManagerMock).isNormalUser(Mockito.anyLong());
quotaResponseBuilderSpy.getCreditsForQuotaCreditsList(cmd);
}
@Test
public void getCreditsForQuotaCreditsListTestReturnsData() {
QuotaCreditsListCmd cmd = createQuotaCreditsListCmdForTests();
List<QuotaCreditsVO> expected = new ArrayList<>();
expected.add(new QuotaCreditsVO());
Mockito.doReturn(expected).when(quotaCreditsDaoMock).findCredits(Mockito.anyLong(), Mockito.anyLong(), Mockito.any(), Mockito.any(), Mockito.anyBoolean());
List<QuotaCreditsVO> result = quotaResponseBuilderSpy.getCreditsForQuotaCreditsList(cmd);
Assert.assertEquals(expected, result);
}
@Test
public void getCreditorForQuotaCreditsListTestReturnsUserFromMapWhenMapHasCreditor() {
Long creditorId = 1L;
Map<Long, UserVO> userMap = new HashMap<>();
userMap.put(creditorId, userVoMock);
Mockito.doReturn(creditorId).when(quotaCreditsVoMock).getUpdatedBy();
UserVO result = quotaResponseBuilderSpy.getCreditorForQuotaCreditsList(quotaCreditsVoMock, userMap);
Assert.assertEquals(userVoMock, result);
}
@Test
public void getCreditorForQuotaCreditsListTestGetsCreditorFromDatabaseAndAddsItToMapWhenMapDoesNotHaveCreditor() {
Long creditorId = 1L;
Map<Long, UserVO> userMap = new HashMap<>();
Mockito.doReturn(creditorId).when(quotaCreditsVoMock).getUpdatedBy();
Mockito.doReturn(userVoMock).when(userDaoMock).findByIdIncludingRemoved(creditorId);
UserVO result = quotaResponseBuilderSpy.getCreditorForQuotaCreditsList(quotaCreditsVoMock, userMap);
Assert.assertEquals(userVoMock, result);
Assert.assertEquals(userVoMock, userMap.get(creditorId));
}
@Test
public void getCreditorForQuotaCreditsTestReturnsCreditorWhenCreditorExists() {
Long creditorId = 1L;
Mockito.when(quotaCreditsVoMock.getUpdatedBy()).thenReturn(creditorId);
Mockito.doReturn(userVoMock).when(userDaoMock).findByIdIncludingRemoved(creditorId);
UserVO result = quotaResponseBuilderSpy.getCreditorForQuotaCredits(quotaCreditsVoMock);
Assert.assertEquals(userVoMock, result);
}
@Test(expected = CloudRuntimeException.class)
public void getCreditorForQuotaCreditsTestThrowsCloudRuntimeExceptionWhenCreditorDoesNotExist() {
quotaResponseBuilderSpy.getCreditorForQuotaCredits(quotaCreditsVoMock);
}
@Test
public void createQuotaCreditsResponseTestReturnsObject() {
QuotaCreditsResponse expected = new QuotaCreditsResponse();
expected.setCreditorUserId("test_uuid");
expected.setCreditorUsername("test_name");
expected.setCredit(new BigDecimal(41.5));
expected.setCreditedOn(new Date());
expected.setCurrency(QuotaConfig.QuotaCurrencySymbol.value());
expected.setObjectName("credit");
Mockito.when(userVoMock.getUuid()).thenReturn(expected.getCreditorUserId());
Mockito.when(userVoMock.getUsername()).thenReturn(expected.getCreditorUsername());
Mockito.when(quotaCreditsVoMock.getCredit()).thenReturn(expected.getCredit());
Mockito.when(quotaCreditsVoMock.getUpdatedOn()).thenReturn(expected.getCreditedOn());
QuotaCreditsResponse result = quotaResponseBuilderSpy.createQuotaCreditsResponse(quotaCreditsVoMock, userVoMock);
Assert.assertEquals(expected.getCreditorUserId(), result.getCreditorUserId());
Assert.assertEquals(expected.getCreditorUsername(), result.getCreditorUsername());
Assert.assertEquals(expected.getCredit(), result.getCredit());
Assert.assertEquals(expected.getCreditedOn(), result.getCreditedOn());
Assert.assertEquals(expected.getCurrency(), result.getCurrency());
Assert.assertEquals(expected.getObjectName(), result.getObjectName());
}
@Test
public void validateActivationRuleTestValidateActivationRuleReturnValidScriptResponse() {
Mockito.doReturn("if (account.name == 'test') { true } else { false }").when(quotaValidateActivationRuleCmdMock).getActivationRule();