This is an automated email from the ASF dual-hosted git repository.

winterhazel pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/cloudstack.git


The following commit(s) were added to refs/heads/main by this push:
     new 3f6866d70a6 Refactor Quota balance (#12961)
3f6866d70a6 is described below

commit 3f6866d70a6e4eb8a12dd257d33bde5aae79aacf
Author: Fabricio Duarte <[email protected]>
AuthorDate: Tue May 26 10:36:04 2026 -0300

    Refactor Quota balance (#12961)
---
 .../org/apache/cloudstack/api/ApiConstants.java    |   3 +
 .../apache/cloudstack/quota/QuotaManagerImpl.java  |   9 +-
 .../cloudstack/quota/dao/QuotaBalanceDao.java      |  10 +-
 .../cloudstack/quota/dao/QuotaBalanceDaoImpl.java  | 187 ++++++++-------------
 .../quota/dao/QuotaBalanceDaoImplTest.java         |  91 ++++++++++
 .../cloudstack/api/command/QuotaBalanceCmd.java    |  62 +++----
 .../api/response/QuotaBalanceResponse.java         | 132 ++++-----------
 .../api/response/QuotaResponseBuilder.java         |   7 +-
 .../api/response/QuotaResponseBuilderImpl.java     | 128 ++------------
 .../org/apache/cloudstack/quota/QuotaService.java  |   2 +-
 .../apache/cloudstack/quota/QuotaServiceImpl.java  | 100 +++++------
 .../api/command/QuotaBalanceCmdTest.java           |  42 ++---
 .../api/response/QuotaResponseBuilderImplTest.java |  70 +++++---
 .../cloudstack/quota/QuotaServiceImplTest.java     |  95 ++++++++---
 .../java/com/cloud/user/AccountManagerImpl.java    |   8 +-
 .../plugins/quota/test_quota_balance.py            |  49 ++++--
 16 files changed, 442 insertions(+), 553 deletions(-)

diff --git a/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java 
b/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java
index 4d4ead277e5..694830ea2f3 100644
--- a/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java
+++ b/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java
@@ -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";
diff --git 
a/framework/quota/src/main/java/org/apache/cloudstack/quota/QuotaManagerImpl.java
 
b/framework/quota/src/main/java/org/apache/cloudstack/quota/QuotaManagerImpl.java
index 816144aa2f1..58f51eae9fc 100644
--- 
a/framework/quota/src/main/java/org/apache/cloudstack/quota/QuotaManagerImpl.java
+++ 
b/framework/quota/src/main/java/org/apache/cloudstack/quota/QuotaManagerImpl.java
@@ -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));
 
diff --git 
a/framework/quota/src/main/java/org/apache/cloudstack/quota/dao/QuotaBalanceDao.java
 
b/framework/quota/src/main/java/org/apache/cloudstack/quota/dao/QuotaBalanceDao.java
index c694eaeefbe..2964746bdf0 100644
--- 
a/framework/quota/src/main/java/org/apache/cloudstack/quota/dao/QuotaBalanceDao.java
+++ 
b/framework/quota/src/main/java/org/apache/cloudstack/quota/dao/QuotaBalanceDao.java
@@ -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);
 
 }
diff --git 
a/framework/quota/src/main/java/org/apache/cloudstack/quota/dao/QuotaBalanceDaoImpl.java
 
b/framework/quota/src/main/java/org/apache/cloudstack/quota/dao/QuotaBalanceDaoImpl.java
index 01272d1a618..21553ebd27b 100644
--- 
a/framework/quota/src/main/java/org/apache/cloudstack/quota/dao/QuotaBalanceDaoImpl.java
+++ 
b/framework/quota/src/main/java/org/apache/cloudstack/quota/dao/QuotaBalanceDaoImpl.java
@@ -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> 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> 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<>();
             }
-        });
-    }
 
-    @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;
+            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> 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;
+    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 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;
+    }
+
 }
diff --git 
a/framework/quota/src/test/java/org/apache/cloudstack/quota/dao/QuotaBalanceDaoImplTest.java
 
b/framework/quota/src/test/java/org/apache/cloudstack/quota/dao/QuotaBalanceDaoImplTest.java
new file mode 100644
index 00000000000..afdd88f8d1d
--- /dev/null
+++ 
b/framework/quota/src/test/java/org/apache/cloudstack/quota/dao/QuotaBalanceDaoImplTest.java
@@ -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);
+    }
+}
diff --git 
a/plugins/database/quota/src/main/java/org/apache/cloudstack/api/command/QuotaBalanceCmd.java
 
