Refactor Quota balance (#12961)

This commit is contained in:
Fabricio Duarte 2026-05-26 10:36:04 -03:00 committed by GitHub
parent 7f849e9362
commit 3f6866d70a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 450 additions and 561 deletions

View File

@ -69,6 +69,8 @@ public class ApiConstants {
public static final String BACKUP_VM_OFFERING_REMOVED = "vmbackupofferingremoved";
public static final String IS_BACKUP_VM_EXPUNGED = "isbackupvmexpunged";
public static final String BACKUP_TOTAL = "backuptotal";
public static final String BALANCE = "balance";
public static final String BALANCES = "balances";
public static final String BASE64_IMAGE = "base64image";
public static final String BGP_PEERS = "bgppeers";
public static final String BGP_PEER_IDS = "bgppeerids";
@ -171,6 +173,7 @@ public class ApiConstants {
public static final String DATACENTER_NAME = "datacentername";
public static final String DATADISKS_DETAILS = "datadisksdetails";
public static final String DATADISK_OFFERING_LIST = "datadiskofferinglist";
public static final String DATE = "date";
public static final String DEFAULT_VALUE = "defaultvalue";
public static final String DELETE_PROTECTION = "deleteprotection";
public static final String DESCRIPTION = "description";

View File

@ -150,8 +150,9 @@ public class QuotaManagerImpl extends ManagerBase implements QuotaManager {
return;
}
Date startDate = accountQuotaUsages.get(0).getStartDate();
Date endDate = accountQuotaUsages.get(0).getEndDate();
QuotaUsageVO firstQuotaUsage = accountQuotaUsages.get(0);
Date startDate = firstQuotaUsage.getStartDate();
Date endDate = firstQuotaUsage.getEndDate();
Date lastQuotaUsageEndDate = accountQuotaUsages.get(accountQuotaUsages.size() - 1).getEndDate();
LinkedHashSet<Pair<Date, Date>> periods = accountQuotaUsages.stream()
@ -215,7 +216,7 @@ public class QuotaManagerImpl extends ManagerBase implements QuotaManager {
logger.debug(String.format("Persisting the first quota balance [%s] for account [%s].", firstBalance, accountToString));
_quotaBalanceDao.saveQuotaBalance(firstBalance);
} else {
QuotaBalanceVO lastRealBalance = _quotaBalanceDao.findLastBalanceEntry(accountId, domainId, startDate);
QuotaBalanceVO lastRealBalance = _quotaBalanceDao.getLastQuotaBalanceEntry(accountId, domainId, startDate);
if (lastRealBalance == null) {
logger.warn("Account [{}] has quota usage entries, however it does not have a quota balance.", accountToString);
@ -244,7 +245,7 @@ public class QuotaManagerImpl extends ManagerBase implements QuotaManager {
}
protected BigDecimal aggregateCreditBetweenDates(Long accountId, Long domainId, Date startDate, Date endDate, String accountToString) {
List<QuotaBalanceVO> creditsReceived = _quotaBalanceDao.findCreditBalance(accountId, domainId, startDate, endDate);
List<QuotaBalanceVO> creditsReceived = _quotaBalanceDao.findCreditBalances(accountId, domainId, startDate, endDate);
logger.debug("Account [{}] has [{}] credit entries before [{}].", accountToString, creditsReceived.size(),
DateUtil.displayDateInTimezone(usageAggregationTimeZone, endDate));

View File

@ -28,16 +28,14 @@ public interface QuotaBalanceDao extends GenericDao<QuotaBalanceVO, Long> {
QuotaBalanceVO saveQuotaBalance(QuotaBalanceVO qb);
List<QuotaBalanceVO> findCreditBalance(Long accountId, Long domainId, Date startDate, Date endDate);
List<QuotaBalanceVO> findCreditBalances(Long accountId, Long domainId, Date startDate, Date endDate);
QuotaBalanceVO findLastBalanceEntry(Long accountId, Long domainId, Date beforeThis);
QuotaBalanceVO getLastQuotaBalanceEntry(Long accountId, Long domainId, Date beforeThis);
QuotaBalanceVO findLaterBalanceEntry(Long accountId, Long domainId, Date afterThis);
List<QuotaBalanceVO> findQuotaBalance(Long accountId, Long domainId, Date startDate, Date endDate);
List<QuotaBalanceVO> listQuotaBalances(Long accountId, Long domainId, Date startDate, Date endDate);
List<QuotaBalanceVO> lastQuotaBalanceVO(Long accountId, Long domainId, Date startDate);
BigDecimal lastQuotaBalance(Long accountId, Long domainId, Date startDate);
BigDecimal getLastQuotaBalance(Long accountId, Long domainId);
}

View File

@ -18,11 +18,14 @@ package org.apache.cloudstack.quota.dao;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import org.apache.cloudstack.quota.vo.QuotaBalanceVO;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.time.DateUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.springframework.stereotype.Component;
import com.cloud.utils.db.Filter;
@ -32,160 +35,104 @@ import com.cloud.utils.db.SearchCriteria;
import com.cloud.utils.db.Transaction;
import com.cloud.utils.db.TransactionCallback;
import com.cloud.utils.db.TransactionLegacy;
import com.cloud.utils.db.TransactionStatus;
@Component
public class QuotaBalanceDaoImpl extends GenericDaoBase<QuotaBalanceVO, Long> implements QuotaBalanceDao {
private static final Logger logger = LogManager.getLogger(QuotaBalanceDaoImpl.class);
@Override
public QuotaBalanceVO findLastBalanceEntry(final Long accountId, final Long domainId, final Date beforeThis) {
return Transaction.execute(TransactionLegacy.USAGE_DB, new TransactionCallback<QuotaBalanceVO>() {
@Override
public QuotaBalanceVO doInTransaction(final TransactionStatus status) {
List<QuotaBalanceVO> quotaBalanceEntries = new ArrayList<>();
Filter filter = new Filter(QuotaBalanceVO.class, "updatedOn", false, 0L, 1L);
QueryBuilder<QuotaBalanceVO> qb = QueryBuilder.create(QuotaBalanceVO.class);
qb.and(qb.entity().getAccountId(), SearchCriteria.Op.EQ, accountId);
qb.and(qb.entity().getDomainId(), SearchCriteria.Op.EQ, domainId);
qb.and(qb.entity().getCreditsId(), SearchCriteria.Op.EQ, 0);
public QuotaBalanceVO getLastQuotaBalanceEntry(final Long accountId, final Long domainId, final Date beforeThis) {
return Transaction.execute(TransactionLegacy.USAGE_DB, (TransactionCallback<QuotaBalanceVO>) status -> {
Filter filter = new Filter(QuotaBalanceVO.class, "updatedOn", false, 0L, 1L);
QueryBuilder<QuotaBalanceVO> qb = getQuotaBalanceQueryBuilder(accountId, domainId);
qb.and(qb.entity().getCreditsId(), SearchCriteria.Op.EQ, 0);
if (beforeThis != null) {
qb.and(qb.entity().getUpdatedOn(), SearchCriteria.Op.LT, beforeThis);
quotaBalanceEntries = search(qb.create(), filter);
return !quotaBalanceEntries.isEmpty() ? quotaBalanceEntries.get(0) : null;
}
List<QuotaBalanceVO> quotaBalanceEntries = search(qb.create(), filter);
return !quotaBalanceEntries.isEmpty() ? quotaBalanceEntries.get(0) : null;
});
}
@Override
public QuotaBalanceVO findLaterBalanceEntry(final Long accountId, final Long domainId, final Date afterThis) {
return Transaction.execute(TransactionLegacy.USAGE_DB, new TransactionCallback<QuotaBalanceVO>() {
@Override
public QuotaBalanceVO doInTransaction(final TransactionStatus status) {
List<QuotaBalanceVO> quotaBalanceEntries = new ArrayList<>();
Filter filter = new Filter(QuotaBalanceVO.class, "updatedOn", true, 0L, 1L);
QueryBuilder<QuotaBalanceVO> qb = QueryBuilder.create(QuotaBalanceVO.class);
qb.and(qb.entity().getAccountId(), SearchCriteria.Op.EQ, accountId);
qb.and(qb.entity().getDomainId(), SearchCriteria.Op.EQ, domainId);
qb.and(qb.entity().getCreditsId(), SearchCriteria.Op.EQ, 0);
qb.and(qb.entity().getUpdatedOn(), SearchCriteria.Op.GT, afterThis);
quotaBalanceEntries = search(qb.create(), filter);
return quotaBalanceEntries.size() > 0 ? quotaBalanceEntries.get(0) : null;
}
return Transaction.execute(TransactionLegacy.USAGE_DB, (TransactionCallback<QuotaBalanceVO>) status -> {
Filter filter = new Filter(QuotaBalanceVO.class, "updatedOn", true, 0L, 1L);
QueryBuilder<QuotaBalanceVO> qb = getQuotaBalanceQueryBuilder(accountId, domainId);
qb.and(qb.entity().getCreditsId(), SearchCriteria.Op.EQ, 0);
qb.and(qb.entity().getUpdatedOn(), SearchCriteria.Op.GT, afterThis);
List<QuotaBalanceVO> quotaBalanceEntries = search(qb.create(), filter);
return !quotaBalanceEntries.isEmpty() ? quotaBalanceEntries.get(0) : null;
});
}
@Override
public QuotaBalanceVO saveQuotaBalance(final QuotaBalanceVO qb) {
return Transaction.execute(TransactionLegacy.USAGE_DB, new TransactionCallback<QuotaBalanceVO>() {
@Override
public QuotaBalanceVO doInTransaction(final TransactionStatus status) {
return persist(qb);
return Transaction.execute(TransactionLegacy.USAGE_DB, (TransactionCallback<QuotaBalanceVO>) status -> persist(qb));
}
@Override
public List<QuotaBalanceVO> findCreditBalances(final Long accountId, final Long domainId, final Date startDate, final Date endDate) {
return Transaction.execute(TransactionLegacy.USAGE_DB, (TransactionCallback<List<QuotaBalanceVO>>) status -> {
if (ObjectUtils.anyNull(startDate, endDate) || startDate.after(endDate)) {
return new ArrayList<>();
}
Filter filter = new Filter(QuotaBalanceVO.class, "updatedOn", true, 0L, Long.MAX_VALUE);
QueryBuilder<QuotaBalanceVO> qb = getQuotaBalanceQueryBuilder(accountId, domainId);
qb.and(qb.entity().getCreditsId(), SearchCriteria.Op.GT, 0);
qb.and(qb.entity().getUpdatedOn(), SearchCriteria.Op.BETWEEN, startDate, endDate);
return search(qb.create(), filter);
});
}
@Override
public List<QuotaBalanceVO> findCreditBalance(final Long accountId, final Long domainId, final Date lastbalancedate, final Date beforeThis) {
return Transaction.execute(TransactionLegacy.USAGE_DB, new TransactionCallback<List<QuotaBalanceVO>>() {
@Override
public List<QuotaBalanceVO> doInTransaction(final TransactionStatus status) {
if ((lastbalancedate != null) && (beforeThis != null) && lastbalancedate.before(beforeThis)) {
Filter filter = new Filter(QuotaBalanceVO.class, "updatedOn", true, 0L, Long.MAX_VALUE);
QueryBuilder<QuotaBalanceVO> qb = QueryBuilder.create(QuotaBalanceVO.class);
qb.and(qb.entity().getAccountId(), SearchCriteria.Op.EQ, accountId);
qb.and(qb.entity().getDomainId(), SearchCriteria.Op.EQ, domainId);
qb.and(qb.entity().getCreditsId(), SearchCriteria.Op.GT, 0);
qb.and(qb.entity().getUpdatedOn(), SearchCriteria.Op.BETWEEN, lastbalancedate, beforeThis);
return search(qb.create(), filter);
} else {
return new ArrayList<QuotaBalanceVO>();
}
public List<QuotaBalanceVO> listQuotaBalances(final Long accountId, final Long domainId, final Date startDate, final Date endDate) {
return Transaction.execute(TransactionLegacy.USAGE_DB, (TransactionCallback<List<QuotaBalanceVO>>) status -> {
QueryBuilder<QuotaBalanceVO> qb = getQuotaBalanceQueryBuilder(accountId, domainId);
qb.and(qb.entity().getCreditsId(), SearchCriteria.Op.EQ, 0);
if (startDate != null) {
qb.and(qb.entity().getUpdatedOn(), SearchCriteria.Op.GTEQ, startDate);
}
if (endDate != null) {
qb.and(qb.entity().getUpdatedOn(), SearchCriteria.Op.LTEQ, endDate);
}
Filter filter = new Filter(QuotaBalanceVO.class, "updatedOn", true);
return listBy(qb.create(), filter);
});
}
@Override
public List<QuotaBalanceVO> findQuotaBalance(final Long accountId, final Long domainId, final Date startDate, final Date endDate) {
return Transaction.execute(TransactionLegacy.USAGE_DB, new TransactionCallback<List<QuotaBalanceVO>>() {
@Override
public List<QuotaBalanceVO> doInTransaction(final TransactionStatus status) {
List<QuotaBalanceVO> quotaUsageRecords = null;
QueryBuilder<QuotaBalanceVO> qb = QueryBuilder.create(QuotaBalanceVO.class);
if (accountId != null) {
qb.and(qb.entity().getAccountId(), SearchCriteria.Op.EQ, accountId);
}
if (domainId != null) {
qb.and(qb.entity().getDomainId(), SearchCriteria.Op.EQ, domainId);
}
if ((startDate != null) && (endDate != null) && startDate.before(endDate)) {
qb.and(qb.entity().getUpdatedOn(), SearchCriteria.Op.BETWEEN, startDate, endDate);
} else {
return Collections.<QuotaBalanceVO> emptyList();
}
quotaUsageRecords = listBy(qb.create());
if (quotaUsageRecords.size() == 0) {
quotaUsageRecords.addAll(lastQuotaBalanceVO(accountId, domainId, startDate));
}
return quotaUsageRecords;
}
});
}
@Override
public List<QuotaBalanceVO> lastQuotaBalanceVO(final Long accountId, final Long domainId, final Date pivotDate) {
return Transaction.execute(TransactionLegacy.USAGE_DB, new TransactionCallback<List<QuotaBalanceVO>>() {
@Override
public List<QuotaBalanceVO> doInTransaction(final TransactionStatus status) {
List<QuotaBalanceVO> quotaUsageRecords = null;
List<QuotaBalanceVO> trimmedRecords = new ArrayList<QuotaBalanceVO>();
Filter filter = new Filter(QuotaBalanceVO.class, "updatedOn", false, 0L, 100L);
// ASSUMPTION there will be less than 100 continuous credit
// transactions
QueryBuilder<QuotaBalanceVO> qb = QueryBuilder.create(QuotaBalanceVO.class);
if (accountId != null) {
qb.and(qb.entity().getAccountId(), SearchCriteria.Op.EQ, accountId);
}
if (domainId != null) {
qb.and(qb.entity().getDomainId(), SearchCriteria.Op.EQ, domainId);
}
if ((pivotDate != null)) {
qb.and(qb.entity().getUpdatedOn(), SearchCriteria.Op.LTEQ, pivotDate);
}
quotaUsageRecords = search(qb.create(), filter);
// get records before startDate to find start balance
for (QuotaBalanceVO entry : quotaUsageRecords) {
if (logger.isDebugEnabled()) {
logger.debug("FindQuotaBalance Entry=" + entry);
}
if (entry.getCreditsId() > 0) {
trimmedRecords.add(entry);
} else {
trimmedRecords.add(entry);
break; // add only consecutive credit entries and last balance entry
}
}
return trimmedRecords;
}
});
}
@Override
public BigDecimal lastQuotaBalance(final Long accountId, final Long domainId, Date startDate) {
List<QuotaBalanceVO> quotaBalance = lastQuotaBalanceVO(accountId, domainId, startDate);
BigDecimal finalBalance = new BigDecimal(0);
if (quotaBalance.isEmpty()) {
logger.info("There are no balance entries on or before the requested date.");
return finalBalance;
public BigDecimal getLastQuotaBalance(final Long accountId, final Long domainId) {
QuotaBalanceVO quotaBalance = getLastQuotaBalanceEntry(accountId, domainId, null);
BigDecimal finalBalance = BigDecimal.ZERO;
Date startDate = DateUtils.addDays(new Date(), -1);
if (quotaBalance == null) {
logger.info("There are no balance entries for account [{}] and domain [{}]. Considering only new added credits.", accountId, domainId);
} else {
finalBalance = quotaBalance.getCreditBalance();
startDate = quotaBalance.getUpdatedOn();
}
for (QuotaBalanceVO entry : quotaBalance) {
if (logger.isDebugEnabled()) {
logger.debug("lastQuotaBalance Entry=" + entry);
}
finalBalance = finalBalance.add(entry.getCreditBalance());
List<QuotaBalanceVO> credits = findCreditBalances(accountId, domainId, startDate, new Date());
for (QuotaBalanceVO credit : credits) {
finalBalance = finalBalance.add(credit.getCreditBalance());
}
return finalBalance;
}
private QueryBuilder<QuotaBalanceVO> getQuotaBalanceQueryBuilder(Long accountId, Long domainId) {
QueryBuilder<QuotaBalanceVO> qb = QueryBuilder.create(QuotaBalanceVO.class);
qb.and(qb.entity().getAccountId(), SearchCriteria.Op.EQ, accountId);
qb.and(qb.entity().getDomainId(), SearchCriteria.Op.EQ, domainId);
return qb;
}
}

View File

@ -0,0 +1,91 @@
// 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.quota.dao;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Arrays;
import org.apache.cloudstack.quota.vo.QuotaBalanceVO;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.junit.MockitoJUnitRunner;
@RunWith(MockitoJUnitRunner.class)
public class QuotaBalanceDaoImplTest {
QuotaBalanceDaoImpl quotaBalanceDaoImplSpy = Mockito.spy(QuotaBalanceDaoImpl.class);
@Mock
QuotaBalanceVO quotaBalanceVoMock;
@Test
public void getLastQuotaBalanceTestLastEntryIsNullAndNoCreditsReturnsZero() {
Mockito.doReturn(null).when(quotaBalanceDaoImplSpy).getLastQuotaBalanceEntry(Mockito.anyLong(), Mockito.anyLong(), Mockito.any());
Mockito.doReturn(new ArrayList<>()).when(quotaBalanceDaoImplSpy).findCreditBalances(Mockito.anyLong(), Mockito.anyLong(), Mockito.any(), Mockito.any());
BigDecimal result = quotaBalanceDaoImplSpy.getLastQuotaBalance(1L, 2L);
Assert.assertEquals(BigDecimal.ZERO, result);
}
@Test
public void getLastQuotaBalanceTestReturnsLastEntryAndNoCredits() {
BigDecimal expected = BigDecimal.valueOf(-1542.46);
Mockito.doReturn(quotaBalanceVoMock).when(quotaBalanceDaoImplSpy).getLastQuotaBalanceEntry(Mockito.anyLong(), Mockito.anyLong(), Mockito.any());
Mockito.doReturn(expected).when(quotaBalanceVoMock).getCreditBalance();
Mockito.doReturn(new ArrayList<>()).when(quotaBalanceDaoImplSpy).findCreditBalances(Mockito.anyLong(), Mockito.anyLong(), Mockito.any(), Mockito.any());
BigDecimal result = quotaBalanceDaoImplSpy.getLastQuotaBalance(5L, 8L);
Assert.assertEquals(expected, result);
}
@Test
public void getLastQuotaBalanceTestReturnsLastEntryPlusCredits() {
BigDecimal balance = BigDecimal.valueOf(-1542.46);
BigDecimal credit1 = new BigDecimal("150.14");
BigDecimal credit2 = new BigDecimal("78.96");
BigDecimal expected = balance.add(credit1).add(credit2);
Mockito.doReturn(quotaBalanceVoMock).when(quotaBalanceDaoImplSpy).getLastQuotaBalanceEntry(Mockito.anyLong(), Mockito.anyLong(), Mockito.any());
Mockito.doReturn(balance, credit1, credit2).when(quotaBalanceVoMock).getCreditBalance();
Mockito.doReturn(Arrays.asList(quotaBalanceVoMock, quotaBalanceVoMock)).when(quotaBalanceDaoImplSpy).findCreditBalances(Mockito.anyLong(), Mockito.anyLong(), Mockito.any(), Mockito.any());
BigDecimal result = quotaBalanceDaoImplSpy.getLastQuotaBalance(5L, 8L);
Assert.assertEquals(expected, result);
}
@Test
public void getLastQuotaBalanceTestReturnsLastEntryIsNullPlusCredits() {
BigDecimal credit1 = new BigDecimal("150.14");
BigDecimal credit2 = new BigDecimal("78.96");
BigDecimal expected = credit1.add(credit2);
Mockito.doReturn(null).when(quotaBalanceDaoImplSpy).getLastQuotaBalanceEntry(Mockito.anyLong(), Mockito.anyLong(), Mockito.any());
Mockito.doReturn(credit1, credit2).when(quotaBalanceVoMock).getCreditBalance();
Mockito.doReturn(Arrays.asList(quotaBalanceVoMock, quotaBalanceVoMock)).when(quotaBalanceDaoImplSpy).findCreditBalances(Mockito.anyLong(), Mockito.anyLong(), Mockito.any(), Mockito.any());
BigDecimal result = quotaBalanceDaoImplSpy.getLastQuotaBalance(5L, 8L);
Assert.assertEquals(expected, result);
}
}

View File

@ -17,12 +17,9 @@
package org.apache.cloudstack.api.command;
import java.util.Date;
import java.util.List;
import javax.inject.Inject;
import com.cloud.user.Account;
import org.apache.cloudstack.api.ACL;
import org.apache.cloudstack.api.APICommand;
import org.apache.cloudstack.api.ApiConstants;
@ -30,36 +27,41 @@ 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.ProjectResponse;
import org.apache.cloudstack.api.response.QuotaBalanceResponse;
import org.apache.cloudstack.api.response.QuotaResponseBuilder;
import org.apache.cloudstack.quota.vo.QuotaBalanceVO;
import org.apache.cloudstack.api.response.QuotaStatementItemResponse;
import org.apache.commons.lang3.ObjectUtils;
@APICommand(name = "quotaBalance", responseObject = QuotaStatementItemResponse.class, description = "Create a quota balance statement", since = "4.7.0", requestHasSensitiveInfo = false, responseHasSensitiveInfo = false,
@APICommand(name = "quotaBalance", responseObject = QuotaBalanceResponse.class, description = "Create Quota balance statements for the provided Accounts or Projects.", since = "4.7.0", requestHasSensitiveInfo = false, responseHasSensitiveInfo = false,
httpMethod = "GET")
public class QuotaBalanceCmd extends BaseCmd {
@Parameter(name = ApiConstants.ACCOUNT, type = CommandType.STRING, required = true, description = "Account Id for which statement needs to be generated")
@ACL
@Parameter(name = ApiConstants.ACCOUNT, type = CommandType.STRING, description = "Name of the Account for which balance statements will be generated. " +
"Deprecated, please use 'accountid' instead.")
private String accountName;
@ACL
@Parameter(name = ApiConstants.DOMAIN_ID, type = CommandType.UUID, required = true, entityType = DomainResponse.class, description = "If domain Id is given and the caller is domain admin then the statement is generated for domain.")
@Parameter(name = ApiConstants.DOMAIN_ID, type = CommandType.UUID, entityType = DomainResponse.class, description = "ID of the domain which the Account identified by 'account' belongs to." +
"Deprecated, please use 'accountid' instead.")
private Long domainId;
@Parameter(name = ApiConstants.END_DATE, type = CommandType.DATE, description = "End of the period of the Quota balance." +
ApiConstants.PARAMETER_DESCRIPTION_END_DATE_POSSIBLE_FORMATS)
@Parameter(name = ApiConstants.END_DATE, type = CommandType.DATE, description = "Date of the last Quota balance to be returned. Must be informed together with the " +
"startdate parameter, and can not be before startdate. " + ApiConstants.PARAMETER_DESCRIPTION_END_DATE_POSSIBLE_FORMATS)
private Date endDate;
@Parameter(name = ApiConstants.START_DATE, type = CommandType.DATE, description = "Start of the period of the Quota balance. " +
@Parameter(name = ApiConstants.START_DATE, type = CommandType.DATE, description = "Date of the first Quota balance to be returned. Must be before today. " +
ApiConstants.PARAMETER_DESCRIPTION_START_DATE_POSSIBLE_FORMATS)
private Date startDate;
@ACL
@Parameter(name = ApiConstants.ACCOUNT_ID, type = CommandType.UUID, entityType = AccountResponse.class, description = "List usage records for the specified Account")
@Parameter(name = ApiConstants.ACCOUNT_ID, type = CommandType.UUID, entityType = AccountResponse.class, description = "ID of the Account for which balance statements will be generated.")
private Long accountId;
@ACL
@Parameter(name = ApiConstants.PROJECT_ID, type = CommandType.UUID, entityType = ProjectResponse.class, description = "ID of the Project for which balance statements will be generated. Can not be specified with accountid.")
private Long projectId;
@Inject
QuotaResponseBuilder _responseBuilder;
@ -88,48 +90,32 @@ public class QuotaBalanceCmd extends BaseCmd {
}
public Date getEndDate() {
if (endDate == null){
return null;
}
else{
return _responseBuilder.startOfNextDay(new Date(endDate.getTime()));
}
return endDate;
}
public void setEndDate(Date endDate) {
this.endDate = endDate == null ? null : new Date(endDate.getTime());
this.endDate = endDate;
}
public Date getStartDate() {
return startDate == null ? null : new Date(startDate.getTime());
return startDate;
}
public void setStartDate(Date startDate) {
this.startDate = startDate == null ? null : new Date(startDate.getTime());
this.startDate = startDate;
}
@Override
public long getEntityOwnerId() {
if (accountId != null) {
return accountId;
if (ObjectUtils.allNull(accountId, accountName, domainId, projectId)) {
return -1;
}
Account account = _accountService.getActiveAccountByName(accountName, domainId);
if (account != null) {
return account.getAccountId();
}
return Account.ACCOUNT_ID_SYSTEM;
return _accountService.finalizeAccountId(accountId, accountName, domainId, projectId);
}
@Override
public void execute() {
List<QuotaBalanceVO> quotaUsage = _responseBuilder.getQuotaBalance(this);
QuotaBalanceResponse response;
if (endDate == null) {
response = _responseBuilder.createQuotaLastBalanceResponse(quotaUsage, getStartDate());
} else {
response = _responseBuilder.createQuotaBalanceResponse(quotaUsage, getStartDate(), new Date(endDate.getTime()));
}
QuotaBalanceResponse response = _responseBuilder.createQuotaBalanceResponse(this);
response.setResponseName(getCommandName());
setResponseObject(response);
}

View File

@ -17,137 +17,65 @@
package org.apache.cloudstack.api.response;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import com.google.gson.annotations.SerializedName;
import org.apache.cloudstack.api.ApiConstants;
import org.apache.cloudstack.api.BaseResponse;
import org.apache.cloudstack.quota.vo.QuotaBalanceVO;
import com.cloud.serializer.Param;
public class QuotaBalanceResponse extends BaseResponse {
@SerializedName("accountid")
@Param(description = "Account ID")
private Long accountId;
@SerializedName("account")
@Param(description = "Account name")
private String accountName;
@SerializedName("domain")
@Param(description = "Domain ID")
private Long domainId;
@SerializedName("startquota")
@Param(description = "Quota started with")
private BigDecimal startQuota;
@SerializedName("endquota")
@Param(description = "Quota by end of this period")
private BigDecimal endQuota;
@SerializedName("credits")
@Param(description = "List of credits made during this period")
private List<QuotaCreditsResponse> credits = null;
@SerializedName("startdate")
@Param(description = "Start date")
private Date startDate = null;
@SerializedName("enddate")
@Param(description = "End date")
private Date endDate = null;
@SerializedName("currency")
@Param(description = "Currency")
@SerializedName(ApiConstants.CURRENCY)
@Param(description = "Balance's currency.")
private String currency;
@SerializedName(ApiConstants.DATE)
@Param(description = "Balance's date.")
private Date date;
@SerializedName(ApiConstants.BALANCE)
@Param(description = "Balance's value.")
private BigDecimal balance;
@SerializedName(ApiConstants.BALANCES)
@Param(description = "List of balances in the period.")
private List<QuotaBalanceResponse> balances;
public QuotaBalanceResponse() {
super();
credits = new ArrayList<QuotaCreditsResponse>();
super("balance");
}
public Long getAccountId() {
return accountId;
public QuotaBalanceResponse(Date date, BigDecimal balance) {
super("balance");
this.date = date;
this.balance = balance;
}
public void setAccountId(Long accountId) {
this.accountId = accountId;
public void setCurrency(String currency) {
this.currency = currency;
}
public String getAccountName() {
return accountName;
}
public void setAccountName(String accountName) {
this.accountName = accountName;
}
public Long getDomainId() {
return domainId;
}
public void setDomainId(Long domainId) {
this.domainId = domainId;
}
public BigDecimal getStartQuota() {
return startQuota;
}
public void setStartQuota(BigDecimal startQuota) {
this.startQuota = startQuota.setScale(2, RoundingMode.HALF_EVEN);
}
public BigDecimal getEndQuota() {
return endQuota;
}
public void setEndQuota(BigDecimal endQuota) {
this.endQuota = endQuota.setScale(2, RoundingMode.HALF_EVEN);
}
public List<QuotaCreditsResponse> getCredits() {
return credits;
}
public void setCredits(List<QuotaCreditsResponse> credits) {
this.credits = credits;
}
public void addCredits(QuotaBalanceVO credit) {
QuotaCreditsResponse cr = new QuotaCreditsResponse();
cr.setCredit(credit.getCreditBalance());
cr.setCreditedOn(credit.getUpdatedOn());
credits.add(0, cr);
}
public Date getStartDate() {
return startDate == null ? null : new Date(startDate.getTime());
}
public void setStartDate(Date startDate) {
this.startDate = startDate == null ? null : new Date(startDate.getTime());
}
public Date getEndDate() {
return endDate == null ? null : new Date(endDate.getTime());
}
public void setEndDate(Date endDate) {
this.endDate = endDate == null ? null : new Date(endDate.getTime());
public void setBalances(List<QuotaBalanceResponse> balances) {
this.balances = balances;
}
public String getCurrency() {
return currency;
}
public void setCurrency(String currency) {
this.currency = currency;
public List<QuotaBalanceResponse> getBalances() {
return balances;
}
public Date getDate() {
return date;
}
public BigDecimal getBalance() {
return balance;
}
}

View File

@ -29,7 +29,6 @@ import org.apache.cloudstack.api.command.QuotaTariffCreateCmd;
import org.apache.cloudstack.api.command.QuotaTariffListCmd;
import org.apache.cloudstack.api.command.QuotaTariffUpdateCmd;
import org.apache.cloudstack.api.command.QuotaValidateActivationRuleCmd;
import org.apache.cloudstack.quota.vo.QuotaBalanceVO;
import org.apache.cloudstack.quota.vo.QuotaEmailConfigurationVO;
import org.apache.cloudstack.quota.vo.QuotaTariffVO;
@ -50,14 +49,10 @@ public interface QuotaResponseBuilder {
QuotaStatementResponse createQuotaStatementResponse(QuotaStatementCmd cmd);
QuotaBalanceResponse createQuotaBalanceResponse(List<QuotaBalanceVO> quotaUsage, Date startDate, Date endDate);
QuotaBalanceResponse createQuotaBalanceResponse(QuotaBalanceCmd cmd);
Pair<List<QuotaSummaryResponse>, Integer> createQuotaSummaryResponse(QuotaSummaryCmd cmd);
QuotaBalanceResponse createQuotaLastBalanceResponse(List<QuotaBalanceVO> quotaBalance, Date startDate);
List<QuotaBalanceVO> getQuotaBalance(QuotaBalanceCmd cmd);
QuotaCreditsResponse addQuotaCredits(Long accountId, Long domainId, Double amount, Long updatedBy, Boolean enforce);
List<QuotaEmailTemplateResponse> listQuotaEmailTemplates(QuotaEmailTemplateListCmd cmd);

View File

@ -26,13 +26,10 @@ import java.time.ZoneId;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
@ -329,93 +326,18 @@ public class QuotaResponseBuilderImpl implements QuotaResponseBuilder {
}
@Override
public QuotaBalanceResponse createQuotaBalanceResponse(List<QuotaBalanceVO> quotaBalance, Date startDate, Date endDate) {
if (quotaBalance == null || quotaBalance.isEmpty()) {
throw new InvalidParameterValueException("The request period does not contain balance entries.");
}
Collections.sort(quotaBalance, new Comparator<QuotaBalanceVO>() {
@Override
public int compare(QuotaBalanceVO o1, QuotaBalanceVO o2) {
o1 = o1 == null ? new QuotaBalanceVO() : o1;
o2 = o2 == null ? new QuotaBalanceVO() : o2;
return o2.getUpdatedOn().compareTo(o1.getUpdatedOn()); // desc
}
});
public QuotaBalanceResponse createQuotaBalanceResponse(QuotaBalanceCmd cmd) {
List<QuotaBalanceVO> quotaBalances = _quotaService.listQuotaBalancesForAccount(cmd.getEntityOwnerId(), cmd.getStartDate(), cmd.getEndDate());
boolean have_balance_entries = false;
//check that there is at least one balance entry
for (Iterator<QuotaBalanceVO> it = quotaBalance.iterator(); it.hasNext();) {
QuotaBalanceVO entry = it.next();
if (entry.isBalanceEntry()) {
have_balance_entries = true;
break;
}
}
//if last entry is a credit deposit then remove that as that is already
//accounted for in the starting balance after that entry, note the sort is desc
if (have_balance_entries) {
ListIterator<QuotaBalanceVO> li = quotaBalance.listIterator(quotaBalance.size());
// Iterate in reverse.
while (li.hasPrevious()) {
QuotaBalanceVO entry = li.previous();
if (logger.isDebugEnabled()) {
logger.debug("createQuotaBalanceResponse: Entry=" + entry);
}
if (entry.getCreditsId() > 0) {
li.remove();
} else {
break;
}
}
}
List<QuotaBalanceResponse> balances = quotaBalances.stream()
.map(balance -> new QuotaBalanceResponse(balance.getUpdatedOn(), balance.getCreditBalance()))
.collect(Collectors.toList());
int quota_activity = quotaBalance.size();
QuotaBalanceResponse resp = new QuotaBalanceResponse();
BigDecimal lastCredits = new BigDecimal(0);
boolean consecutive = true;
for (Iterator<QuotaBalanceVO> it = quotaBalance.iterator(); it.hasNext();) {
QuotaBalanceVO entry = it.next();
if (logger.isDebugEnabled()) {
logger.debug("createQuotaBalanceResponse: All Credit Entry=" + entry);
}
if (entry.getCreditsId() > 0) {
if (consecutive) {
lastCredits = lastCredits.add(entry.getCreditBalance());
}
resp.addCredits(entry);
it.remove();
} else {
consecutive = false;
}
}
QuotaBalanceResponse response = new QuotaBalanceResponse();
response.setCurrency(QuotaConfig.QuotaCurrencySymbol.value());
response.setBalances(balances);
if (quota_activity > 0 && quotaBalance.size() > 0) {
// order is desc last item is the start item
QuotaBalanceVO startItem = quotaBalance.get(quotaBalance.size() - 1);
QuotaBalanceVO endItem = quotaBalance.get(0);
resp.setStartDate(startDate);
resp.setStartQuota(startItem.getCreditBalance());
resp.setEndDate(endDate);
if (logger.isDebugEnabled()) {
logger.debug("createQuotaBalanceResponse: Start Entry=" + startItem);
logger.debug("createQuotaBalanceResponse: End Entry=" + endItem);
}
resp.setEndQuota(endItem.getCreditBalance().add(lastCredits));
} else if (quota_activity > 0) {
// order is desc last item is the start item
resp.setStartDate(startDate);
resp.setStartQuota(new BigDecimal(0));
resp.setEndDate(endDate);
resp.setEndQuota(new BigDecimal(0).add(lastCredits));
} else {
resp.setStartDate(startDate);
resp.setEndDate(endDate);
resp.setStartQuota(new BigDecimal(0));
resp.setEndQuota(new BigDecimal(0));
}
resp.setCurrency(QuotaConfig.QuotaCurrencySymbol.value());
resp.setObjectName("balance");
return resp;
return response;
}
@Override
@ -784,7 +706,7 @@ public class QuotaResponseBuilderImpl implements QuotaResponseBuilder {
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(depositedOn.getTime())));
final BigDecimal currentAccountBalance = _quotaBalanceDao.getLastQuotaBalance(accountId, domainId);
logger.debug("Depositing [{}] credits on adjusted date [{}]; current balance is [{}].", amount,
DateUtil.displayDateInTimezone(QuotaManagerImpl.getUsageAggregationTimeZone(), depositedOn), currentAccountBalance);
// update quota account with the balance
@ -848,34 +770,6 @@ public class QuotaResponseBuilderImpl implements QuotaResponseBuilder {
return false;
}
@Override
public QuotaBalanceResponse createQuotaLastBalanceResponse(List<QuotaBalanceVO> quotaBalance, Date startDate) {
if (quotaBalance == null) {
throw new InvalidParameterValueException("There are no balance entries on or before the requested date.");
}
if (startDate == null) {
startDate = new Date();
}
QuotaBalanceResponse resp = new QuotaBalanceResponse();
BigDecimal lastCredits = new BigDecimal(0);
for (QuotaBalanceVO entry : quotaBalance) {
logger.debug("createQuotaLastBalanceResponse Date={} balance={} credit={}",
DateUtil.displayDateInTimezone(QuotaManagerImpl.getUsageAggregationTimeZone(), entry.getUpdatedOn()),
entry.getCreditBalance(), entry.getCreditsId());
lastCredits = lastCredits.add(entry.getCreditBalance());
}
resp.setStartQuota(lastCredits);
resp.setStartDate(startDate);
resp.setCurrency(QuotaConfig.QuotaCurrencySymbol.value());
resp.setObjectName("balance");
return resp;
}
@Override
public List<QuotaBalanceVO> getQuotaBalance(QuotaBalanceCmd cmd) {
return _quotaService.findQuotaBalanceVO(cmd.getAccountId(), cmd.getAccountName(), cmd.getDomainId(), cmd.getStartDate(), cmd.getEndDate());
}
@Override
public Date startOfNextDay(Date date) {
LocalDate localDate = date.toInstant().atZone(ZoneId.systemDefault()).toLocalDate();

View File

@ -30,7 +30,7 @@ public interface QuotaService extends PluggableService {
List<QuotaUsageJoinVO> getQuotaUsage(Long accountId, String accountName, Long domainId, Integer usageType, Date startDate, Date endDate);
List<QuotaBalanceVO> findQuotaBalanceVO(Long accountId, String accountName, Long domainId, Date startDate, Date endDate);
List<QuotaBalanceVO> listQuotaBalancesForAccount(Long accountId, Date startDate, Date endDate);
void setLockAccount(Long accountId, Boolean state);

View File

@ -28,6 +28,7 @@ import javax.naming.ConfigurationException;
import com.cloud.projects.ProjectManager;
import com.cloud.user.AccountService;
import com.cloud.utils.DateUtil;
import org.apache.cloudstack.api.command.QuotaBalanceCmd;
import org.apache.cloudstack.api.command.QuotaConfigureEmailCmd;
import org.apache.cloudstack.api.command.QuotaCreditsCmd;
@ -58,17 +59,16 @@ import org.apache.cloudstack.quota.vo.QuotaAccountVO;
import org.apache.cloudstack.quota.vo.QuotaBalanceVO;
import org.apache.cloudstack.quota.vo.QuotaUsageJoinVO;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.time.DateUtils;
import org.springframework.stereotype.Component;
import com.cloud.configuration.Config;
import com.cloud.domain.dao.DomainDao;
import com.cloud.exception.InvalidParameterValueException;
import com.cloud.exception.PermissionDeniedException;
import com.cloud.user.Account;
import com.cloud.user.AccountVO;
import com.cloud.user.dao.AccountDao;
import com.cloud.utils.component.ManagerBase;
import com.cloud.utils.db.Filter;
@Component
public class QuotaServiceImpl extends ManagerBase implements QuotaService, Configurable, QuotaConfig {
@ -152,63 +152,53 @@ public class QuotaServiceImpl extends ManagerBase implements QuotaService, Confi
}
@Override
public List<QuotaBalanceVO> findQuotaBalanceVO(Long accountId, String accountName, Long domainId, Date startDate, Date endDate) {
if ((accountId == null) && (accountName != null) && (domainId != null)) {
Account userAccount = null;
Account caller = CallContext.current().getCallingAccount();
if (_domainDao.isChildDomain(caller.getDomainId(), domainId)) {
Filter filter = new Filter(AccountVO.class, "id", Boolean.FALSE, null, null);
List<AccountVO> accounts = _accountDao.listAccounts(accountName, domainId, filter);
if (!accounts.isEmpty()) {
userAccount = accounts.get(0);
}
if (userAccount != null) {
accountId = userAccount.getId();
} else {
throw new InvalidParameterValueException("Unable to find account " + accountName + " in domain " + domainId);
}
} else {
throw new PermissionDeniedException("Invalid Domain Id or Account");
public List<QuotaBalanceVO> listQuotaBalancesForAccount(Long accountId, Date startDate, Date endDate) {
validateStartDateAndEndDateForListQuotaBalancesForAccount(startDate, endDate);
if (accountId == -1) {
accountId = CallContext.current().getCallingAccountId();
}
Account account = _accountDao.findByIdIncludingRemoved(accountId);
Long domainId = account.getDomainId();
if (startDate == null && endDate == null) {
logger.debug("Retrieving last quota balance for {}.", account);
QuotaBalanceVO lastQuotaBalance = _quotaBalanceDao.getLastQuotaBalanceEntry(accountId, domainId, null);
if (lastQuotaBalance == null) {
logger.debug("Did not found a quota balance entry for {}.", account);
return new ArrayList<>();
}
return List.of(lastQuotaBalance);
}
startDate = startDate == null ? new Date() : startDate;
if (endDate == null) {
// adjust start date to end of day as there is no end date
startDate = _respBldr.startOfNextDay(startDate);
if (logger.isDebugEnabled()) {
logger.debug("getQuotaBalance1: Getting quota balance records for account: " + accountId + ", domainId: " + domainId + ", on or before " + startDate);
}
List<QuotaBalanceVO> qbrecords = _quotaBalanceDao.lastQuotaBalanceVO(accountId, domainId, startDate);
if (logger.isDebugEnabled()) {
logger.debug("Found records size=" + qbrecords.size());
}
if (qbrecords.isEmpty()) {
logger.info("Incorrect Date there are no quota records before this date " + startDate);
return qbrecords;
} else {
return qbrecords;
}
} else {
if (startDate.before(endDate)) {
if (logger.isDebugEnabled()) {
logger.debug("getQuotaBalance2: Getting quota balance records for account: " + accountId + ", domainId: " + domainId + ", between " + startDate
+ " and " + endDate);
}
List<QuotaBalanceVO> qbrecords = _quotaBalanceDao.findQuotaBalance(accountId, domainId, startDate, endDate);
if (logger.isDebugEnabled()) {
logger.debug("getQuotaBalance3: Found records size=" + qbrecords.size());
}
if (qbrecords.isEmpty()) {
logger.info("There are no quota records between these dates start date " + startDate + " and end date:" + endDate);
return qbrecords;
} else {
return qbrecords;
}
} else {
throw new InvalidParameterValueException("Incorrect Date Range. Start date: " + startDate + " is after end date:" + endDate);
}
endDate = DateUtils.addDays(new Date(), -1);
}
List<QuotaBalanceVO> quotaBalances = _quotaBalanceDao.listQuotaBalances(accountId, domainId, startDate, endDate);
if (quotaBalances.isEmpty()) {
logger.info("There are no quota balances for {} between [{}] and [{}].", account,
DateUtil.getOutputString(startDate), DateUtil.getOutputString(endDate));
}
return quotaBalances;
}
protected void validateStartDateAndEndDateForListQuotaBalancesForAccount(Date startDate, Date endDate) {
if (startDate == null && endDate != null) {
throw new InvalidParameterValueException("Parameter \"enddate\" must be informed together with parameter \"startdate\".");
}
Date now = new Date();
if (startDate != null && startDate.after(now)) {
throw new InvalidParameterValueException("The last balance can be at most from yesterday; therefore, the start date must be before today.");
}
if (ObjectUtils.allNotNull(startDate, endDate) && startDate.after(endDate)) {
throw new InvalidParameterValueException("The start date cannot be after the end date.");
}
}

View File

@ -16,18 +16,15 @@
// under the License.
package org.apache.cloudstack.api.command;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import org.apache.cloudstack.api.response.QuotaBalanceResponse;
import org.apache.cloudstack.api.response.QuotaResponseBuilder;
import org.apache.cloudstack.quota.vo.QuotaBalanceVO;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.Spy;
import org.mockito.junit.MockitoJUnitRunner;
import junit.framework.TestCase;
@ -36,31 +33,20 @@ import junit.framework.TestCase;
public class QuotaBalanceCmdTest extends TestCase {
@Mock
QuotaResponseBuilder responseBuilder;
QuotaResponseBuilder quotaResponseBuilderMock;
@InjectMocks
@Spy
QuotaBalanceCmd quotaBalanceCmdSpy;
@Test
public void testQuotaBalanceCmd() throws NoSuchFieldException, IllegalAccessException {
QuotaBalanceCmd cmd = new QuotaBalanceCmd();
Field rbField = QuotaBalanceCmd.class.getDeclaredField("_responseBuilder");
rbField.setAccessible(true);
rbField.set(cmd, responseBuilder);
public void executeTestSetResponseObject() {
QuotaBalanceResponse expected = new QuotaBalanceResponse();
Mockito.doReturn(expected).when(quotaResponseBuilderMock).createQuotaBalanceResponse(Mockito.eq(quotaBalanceCmdSpy));
List<QuotaBalanceVO> quotaBalanceVOList = new ArrayList<QuotaBalanceVO>();
Mockito.when(responseBuilder.getQuotaBalance(Mockito.any(cmd.getClass()))).thenReturn(quotaBalanceVOList);
Mockito.when(responseBuilder.createQuotaLastBalanceResponse(Mockito.eq(quotaBalanceVOList), Mockito.any(Date.class))).thenReturn(new QuotaBalanceResponse());
Mockito.when(responseBuilder.createQuotaBalanceResponse(Mockito.eq(quotaBalanceVOList), Mockito.any(Date.class), Mockito.any(Date.class))).thenReturn(new QuotaBalanceResponse());
Mockito.lenient().when(responseBuilder.startOfNextDay(Mockito.any(Date.class))).thenReturn(new Date());
quotaBalanceCmdSpy.execute();
// end date not specified
cmd.setStartDate(new Date());
cmd.setEndDate(null);
cmd.execute();
Mockito.verify(responseBuilder, Mockito.times(1)).createQuotaLastBalanceResponse(Mockito.eq(quotaBalanceVOList), Mockito.any(Date.class));
Mockito.verify(responseBuilder, Mockito.times(0)).createQuotaBalanceResponse(Mockito.eq(quotaBalanceVOList), Mockito.any(Date.class), Mockito.any(Date.class));
// end date specified
cmd.setEndDate(new Date());
cmd.execute();
Mockito.verify(responseBuilder, Mockito.times(1)).createQuotaBalanceResponse(Mockito.eq(quotaBalanceVOList), Mockito.any(Date.class), Mockito.any(Date.class));
Assert.assertEquals(expected, quotaBalanceCmdSpy.getResponseObject());
}
}

View File

@ -40,6 +40,7 @@ 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.QuotaBalanceCmd;
import org.apache.cloudstack.api.command.QuotaConfigureEmailCmd;
import org.apache.cloudstack.api.command.QuotaCreditsListCmd;
import org.apache.cloudstack.api.command.QuotaEmailTemplateListCmd;
@ -249,7 +250,7 @@ public class QuotaResponseBuilderImplTest extends TestCase {
credit.setCredit(new BigDecimal(amount));
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.when(quotaBalanceDaoMock.getLastQuotaBalance(Mockito.anyLong(), Mockito.anyLong())).thenReturn(new BigDecimal(111));
Mockito.doReturn(userVoMock).when(quotaResponseBuilderSpy).getCreditorForQuotaCredits(credit);
AccountVO account = new AccountVO();
@ -295,33 +296,6 @@ public class QuotaResponseBuilderImplTest extends TestCase {
assertTrue(quotaResponseBuilderSpy.updateQuotaEmailTemplate(cmd));
}
@Test
public void testCreateQuotaLastBalanceResponse() {
List<QuotaBalanceVO> quotaBalance = new ArrayList<>();
// null balance test
try {
quotaResponseBuilderSpy.createQuotaLastBalanceResponse(null, new Date());
} catch (InvalidParameterValueException e) {
assertTrue(e.getMessage().equals("There are no balance entries on or before the requested date."));
}
// empty balance test
try {
quotaResponseBuilderSpy.createQuotaLastBalanceResponse(quotaBalance, new Date());
} catch (InvalidParameterValueException e) {
assertTrue(e.getMessage().equals("There are no balance entries on or before the requested date."));
}
// valid balance test
QuotaBalanceVO entry = new QuotaBalanceVO();
entry.setAccountId(2L);
entry.setCreditBalance(new BigDecimal(100));
quotaBalance.add(entry);
quotaBalance.add(entry);
QuotaBalanceResponse resp = quotaResponseBuilderSpy.createQuotaLastBalanceResponse(quotaBalance, null);
assertTrue(resp.getStartQuota().compareTo(new BigDecimal(200)) == 0);
}
@Test
public void testStartOfNextDayWithoutParameters() {
Date nextDate = quotaResponseBuilderSpy.startOfNextDay();
@ -919,6 +893,46 @@ public class QuotaResponseBuilderImplTest extends TestCase {
Assert.assertTrue(formattedVariables.containsValue("zonename"));
}
private List<QuotaBalanceVO> getQuotaBalancesForTest() {
List<QuotaBalanceVO> balances = new ArrayList<>();
QuotaBalanceVO balance = new QuotaBalanceVO();
balance.setUpdatedOn(new Date());
balance.setCreditBalance(BigDecimal.valueOf(-10.42));
balances.add(balance);
balance = new QuotaBalanceVO();
balance.setUpdatedOn(new Date());
balance.setCreditBalance(BigDecimal.valueOf(-18.94));
balances.add(balance);
balance = new QuotaBalanceVO();
balance.setUpdatedOn(new Date());
balance.setCreditBalance(BigDecimal.valueOf(-29.37));
balances.add(balance);
return balances;
}
@Test
public void createQuotaBalancesResponseTestCreateResponse() {
List<QuotaBalanceVO> balances = getQuotaBalancesForTest();
QuotaBalanceResponse expected = new QuotaBalanceResponse();
expected.setObjectName("balance");
expected.setCurrency("$");
Mockito.doReturn(balances).when(quotaServiceMock).listQuotaBalancesForAccount(Mockito.any(), Mockito.any(), Mockito.any());
QuotaBalanceResponse result = quotaResponseBuilderSpy.createQuotaBalanceResponse(new QuotaBalanceCmd());
Assert.assertEquals(expected.getCurrency(), result.getCurrency());
for (int i = 0; i < balances.size(); i++) {
Assert.assertEquals(balances.get(i).getUpdatedOn(), result.getBalances().get(i).getDate());
Assert.assertEquals(balances.get(i).getCreditBalance(), result.getBalances().get(i).getBalance());
}
}
@Test
public void createDummyRecordForEachQuotaTypeIfUsageTypeIsNotInformedTestUsageTypeDifferentFromNullDoNothing() {
List<QuotaUsageJoinVO> listUsage = new ArrayList<>();

View File

@ -18,6 +18,7 @@ package org.apache.cloudstack.quota;
import com.cloud.configuration.Config;
import com.cloud.domain.dao.DomainDao;
import com.cloud.exception.InvalidParameterValueException;
import com.cloud.user.AccountVO;
import com.cloud.user.dao.AccountDao;
import com.cloud.utils.db.TransactionLegacy;
@ -31,7 +32,9 @@ import org.apache.cloudstack.quota.dao.QuotaUsageDao;
import org.apache.cloudstack.quota.dao.QuotaUsageJoinDao;
import org.apache.cloudstack.quota.vo.QuotaAccountVO;
import org.apache.cloudstack.quota.vo.QuotaBalanceVO;
import org.apache.commons.lang3.time.DateUtils;
import org.joda.time.DateTime;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
@ -43,7 +46,6 @@ import org.mockito.junit.MockitoJUnitRunner;
import javax.naming.ConfigurationException;
import java.lang.reflect.Field;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
@ -51,6 +53,8 @@ import java.util.List;
@RunWith(MockitoJUnitRunner.class)
public class QuotaServiceImplTest extends TestCase {
@Mock
AccountVO accountVoMock;
@Mock
AccountDao accountDaoMock;
@Mock
@ -67,9 +71,6 @@ public class QuotaServiceImplTest extends TestCase {
QuotaUsageJoinDao quotaUsageJoinDaoMock;
@Mock
QuotaResponseBuilder respBldr;
@Mock
private AccountVO accountVoMock;
@Spy
@InjectMocks
QuotaServiceImpl quotaServiceImplSpy;
@ -112,30 +113,6 @@ public class QuotaServiceImplTest extends TestCase {
quotaServiceImplSpy.configure("randomName", null);
}
@Test
public void testFindQuotaBalanceVO() {
final long accountId = 2L;
final String accountName = "admin123";
final long domainId = 1L;
final Date startDate = new DateTime().minusDays(2).toDate();
final Date endDate = new Date();
List<QuotaBalanceVO> records = new ArrayList<>();
QuotaBalanceVO qb = new QuotaBalanceVO();
qb.setCreditBalance(new BigDecimal(100));
qb.setAccountId(accountId);
records.add(qb);
Mockito.when(respBldr.startOfNextDay(Mockito.any(Date.class))).thenReturn(startDate);
Mockito.when(quotaBalanceDao.findQuotaBalance(Mockito.eq(accountId), Mockito.eq(domainId), Mockito.any(Date.class), Mockito.any(Date.class))).thenReturn(records);
Mockito.when(quotaBalanceDao.lastQuotaBalanceVO(Mockito.eq(accountId), Mockito.eq(domainId), Mockito.any(Date.class))).thenReturn(records);
// with enddate
assertTrue(quotaServiceImplSpy.findQuotaBalanceVO(accountId, accountName, domainId, startDate, endDate).get(0).equals(qb));
// without enddate
assertTrue(quotaServiceImplSpy.findQuotaBalanceVO(accountId, accountName, domainId, startDate, null).get(0).equals(qb));
}
@Test
public void testGetQuotaUsage() {
final long accountId = 2L;
@ -182,4 +159,66 @@ public class QuotaServiceImplTest extends TestCase {
Mockito.verify(quotaAcc, Mockito.times(1)).persistQuotaAccount(Mockito.any(QuotaAccountVO.class));
}
@Test(expected = InvalidParameterValueException.class)
public void validateStartDateAndEndDateForListQuotaBalancesForAccountTestStartDateIsNullAndEndDateIsNotNullThrowsInvalidParameterException() {
quotaServiceImplSpy.validateStartDateAndEndDateForListQuotaBalancesForAccount(null, new Date());
}
@Test(expected = InvalidParameterValueException.class)
public void validateStartDateAndEndDateForListQuotaBalancesForAccountTestStartDateIsAfterNowThrowsInvalidParameterValueException() {
Date startDate = DateUtils.addMinutes(new Date(), 1);
quotaServiceImplSpy.validateStartDateAndEndDateForListQuotaBalancesForAccount(startDate, null);
}
@Test
public void validateStartDateAndEndDateForListQuotaBalancesForAccountTestEndDateIsAfterNowDoesNothing() {
Date startDate = DateUtils.addMinutes(new Date(), -1);
Date endDate = DateUtils.addMinutes(new Date(), 1);
quotaServiceImplSpy.validateStartDateAndEndDateForListQuotaBalancesForAccount(startDate, endDate);
}
@Test(expected = InvalidParameterValueException.class)
public void validateStartDateAndEndDateForListQuotaBalancesForAccountTestStartDateIsAfterEndDateThrowsInvalidParameterValueException() {
Date startDate = DateUtils.addMinutes(new Date(), -10);
Date endDate = DateUtils.addMinutes(new Date(), -15);
quotaServiceImplSpy.validateStartDateAndEndDateForListQuotaBalancesForAccount(startDate, endDate);
}
@Test
public void listQuotaBalancesForAccountTestLastQuotaBalanceIsNullReturnsEmptyList() {
Mockito.doNothing().when(quotaServiceImplSpy).validateStartDateAndEndDateForListQuotaBalancesForAccount(Mockito.any(), Mockito.any());
Mockito.doReturn(null).when(quotaBalanceDao).getLastQuotaBalanceEntry(Mockito.anyLong(), Mockito.anyLong(), Mockito.any());
Mockito.doReturn(Mockito.mock(AccountVO.class)).when(accountDaoMock).findByIdIncludingRemoved(Mockito.anyLong());
List<QuotaBalanceVO> result = quotaServiceImplSpy.listQuotaBalancesForAccount(1L, null, null);
Assert.assertTrue(result.isEmpty());
}
@Test
public void listQuotaBalancesForAccountTestLastQuotaBalanceIsNotNullReturnsIt() {
QuotaBalanceVO expected = new QuotaBalanceVO();
Mockito.doNothing().when(quotaServiceImplSpy).validateStartDateAndEndDateForListQuotaBalancesForAccount(Mockito.any(), Mockito.any());
Mockito.doReturn(expected).when(quotaBalanceDao).getLastQuotaBalanceEntry(Mockito.anyLong(), Mockito.anyLong(), Mockito.any());
Mockito.doReturn(Mockito.mock(AccountVO.class)).when(accountDaoMock).findByIdIncludingRemoved(Mockito.anyLong());
List<QuotaBalanceVO> result = quotaServiceImplSpy.listQuotaBalancesForAccount(1L, null, null);
Assert.assertEquals(expected, result.get(0));
}
@Test
public void listQuotaBalancesForAccountTestReturnsQuotaBalances() {
List<QuotaBalanceVO> expected = new ArrayList<>();
Mockito.doNothing().when(quotaServiceImplSpy).validateStartDateAndEndDateForListQuotaBalancesForAccount(Mockito.any(), Mockito.any());
Mockito.doReturn(expected).when(quotaBalanceDao).listQuotaBalances(Mockito.anyLong(), Mockito.anyLong(), Mockito.any(), Mockito.any());
Mockito.doReturn(Mockito.mock(AccountVO.class)).when(accountDaoMock).findByIdIncludingRemoved(Mockito.anyLong());
List<QuotaBalanceVO> result = quotaServiceImplSpy.listQuotaBalancesForAccount(1L, new Date(), null);
Assert.assertEquals(expected, result);
}
}

View File

@ -3922,7 +3922,7 @@ public class AccountManagerImpl extends ManagerBase implements AccountManager, M
if (getActiveAccountById(accountId) != null) {
return accountId;
}
throw new InvalidParameterValueException(String.format("Unable to find account with ID [%s].", accountId));
throw new InvalidParameterValueException(String.format("Unable to find account with the specified ID."));
}
if (accountName == null && domainId == null) {
@ -3938,16 +3938,16 @@ public class AccountManagerImpl extends ManagerBase implements AccountManager, M
throw new ServerApiException(ApiErrorCode.PARAM_ERROR, String.format("Both %s and %s are needed if using either. Consider using %s instead.",
ApiConstants.ACCOUNT, ApiConstants.DOMAIN_ID, ApiConstants.ACCOUNT_ID));
}
throw new InvalidParameterValueException(String.format("Unable to find account by name [%s] on domain [%s].", accountName, domainId));
throw new InvalidParameterValueException(String.format("Unable to find account with name [%s] on the specified domain.", accountName));
}
protected long getActiveProjectAccountByProjectId(long projectId) {
Project project = _projectMgr.getProject(projectId);
if (project == null) {
throw new ServerApiException(ApiErrorCode.PARAM_ERROR, String.format("Unable to find project with ID [%s].", projectId));
throw new ServerApiException(ApiErrorCode.PARAM_ERROR, "Unable to find project with the specified ID.");
}
if (project.getState() != Project.State.Active) {
throw new ServerApiException(ApiErrorCode.PARAM_ERROR, String.format("Project with ID [%s] is not active.", projectId));
throw new ServerApiException(ApiErrorCode.PARAM_ERROR, "Project is not active.");
}
return project.getProjectAccountId();
}

View File

@ -117,9 +117,28 @@ class TestQuotaBalance(cloudstackTestCase):
def delete_tariffs(self):
for tariff in self.tariffs:
cmd = quotaTariffDelete.quotaTariffDeleteCmd()
cmd.id = tariff.uuid
cmd.id = tariff.id
self.apiclient.quotaTariffDelete(cmd)
def insert_usage_and_update_quota(self, zone_id, account_id, domain_id, relative_start_date, relative_end_date):
start_date = f"DATE_ADD(UTC_TIMESTAMP(), INTERVAL {relative_start_date} HOUR)"
end_date = f"DATE_ADD(UTC_TIMESTAMP(), INTERVAL {relative_end_date} HOUR)"
# Manually insert a usage regarding the usage type 21 (VM_DISK_IO_READ)
sql_query = (f"INSERT INTO cloud_usage.cloud_usage (zone_id,account_id,domain_id,description,usage_display,usage_type,raw_usage,vm_instance_id,vm_name,offering_id,template_id,"
f"usage_id,`type`,`size`,network_id,start_date,end_date,virtual_size,cpu_speed,cpu_cores,memory,quota_calculated,is_hidden,state)"
f" VALUES ('{zone_id}','{account_id}','{domain_id}','Test','1 Hrs',21,1,NULL,NULL,NULL,NULL,NULL,'VirtualMachine',NULL,NULL,{start_date},{end_date},NULL,NULL,NULL,NULL,0,0,NULL);")
self.debug(sql_query)
self.dbclient.execute(sql_query)
# Update quota to calculate the balance of the account
cmd = quotaUpdate.quotaUpdateCmd()
self.apiclient.quotaUpdate(cmd)
time.sleep(1)
def format_date(self, date):
return date.strftime("%Y-%m-%d %H:%M:%S")
@attr(tags=["advanced", "smoke", "quota"], required_hardware="false")
def test_quota_balance(self):
"""
@ -146,6 +165,7 @@ class TestQuotaBalance(cloudstackTestCase):
cmd.domainid = self.domain.id
cmd.value = 100
self.apiclient.quotaCredits(cmd)
time.sleep(1)
# Fetch account ID from account_uuid
account_id_select = f"SELECT id FROM account WHERE uuid = '{self.account.id}';"
@ -165,27 +185,24 @@ class TestQuotaBalance(cloudstackTestCase):
qresultset = self.dbclient.execute(zone_id_select)
zone_id = qresultset[0][0]
start_date = datetime.datetime.now() + datetime.timedelta(seconds=1)
end_date = datetime.datetime.now() + datetime.timedelta(hours=1)
# Manually insert a usage regarding the usage type 21 (VM_DISK_IO_READ)
sql_query = (f"INSERT INTO cloud_usage.cloud_usage (zone_id,account_id,domain_id,description,usage_display,usage_type,raw_usage,vm_instance_id,vm_name,offering_id,template_id,"
f"usage_id,`type`,`size`,network_id,start_date,end_date,virtual_size,cpu_speed,cpu_cores,memory,quota_calculated,is_hidden,state)"
f" VALUES ('{zone_id}','{account_id}','{domain_id}','Test','1 Hrs',21,1,NULL,NULL,NULL,NULL,NULL,'VirtualMachine',NULL,NULL,'{start_date}','{end_date}',NULL,NULL,NULL,NULL,0,0,NULL);")
self.debug(sql_query)
self.dbclient.execute(sql_query)
# Update quota to calculate the balance of the account
cmd = quotaUpdate.quotaUpdateCmd()
self.apiclient.quotaUpdate(cmd)
# Generate three quota_balance entries
self.insert_usage_and_update_quota(zone_id, account_id, domain_id, 0, 1)
self.insert_usage_and_update_quota(zone_id, account_id, domain_id, 1, 2)
self.insert_usage_and_update_quota(zone_id, account_id, domain_id, 2, 3)
# Retrieve the quota balance of the account
cmd = quotaBalance.quotaBalanceCmd()
cmd.domainid = self.account.domainid
cmd.account = self.account.name
cmd.startdate = datetime.datetime.now() + datetime.timedelta(hours=-1)
cmd.enddate = datetime.datetime.now() + datetime.timedelta(hours=3)
response = self.apiclient.quotaBalance(cmd)
self.debug(f"The quota balance for the account {self.account.name} is {response.balance}.")
self.assertEqual(response.balance.startquota, 90, f"The `startQuota` response field is supposed to be 90 but was {response.balance.startquota}.")
self.assertTrue(len(response.balance.balances) == 4, f"Expected 4 balance entries for between {self.format_date(cmd.startdate)} " +
f"and {self.format_date(cmd.enddate)} but got {len(response.balance.balances)}.")
for i, balance in enumerate(response.balance.balances):
expected_balance = 100 - 10 * i
self.debug(f"The quota balance for the account {self.account.name} at {balance.date} was {balance.balance}.")
self.assertEqual(balance.balance, expected_balance, f"The `balance` field at {balance.date} is supposed to be {expected_balance} but was {balance.balance}.")
return