b/plugins/database/quota/src/main/java/org/apache/cloudstack/api/command/QuotaBalanceCmd.java
index 0cec0df6618..e5c89c5de1c 100644
--- 
a/plugins/database/quota/src/main/java/org/apache/cloudstack/api/command/QuotaBalanceCmd.java
+++ 
b/plugins/database/quota/src/main/java/org/apache/cloudstack/api/command/QuotaBalanceCmd.java
@@ -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;
-        }
-        Account account = _accountService.getActiveAccountByName(accountName, 
domainId);
-        if (account != null) {
-            return account.getAccountId();
+        if (ObjectUtils.allNull(accountId, accountName, domainId, projectId)) {
+            return -1;
         }
-        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);
     }
diff --git 
a/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaBalanceResponse.java
 
b/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaBalanceResponse.java
index 625f4829c67..714066b048a 100644
--- 
a/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaBalanceResponse.java
+++ 
b/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaBalanceResponse.java
@@ -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;
 
-    public QuotaBalanceResponse() {
-        super();
-        credits = new ArrayList<QuotaCreditsResponse>();
-    }
-
-    public Long getAccountId() {
-        return accountId;
-    }
-
-    public void setAccountId(Long accountId) {
-        this.accountId = accountId;
-    }
-
-    public String getAccountName() {
-        return accountName;
-    }
+    @SerializedName(ApiConstants.DATE)
+    @Param(description = "Balance's date.")
+    private Date date;
 
-    public void setAccountName(String accountName) {
-        this.accountName = accountName;
-    }
+    @SerializedName(ApiConstants.BALANCE)
+    @Param(description = "Balance's value.")
+    private BigDecimal balance;
 
-    public Long getDomainId() {
-        return domainId;
-    }
-
-    public void setDomainId(Long domainId) {
-        this.domainId = domainId;
-    }
+    @SerializedName(ApiConstants.BALANCES)
+    @Param(description = "List of balances in the period.")
+    private List<QuotaBalanceResponse> balances;
 
-    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 QuotaBalanceResponse() {
+        super("balance");
     }
 
-    public void addCredits(QuotaBalanceVO credit) {
-        QuotaCreditsResponse cr = new QuotaCreditsResponse();
-        cr.setCredit(credit.getCreditBalance());
-        cr.setCreditedOn(credit.getUpdatedOn());
-        credits.add(0, cr);
+    public QuotaBalanceResponse(Date date, BigDecimal balance) {
+        super("balance");
+        this.date = date;
+        this.balance = balance;
     }
 
-    public Date getStartDate() {
-        return startDate == null ? null : new Date(startDate.getTime());
+    public void setCurrency(String currency) {
+        this.currency = currency;
     }
 
-    public void setStartDate(Date startDate) {
-        this.startDate = startDate == null ? null : new 
Date(startDate.getTime());
+    public void setBalances(List<QuotaBalanceResponse> balances) {
+        this.balances = balances;
     }
 
-    public Date getEndDate() {
-        return endDate == null ? null : new Date(endDate.getTime());
+    public String getCurrency() {
+        return currency;
     }
 
-    public void setEndDate(Date endDate) {
-        this.endDate = endDate == null ? null : new Date(endDate.getTime());
+    public List<QuotaBalanceResponse> getBalances() {
+        return balances;
     }
 
-    public String getCurrency() {
-        return currency;
+    public Date getDate() {
+        return date;
     }
 
-    public void setCurrency(String currency) {
-        this.currency = currency;
+    public BigDecimal getBalance() {
+        return balance;
     }
 }
diff --git 
a/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaResponseBuilder.java
 
b/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaResponseBuilder.java
index bde905c487b..f0390bf626d 100644
--- 
a/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaResponseBuilder.java
+++ 
b/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaResponseBuilder.java
@@ -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);
diff --git 
a/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaResponseBuilderImpl.java
 
b/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaResponseBuilderImpl.java
index c919bb5887c..231ba777e60 100644
--- 
a/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaResponseBuilderImpl.java
+++ 
b/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaResponseBuilderImpl.java
@@ -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
-            }
-        });
-
-        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;
-                }
-            }
-        }
+    public QuotaBalanceResponse createQuotaBalanceResponse(QuotaBalanceCmd 
cmd) {
+        List<QuotaBalanceVO> quotaBalances = 
_quotaService.listQuotaBalancesForAccount(cmd.getEntityOwnerId(), 
cmd.getStartDate(), cmd.getEndDate());
 
-        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;
-            }
-        }
+        List<QuotaBalanceResponse> balances = quotaBalances.stream()
+                .map(balance -> new 
QuotaBalanceResponse(balance.getUpdatedOn(), balance.getCreditBalance()))
+                .collect(Collectors.toList());
 
-        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;
+        QuotaBalanceResponse response = new QuotaBalanceResponse();
+        response.setCurrency(QuotaConfig.QuotaCurrencySymbol.value());
+        response.setBalances(balances);
+
+        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();
diff --git 
a/plugins/database/quota/src/main/java/org/apache/cloudstack/quota/QuotaService.java
 
b/plugins/database/quota/src/main/java/org/apache/cloudstack/quota/QuotaService.java
index 78acfc11682..478e43d2e20 100644
--- 
a/plugins/database/quota/src/main/java/org/apache/cloudstack/quota/QuotaService.java
+++ 
b/plugins/database/quota/src/main/java/org/apache/cloudstack/quota/QuotaService.java
@@ -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);
 
diff --git 
a/plugins/database/quota/src/main/java/org/apache/cloudstack/quota/QuotaServiceImpl.java
 
b/plugins/database/quota/src/main/java/org/apache/cloudstack/quota/QuotaServiceImpl.java
index a0ba2fbc751..e1ec2ebb2a6 100644
--- 
a/plugins/database/quota/src/main/java/org/apache/cloudstack/quota/QuotaServiceImpl.java
+++ 
b/plugins/database/quota/src/main/java/org/apache/cloudstack/quota/QuotaServiceImpl.java
@@ -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();
 
-        startDate = startDate == null ? new Date() : startDate;
+        if (startDate == null && endDate == null) {
+            logger.debug("Retrieving last quota balance for {}.", account);
+            QuotaBalanceVO lastQuotaBalance = 
_quotaBalanceDao.getLastQuotaBalanceEntry(accountId, domainId, null);
 
-        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);
+            if (lastQuotaBalance == null) {
+                logger.debug("Did not found a quota balance entry for {}.", 
account);
+                return new ArrayList<>();
             }
+
+            return List.of(lastQuotaBalance);
+        }
+
+        if (endDate == null) {
+            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.");
         }
     }
 
diff --git 
a/plugins/database/quota/src/test/java/org/apache/cloudstack/api/command/QuotaBalanceCmdTest.java
 
b/plugins/database/quota/src/test/java/org/apache/cloudstack/api/command/QuotaBalanceCmdTest.java
index adabc694f25..cd0eeb2c3b4 100644
--- 
a/plugins/database/quota/src/test/java/org/apache/cloudstack/api/command/QuotaBalanceCmdTest.java
+++ 
b/plugins/database/quota/src/test/java/org/apache/cloudstack/api/command/QuotaBalanceCmdTest.java
@@ -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;
 
-    @Test
-    public void testQuotaBalanceCmd() throws NoSuchFieldException, 
IllegalAccessException {
-        QuotaBalanceCmd cmd = new QuotaBalanceCmd();
-        Field rbField = 
QuotaBalanceCmd.class.getDeclaredField("_responseBuilder");
-        rbField.setAccessible(true);
-        rbField.set(cmd, responseBuilder);
+    @InjectMocks
+    @Spy
+    QuotaBalanceCmd 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());
+    @Test
+    public void executeTestSetResponseObject() {
+        QuotaBalanceResponse expected = new QuotaBalanceResponse();
+        
Mockito.doReturn(expected).when(quotaResponseBuilderMock).createQuotaBalanceResponse(Mockito.eq(quotaBalanceCmdSpy));
 
-        // 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));
+        quotaBalanceCmdSpy.execute();
 
-        // 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());
     }
+
 }
diff --git 
a/plugins/database/quota/src/test/java/org/apache/cloudstack/api/response/QuotaResponseBuilderImplTest.java
 
b/plugins/database/quota/src/test/java/org/apache/cloudstack/api/response/QuotaResponseBuilderImplTest.java
index 81b4992082d..0c7610461c1 100644
--- 
a/plugins/database/quota/src/test/java/org/apache/cloudstack/api/response/QuotaResponseBuilderImplTest.java
+++ 
b/plugins/database/quota/src/test/java/org/apache/cloudstack/api/response/QuotaResponseBuilderImplTest.java
@@ -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<>();
diff --git 
a/plugins/database/quota/src/test/java/org/apache/cloudstack/quota/QuotaServiceImplTest.java
 
b/plugins/database/quota/src/test/java/org/apache/cloudstack/quota/QuotaServiceImplTest.java
index a0fe63de851..89c48b1cc66 100644
--- 
a/plugins/database/quota/src/test/java/org/apache/cloudstack/quota/QuotaServiceImplTest.java
+++ 
b/plugins/database/quota/src/test/java/org/apache/cloudstack/quota/QuotaServiceImplTest.java
@@ -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);
+    }
+
 }
diff --git a/server/src/main/java/com/cloud/user/AccountManagerImpl.java 
b/server/src/main/java/com/cloud/user/AccountManagerImpl.java
index d4d48bdf77b..52ec26661c9 100644
--- a/server/src/main/java/com/cloud/user/AccountManagerImpl.java
+++ b/server/src/main/java/com/cloud/user/AccountManagerImpl.java
@@ -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();
     }
diff --git a/test/integration/plugins/quota/test_quota_balance.py 
b/test/integration/plugins/quota/test_quota_balance.py
index f5c1c75d7b2..ca702bbda01 100644
--- a/test/integration/plugins/quota/test_quota_balance.py
+++ b/test/integration/plugins/quota/test_quota_balance.py
@@ -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

Reply via email to