You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@cloudstack.apache.org by da...@apache.org on 2025/01/16 14:19:37 UTC

(cloudstack) branch main updated: Create API to list Quota credits (#9590)

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

dahn 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 449d3c7cb1d Create API to list Quota credits (#9590)
449d3c7cb1d is described below

commit 449d3c7cb1df75867a3e1a9c3648fc3675c6b39e
Author: Fabricio Duarte <fa...@gmail.com>
AuthorDate: Thu Jan 16 11:19:32 2025 -0300

    Create API to list Quota credits (#9590)
    
    Co-authored-by: Bernardo De Marco Gonçalves <be...@gmail.com>
---
 .../org/apache/cloudstack/api/ApiConstants.java    |  16 +-
 .../main/java/com/cloud/domain/dao/DomainDao.java  |   2 +
 .../java/com/cloud/domain/dao/DomainDaoImpl.java   |  10 ++
 .../resources/META-INF/db/schema-42010to42100.sql  |   7 +
 .../cloudstack/quota/dao/QuotaCreditsDao.java      |   2 +-
 .../cloudstack/quota/dao/QuotaCreditsDaoImpl.java  |  54 ++++---
 .../apache/cloudstack/quota/vo/QuotaCreditsVO.java |   6 +
 .../api/command/QuotaCreditsListCmd.java           | 122 +++++++++++++++
 .../api/response/QuotaBalanceResponse.java         |   4 +-
 .../api/response/QuotaCreditsResponse.java         |  63 ++++----
 .../api/response/QuotaResponseBuilder.java         |   3 +
 .../api/response/QuotaResponseBuilderImpl.java     | 122 ++++++++++++---
 .../apache/cloudstack/quota/QuotaServiceImpl.java  |   2 +
 .../api/command/QuotaCreditsListCmdTest.java       |  79 ++++++++++
 .../api/response/QuotaResponseBuilderImplTest.java | 165 ++++++++++++++++++++-
 15 files changed, 572 insertions(+), 85 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 cf03f1d2699..a406e2d7a72 100644
--- a/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java
+++ b/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java
@@ -1192,6 +1192,14 @@ public class ApiConstants {
             "numeric value will be applied; if the result is neither a boolean nor a numeric value, the tariff will not be applied. If the rule is not informed, the tariff " +
             "value will be applied.";
 
+    public static final String PARAMETER_DESCRIPTION_START_DATE_POSSIBLE_FORMATS = "The recommended format is \"yyyy-MM-dd'T'HH:mm:ssZ\" (e.g.: \"2023-01-01T12:00:00+0100\"); " +
+            "however, the following formats are also accepted: \"yyyy-MM-dd HH:mm:ss\" (e.g.: \"2023-01-01 12:00:00\") and \"yyyy-MM-dd\" (e.g.: \"2023-01-01\" - if the time is not " +
+            "added, it will be interpreted as \"00:00:00\"). If the recommended format is not used, the date will be considered in the server timezone.";
+
+    public static final String PARAMETER_DESCRIPTION_END_DATE_POSSIBLE_FORMATS = "The recommended format is \"yyyy-MM-dd'T'HH:mm:ssZ\" (e.g.: \"2023-01-01T12:00:00+0100\"); " +
+            "however, the following formats are also accepted: \"yyyy-MM-dd HH:mm:ss\" (e.g.: \"2023-01-01 12:00:00\") and \"yyyy-MM-dd\" (e.g.: \"2023-01-01\" - if the time is not " +
+            "added, it will be interpreted as \"23:59:59\"). If the recommended format is not used, the date will be considered in the server timezone.";
+
     /**
      * This enum specifies IO Drivers, each option controls specific policies on I/O.
      * Qemu guests support "threads" and "native" options Since 0.8.8 ; "io_uring" is supported Since 6.3.0 (QEMU 5.0).
@@ -1214,14 +1222,6 @@ public class ApiConstants {
         }
     }
 
-    public static final String PARAMETER_DESCRIPTION_START_DATE_POSSIBLE_FORMATS = "The recommended format is \"yyyy-MM-dd'T'HH:mm:ssZ\" (e.g.: \"2023-01-01T12:00:00+0100\"); " +
-            "however, the following formats are also accepted: \"yyyy-MM-dd HH:mm:ss\" (e.g.: \"2023-01-01 12:00:00\") and \"yyyy-MM-dd\" (e.g.: \"2023-01-01\" - if the time is not " +
-            "added, it will be interpreted as \"00:00:00\"). If the recommended format is not used, the date will be considered in the server timezone.";
-
-    public static final String PARAMETER_DESCRIPTION_END_DATE_POSSIBLE_FORMATS = "The recommended format is \"yyyy-MM-dd'T'HH:mm:ssZ\" (e.g.: \"2023-01-01T12:00:00+0100\"); " +
-            "however, the following formats are also accepted: \"yyyy-MM-dd HH:mm:ss\" (e.g.: \"2023-01-01 12:00:00\") and \"yyyy-MM-dd\" (e.g.: \"2023-01-01\" - if the time is not " +
-            "added, it will be interpreted as \"23:59:59\"). If the recommended format is not used, the date will be considered in the server timezone.";
-
     public enum BootType {
         UEFI, BIOS;
 
diff --git a/engine/schema/src/main/java/com/cloud/domain/dao/DomainDao.java b/engine/schema/src/main/java/com/cloud/domain/dao/DomainDao.java
index 937d99f5052..3aee371da25 100644
--- a/engine/schema/src/main/java/com/cloud/domain/dao/DomainDao.java
+++ b/engine/schema/src/main/java/com/cloud/domain/dao/DomainDao.java
@@ -42,5 +42,7 @@ public interface DomainDao extends GenericDao<DomainVO, Long> {
 
     List<Long> getDomainChildrenIds(String path);
 
+    List<Long> getDomainAndChildrenIds(long domainId);
+
     boolean domainIdListContainsAccessibleDomain(String domainIdList, Account caller, Long domainId);
 }
diff --git a/engine/schema/src/main/java/com/cloud/domain/dao/DomainDaoImpl.java b/engine/schema/src/main/java/com/cloud/domain/dao/DomainDaoImpl.java
index 74f2932ca43..56d971bbe01 100644
--- a/engine/schema/src/main/java/com/cloud/domain/dao/DomainDaoImpl.java
+++ b/engine/schema/src/main/java/com/cloud/domain/dao/DomainDaoImpl.java
@@ -19,6 +19,7 @@ package com.cloud.domain.dao;
 import java.sql.PreparedStatement;
 import java.sql.ResultSet;
 import java.sql.SQLException;
+import java.util.ArrayList;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Set;
@@ -238,6 +239,15 @@ public class DomainDaoImpl extends GenericDaoBase<DomainVO, Long> implements Dom
         return customSearch(sc, null);
     }
 
+    @Override
+    public List<Long> getDomainAndChildrenIds(long domainId) {
+        DomainVO domain = findById(domainId);
+        if (domain != null) {
+            return getDomainChildrenIds(domain.getPath());
+        }
+        return new ArrayList<>();
+    }
+
     @Override
     public boolean isChildDomain(Long parentId, Long childId) {
         if ((parentId == null) || (childId == null)) {
diff --git a/engine/schema/src/main/resources/META-INF/db/schema-42010to42100.sql b/engine/schema/src/main/resources/META-INF/db/schema-42010to42100.sql
index 91223bab798..47e7bebbee4 100644
--- a/engine/schema/src/main/resources/META-INF/db/schema-42010to42100.sql
+++ b/engine/schema/src/main/resources/META-INF/db/schema-42010to42100.sql
@@ -24,3 +24,10 @@ CALL `cloud`.`IDEMPOTENT_ADD_COLUMN`('cloud.console_session', 'console_endpoint_
 
 -- Add client_address column to cloud.console_session table
 CALL `cloud`.`IDEMPOTENT_ADD_COLUMN`('cloud.console_session', 'client_address', 'VARCHAR(45)');
+
+-- Allow default roles to use quotaCreditsList
+INSERT INTO `cloud`.`role_permissions` (uuid, role_id, rule, permission, sort_order)
+SELECT uuid(), role_id, 'quotaCreditsList', permission, sort_order
+FROM `cloud`.`role_permissions` rp
+WHERE rp.rule = 'quotaStatement'
+AND NOT EXISTS(SELECT 1 FROM cloud.role_permissions rp_ WHERE rp.role_id = rp_.role_id AND rp_.rule = 'quotaCreditsList');
diff --git a/framework/quota/src/main/java/org/apache/cloudstack/quota/dao/QuotaCreditsDao.java b/framework/quota/src/main/java/org/apache/cloudstack/quota/dao/QuotaCreditsDao.java
index f08d8f96ca8..da36bc0b98c 100644
--- a/framework/quota/src/main/java/org/apache/cloudstack/quota/dao/QuotaCreditsDao.java
+++ b/framework/quota/src/main/java/org/apache/cloudstack/quota/dao/QuotaCreditsDao.java
@@ -25,7 +25,7 @@ import com.cloud.utils.db.GenericDao;
 
 public interface QuotaCreditsDao extends GenericDao<QuotaCreditsVO, Long> {
 
-    List<QuotaCreditsVO> findCredits(long accountId, long domainId, Date startDate, Date endDate);
+    List<QuotaCreditsVO> findCredits(Long accountId, Long domainId, Date startDate, Date endDate, boolean recursive);
 
     QuotaCreditsVO saveCredits(QuotaCreditsVO credits);
 
diff --git a/framework/quota/src/main/java/org/apache/cloudstack/quota/dao/QuotaCreditsDaoImpl.java b/framework/quota/src/main/java/org/apache/cloudstack/quota/dao/QuotaCreditsDaoImpl.java
index 40e18aa282b..ce51177d0ae 100644
--- a/framework/quota/src/main/java/org/apache/cloudstack/quota/dao/QuotaCreditsDaoImpl.java
+++ b/framework/quota/src/main/java/org/apache/cloudstack/quota/dao/QuotaCreditsDaoImpl.java
@@ -16,19 +16,20 @@
 //under the License.
 package org.apache.cloudstack.quota.dao;
 
-import java.util.Collections;
 import java.util.Date;
 import java.util.List;
 
 import javax.inject.Inject;
 
+import com.cloud.domain.dao.DomainDao;
+import com.cloud.utils.db.Filter;
+import com.cloud.utils.db.SearchBuilder;
 import org.apache.cloudstack.quota.vo.QuotaBalanceVO;
 import org.apache.cloudstack.quota.vo.QuotaCreditsVO;
+import org.apache.commons.lang3.ObjectUtils;
 import org.springframework.stereotype.Component;
 
-import com.cloud.utils.db.Filter;
 import com.cloud.utils.db.GenericDaoBase;
-import com.cloud.utils.db.QueryBuilder;
 import com.cloud.utils.db.SearchCriteria;
 import com.cloud.utils.db.Transaction;
 import com.cloud.utils.db.TransactionCallback;
@@ -39,25 +40,36 @@ import com.cloud.utils.db.TransactionStatus;
 public class QuotaCreditsDaoImpl extends GenericDaoBase<QuotaCreditsVO, Long> implements QuotaCreditsDao {
 
     @Inject
-    QuotaBalanceDao _quotaBalanceDao;
+    DomainDao domainDao;
+    @Inject
+    QuotaBalanceDao quotaBalanceDao;
+
+    private SearchBuilder<QuotaCreditsVO> quotaCreditsVoSearch;
+
+    public QuotaCreditsDaoImpl() {
+        quotaCreditsVoSearch = createSearchBuilder();
+        quotaCreditsVoSearch.and("updatedOn", quotaCreditsVoSearch.entity().getUpdatedOn(), SearchCriteria.Op.BETWEEN);
+        quotaCreditsVoSearch.and("accountId", quotaCreditsVoSearch.entity().getAccountId(), SearchCriteria.Op.EQ);
+        quotaCreditsVoSearch.and("domainId", quotaCreditsVoSearch.entity().getDomainId(), SearchCriteria.Op.IN);
+        quotaCreditsVoSearch.done();
+    }
 
     @Override
-    public List<QuotaCreditsVO> findCredits(final long accountId, final long domainId, final Date startDate, final Date endDate) {
-        return Transaction.execute(TransactionLegacy.USAGE_DB, new TransactionCallback<List<QuotaCreditsVO>>() {
-            @Override
-            public List<QuotaCreditsVO> doInTransaction(final TransactionStatus status) {
-                if ((startDate != null) && (endDate != null) && startDate.before(endDate)) {
-                    Filter filter = new Filter(QuotaCreditsVO.class, "updatedOn", true, 0L, Long.MAX_VALUE);
-                    QueryBuilder<QuotaCreditsVO> qb = QueryBuilder.create(QuotaCreditsVO.class);
-                    qb.and(qb.entity().getAccountId(), SearchCriteria.Op.EQ, accountId);
-                    qb.and(qb.entity().getDomainId(), SearchCriteria.Op.EQ, domainId);
-                    qb.and(qb.entity().getUpdatedOn(), SearchCriteria.Op.BETWEEN, startDate, endDate);
-                    return search(qb.create(), filter);
-                } else {
-                    return Collections.<QuotaCreditsVO> emptyList();
-                }
-            }
-        });
+    public List<QuotaCreditsVO> findCredits(Long accountId, Long domainId, Date startDate, Date endDate, boolean recursive) {
+        SearchCriteria<QuotaCreditsVO> sc = quotaCreditsVoSearch.create();
+        Filter filter = new Filter(QuotaCreditsVO.class, "updatedOn", true, 0L, Long.MAX_VALUE);
+
+        sc.setParametersIfNotNull("accountId", accountId);
+        if (domainId != null) {
+            List<Long> domainIds = recursive ? domainDao.getDomainAndChildrenIds(domainId) : List.of(domainId);
+            sc.setParameters("domainId", domainIds.toArray());
+        }
+
+        if (ObjectUtils.allNotNull(startDate, endDate)) {
+            sc.setParameters("updatedOn", startDate, endDate);
+        }
+
+        return Transaction.execute(TransactionLegacy.USAGE_DB, (TransactionCallback<List<QuotaCreditsVO>>) status -> search(sc, filter));
     }
 
     @Override
@@ -68,7 +80,7 @@ public class QuotaCreditsDaoImpl extends GenericDaoBase<QuotaCreditsVO, Long> im
                 persist(credits);
                 // make an entry in the balance table
                 QuotaBalanceVO bal = new QuotaBalanceVO(credits);
-                _quotaBalanceDao.persist(bal);
+                quotaBalanceDao.persist(bal);
                 return credits;
             }
         });
diff --git a/framework/quota/src/main/java/org/apache/cloudstack/quota/vo/QuotaCreditsVO.java b/framework/quota/src/main/java/org/apache/cloudstack/quota/vo/QuotaCreditsVO.java
index f9c7b45b8a4..5cf6b1f0e57 100644
--- a/framework/quota/src/main/java/org/apache/cloudstack/quota/vo/QuotaCreditsVO.java
+++ b/framework/quota/src/main/java/org/apache/cloudstack/quota/vo/QuotaCreditsVO.java
@@ -17,6 +17,7 @@
 package org.apache.cloudstack.quota.vo;
 
 import org.apache.cloudstack.api.InternalIdentity;
+import org.apache.cloudstack.utils.reflectiontostringbuilderutils.ReflectionToStringBuilderUtils;
 
 import javax.persistence.Column;
 import javax.persistence.Entity;
@@ -113,4 +114,9 @@ public class QuotaCreditsVO implements InternalIdentity {
     public long getId() {
         return this.id;
     }
+
+    @Override
+    public String toString() {
+        return ReflectionToStringBuilderUtils.reflectOnlySelectedFields(this, "id", "accountId", "domainId", "credit");
+    }
 }
diff --git a/plugins/database/quota/src/main/java/org/apache/cloudstack/api/command/QuotaCreditsListCmd.java b/plugins/database/quota/src/main/java/org/apache/cloudstack/api/command/QuotaCreditsListCmd.java
new file mode 100644
index 00000000000..48bb7ef79e7
--- /dev/null
+++ b/plugins/database/quota/src/main/java/org/apache/cloudstack/api/command/QuotaCreditsListCmd.java
@@ -0,0 +1,122 @@
+//Licensed to the Apache Software Foundation (ASF) under one
+//or more contributor license agreements.  See the NOTICE file
+//distributed with this work for additional information
+//regarding copyright ownership.  The ASF licenses this file
+//to you under the Apache License, Version 2.0 (the
+//"License"); you may not use this file except in compliance
+//with the License.  You may obtain a copy of the License at
+//
+//http://www.apache.org/licenses/LICENSE-2.0
+//
+//Unless required by applicable law or agreed to in writing,
+//software distributed under the License is distributed on an
+//"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+//KIND, either express or implied.  See the License for the
+//specific language governing permissions and limitations
+//under the License.
+package org.apache.cloudstack.api.command;
+
+import com.cloud.utils.Pair;
+
+import org.apache.cloudstack.api.ACL;
+import org.apache.cloudstack.api.APICommand;
+import org.apache.cloudstack.api.ApiConstants;
+import org.apache.cloudstack.api.BaseCmd;
+import org.apache.cloudstack.api.Parameter;
+import org.apache.cloudstack.api.response.AccountResponse;
+import org.apache.cloudstack.api.response.DomainResponse;
+import org.apache.cloudstack.api.response.ListResponse;
+import org.apache.cloudstack.api.response.QuotaCreditsResponse;
+import org.apache.cloudstack.api.response.QuotaResponseBuilder;
+import org.apache.commons.lang3.ObjectUtils;
+import org.apache.commons.lang3.time.DateUtils;
+
+import java.util.Calendar;
+import java.util.Date;
+import java.util.List;
+
+import javax.inject.Inject;
+
+@APICommand(name = "quotaCreditsList", responseObject = QuotaCreditsResponse.class, description = "Lists quota credits of an account.", since = "4.21.0",
+        requestHasSensitiveInfo = false, responseHasSensitiveInfo = false)
+public class QuotaCreditsListCmd extends BaseCmd {
+
+    @Inject
+    QuotaResponseBuilder quotaResponseBuilder;
+
+    @ACL
+    @Parameter(name = ApiConstants.ACCOUNT_ID, type = CommandType.UUID, entityType = AccountResponse.class, description = "ID of the account for which the credit statement will be generated.")
+    private Long accountId;
+
+    @ACL
+    @Parameter(name = ApiConstants.DOMAIN_ID, type = CommandType.UUID, entityType = DomainResponse.class, description = "ID of the domain for which credit statement will be generated. " +
+            "Available only for administrators.")
+    private Long domainId;
+
+    @Parameter(name = ApiConstants.END_DATE, type = CommandType.DATE, description = "End date of the credit statement. If not provided, the current date will be " +
+            "considered as the end date. " + ApiConstants.PARAMETER_DESCRIPTION_END_DATE_POSSIBLE_FORMATS)
+    private Date endDate;
+
+    @Parameter(name = ApiConstants.START_DATE, type = CommandType.DATE, description = "Start date of the credit statement. If not provided, the first day of the current month " +
+            "will be considered as the start date. " + ApiConstants.PARAMETER_DESCRIPTION_START_DATE_POSSIBLE_FORMATS)
+    private Date startDate;
+
+    @Parameter(name = ApiConstants.IS_RECURSIVE, type = CommandType.BOOLEAN, description = "Whether to generate the credit statement for the provided domain and its children. " +
+            "Defaults to false.")
+    private Boolean recursive = false;
+
+    public Long getAccountId() {
+        return accountId;
+    }
+
+    public void setAccountId(Long accountId) {
+        this.accountId = accountId;
+    }
+
+    public Long getDomainId() {
+        return domainId;
+    }
+
+    public void setDomainId(Long domainId) {
+        this.domainId = domainId;
+    }
+
+    public Date getEndDate() {
+        return ObjectUtils.defaultIfNull(endDate, new Date());
+    }
+
+    public void setEndDate(Date endDate) {
+        this.endDate = endDate;
+    }
+
+    public Date getStartDate() {
+        return ObjectUtils.defaultIfNull(startDate, DateUtils.truncate(new Date(), Calendar.MONTH));
+    }
+
+    public void setStartDate(Date startDate) {
+        this.startDate = startDate;
+    }
+
+    public Boolean getRecursive() {
+        return recursive;
+    }
+
+    public void setRecursive(Boolean recursive) {
+        this.recursive = recursive;
+    }
+
+    @Override
+    public void execute() {
+        Pair<List<QuotaCreditsResponse>, Integer> responses = quotaResponseBuilder.createQuotaCreditsListResponse(this);
+        ListResponse<QuotaCreditsResponse> response = new ListResponse<>();
+        response.setResponses(responses.first(), responses.second());
+        response.setResponseName(getCommandName());
+        setResponseObject(response);
+    }
+
+    @Override
+    public long getEntityOwnerId() {
+        return -1;
+    }
+
+}
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 fca6b6cbb1a..4c6cc750d99 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
@@ -122,8 +122,8 @@ public class QuotaBalanceResponse extends BaseResponse {
 
     public void addCredits(QuotaBalanceVO credit) {
         QuotaCreditsResponse cr = new QuotaCreditsResponse();
-        cr.setCredits(credit.getCreditBalance());
-        cr.setUpdatedOn(credit.getUpdatedOn() == null ? null : new Date(credit.getUpdatedOn().getTime()));
+        cr.setCredit(credit.getCreditBalance());
+        cr.setCreditedOn(credit.getUpdatedOn());
         credits.add(0, cr);
     }
 
diff --git a/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaCreditsResponse.java b/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaCreditsResponse.java
index 561937ddcca..c81f777268f 100644
--- a/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaCreditsResponse.java
+++ b/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaCreditsResponse.java
@@ -20,65 +20,62 @@ import com.cloud.serializer.Param;
 import com.google.gson.annotations.SerializedName;
 
 import org.apache.cloudstack.api.BaseResponse;
-import org.apache.cloudstack.quota.vo.QuotaCreditsVO;
 
 import java.math.BigDecimal;
-import java.math.RoundingMode;
 import java.util.Date;
 
 public class QuotaCreditsResponse extends BaseResponse {
 
-    @SerializedName("credits")
-    @Param(description = "the credit deposited")
-    private BigDecimal credits;
+    @SerializedName("credit")
+    @Param(description = "The credit deposited.")
+    private BigDecimal credit;
 
-    @SerializedName("updated_by")
-    @Param(description = "the user name of the admin who updated the credits")
-    private String updatedBy;
+    @SerializedName("creditoruserid")
+    @Param(description = "ID of the creditor user.")
+    private String creditorUserId;
 
-    @SerializedName("updated_on")
-    @Param(description = "the account name of the admin who updated the credits")
-    private Date updatedOn;
+    @SerializedName("creditorusername")
+    @Param(description = "Username of the creditor user.")
+    private String creditorUsername;
+
+    @SerializedName("creditedon")
+    @Param(description = "When the credit was added.")
+    private Date creditedOn;
 
     @SerializedName("currency")
-    @Param(description = "currency")
+    @Param(description = "Credit's currency.")
     private String currency;
 
-    public QuotaCreditsResponse() {
-        super();
+    public BigDecimal getCredit() {
+        return credit;
     }
 
-    public QuotaCreditsResponse(QuotaCreditsVO result, String updatedBy) {
-        super();
-        if (result != null) {
-            setCredits(result.getCredit());
-            setUpdatedBy(updatedBy);
-            setUpdatedOn(new Date());
-        }
+    public void setCredit(BigDecimal credit) {
+        this.credit = credit;
     }
 
-    public BigDecimal getCredits() {
-        return credits;
+    public String getCreditorUserId() {
+        return creditorUserId;
     }
 
-    public void setCredits(BigDecimal credits) {
-        this.credits = credits.setScale(2, RoundingMode.HALF_EVEN);
+    public void setCreditorUserId(String creditorUserId) {
+        this.creditorUserId = creditorUserId;
     }
 
-    public String getUpdatedBy() {
-        return updatedBy;
+    public String getCreditorUsername() {
+        return creditorUsername;
     }
 
-    public void setUpdatedBy(String updatedBy) {
-        this.updatedBy = updatedBy;
+    public void setCreditorUsername(String creditorUsername) {
+        this.creditorUsername = creditorUsername;
     }
 
-    public Date getUpdatedOn() {
-        return updatedOn;
+    public Date getCreditedOn() {
+        return creditedOn;
     }
 
-    public void setUpdatedOn(Date updatedOn) {
-        this.updatedOn = updatedOn;
+    public void setCreditedOn(Date creditedOn) {
+        this.creditedOn = creditedOn;
     }
 
     public String getCurrency() {
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 56935a1360c..67f75ebf82f 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
@@ -19,6 +19,7 @@ package org.apache.cloudstack.api.response;
 import com.cloud.user.User;
 import org.apache.cloudstack.api.command.QuotaBalanceCmd;
 import org.apache.cloudstack.api.command.QuotaConfigureEmailCmd;
+import org.apache.cloudstack.api.command.QuotaCreditsListCmd;
 import org.apache.cloudstack.api.command.QuotaEmailTemplateListCmd;
 import org.apache.cloudstack.api.command.QuotaEmailTemplateUpdateCmd;
 import org.apache.cloudstack.api.command.QuotaPresetVariablesListCmd;
@@ -90,5 +91,7 @@ public interface QuotaResponseBuilder {
 
     List<QuotaConfigureEmailResponse> listEmailConfiguration(long accountId);
 
+    Pair<List<QuotaCreditsResponse>, Integer> createQuotaCreditsListResponse(QuotaCreditsListCmd cmd);
+
     QuotaValidateActivationRuleResponse validateActivationRule(QuotaValidateActivationRuleCmd cmd);
 }
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 733f7792356..7a987df0a35 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
@@ -42,12 +42,16 @@ import java.util.stream.Collectors;
 
 import javax.inject.Inject;
 
+import com.cloud.exception.PermissionDeniedException;
+import com.cloud.user.User;
+import com.cloud.user.UserVO;
 import com.cloud.utils.DateUtil;
 import com.cloud.utils.exception.CloudRuntimeException;
 import org.apache.cloudstack.api.ApiErrorCode;
 import org.apache.cloudstack.api.ServerApiException;
 import org.apache.cloudstack.api.command.QuotaBalanceCmd;
 import org.apache.cloudstack.api.command.QuotaConfigureEmailCmd;
+import org.apache.cloudstack.api.command.QuotaCreditsListCmd;
 import org.apache.cloudstack.api.command.QuotaEmailTemplateListCmd;
 import org.apache.cloudstack.api.command.QuotaEmailTemplateUpdateCmd;
 import org.apache.cloudstack.api.command.QuotaPresetVariablesListCmd;
@@ -99,7 +103,6 @@ import com.cloud.exception.InvalidParameterValueException;
 import com.cloud.user.Account;
 import com.cloud.user.AccountManager;
 import com.cloud.user.AccountVO;
-import com.cloud.user.User;
 import com.cloud.user.dao.AccountDao;
 import com.cloud.user.dao.UserDao;
 import com.cloud.utils.Pair;
@@ -116,7 +119,7 @@ public class QuotaResponseBuilderImpl implements QuotaResponseBuilder {
     @Inject
     private QuotaBalanceDao _quotaBalanceDao;
     @Inject
-    private QuotaCreditsDao _quotaCreditsDao;
+    private QuotaCreditsDao quotaCreditsDao;
     @Inject
     private QuotaUsageDao _quotaUsageDao;
     @Inject
@@ -548,28 +551,32 @@ public class QuotaResponseBuilderImpl implements QuotaResponseBuilder {
 
     @Override
     public QuotaCreditsResponse addQuotaCredits(Long accountId, Long domainId, Double amount, Long updatedBy, Boolean enforce) {
-        Date despositedOn = new Date();
-        QuotaBalanceVO qb = _quotaBalanceDao.findLaterBalanceEntry(accountId, domainId, despositedOn);
+        Date depositedOn = new Date();
+        QuotaBalanceVO qb = _quotaBalanceDao.findLaterBalanceEntry(accountId, domainId, depositedOn);
 
         if (qb != null) {
             throw new InvalidParameterValueException(String.format("Incorrect deposit date [%s], as there are balance entries after this date.",
-                    despositedOn));
+                    depositedOn));
         }
 
         QuotaCreditsVO credits = new QuotaCreditsVO(accountId, domainId, new BigDecimal(amount), updatedBy);
-        credits.setUpdatedOn(despositedOn);
-        QuotaCreditsVO result = _quotaCreditsDao.saveCredits(credits);
+        credits.setUpdatedOn(depositedOn);
+        QuotaCreditsVO result = quotaCreditsDao.saveCredits(credits);
+        if (result == null) {
+            logger.error("Unable to add credits to account ID [{}].", accountId);
+            throw new CloudRuntimeException("Unable to add credits to account.");
+        }
 
         final AccountVO account = _accountDao.findById(accountId);
         if (account == null) {
             throw new InvalidParameterValueException("Account does not exist with account id " + accountId);
         }
         final boolean lockAccountEnforcement = "true".equalsIgnoreCase(QuotaConfig.QuotaEnableEnforcement.value());
-        final BigDecimal currentAccountBalance = _quotaBalanceDao.lastQuotaBalance(accountId, domainId, startOfNextDay(new Date(despositedOn.getTime())));
+        final BigDecimal currentAccountBalance = _quotaBalanceDao.lastQuotaBalance(accountId, domainId, startOfNextDay(new Date(depositedOn.getTime())));
         logger.debug("Depositing [{}] credits on adjusted date [{}]; current balance is [{}].", amount,
-                DateUtil.displayDateInTimezone(QuotaManagerImpl.getUsageAggregationTimeZone(), despositedOn), currentAccountBalance);
+                DateUtil.displayDateInTimezone(QuotaManagerImpl.getUsageAggregationTimeZone(), depositedOn), currentAccountBalance);
         // update quota account with the balance
-        _quotaService.saveQuotaAccount(account, currentAccountBalance, despositedOn);
+        _quotaService.saveQuotaAccount(account, currentAccountBalance, depositedOn);
         if (lockAccountEnforcement) {
             if (currentAccountBalance.compareTo(new BigDecimal(0)) >= 0) {
                 if (account.getState() == Account.State.LOCKED) {
@@ -584,14 +591,8 @@ public class QuotaResponseBuilderImpl implements QuotaResponseBuilder {
             }
         }
 
-        String creditor = String.valueOf(Account.ACCOUNT_ID_SYSTEM);
-        User creditorUser = _userDao.getUser(updatedBy);
-        if (creditorUser != null) {
-            creditor = creditorUser.getUsername();
-        }
-        QuotaCreditsResponse response = new QuotaCreditsResponse(result, creditor);
-        response.setCurrency(QuotaConfig.QuotaCurrencySymbol.value());
-        return response;
+        UserVO creditor = getCreditorForQuotaCredits(result);
+        return createQuotaCreditsResponse(result, creditor);
     }
 
     private QuotaEmailTemplateResponse createQuotaEmailResponse(QuotaEmailTemplatesVO template) {
@@ -938,6 +939,91 @@ public class QuotaResponseBuilderImpl implements QuotaResponseBuilder {
         return quotaConfigureEmailResponse;
     }
 
+    @Override
+    public Pair<List<QuotaCreditsResponse>, Integer> createQuotaCreditsListResponse(QuotaCreditsListCmd cmd) {
+        List<QuotaCreditsVO> credits = getCreditsForQuotaCreditsList(cmd);
+
+        List<QuotaCreditsResponse> creditResponses = new ArrayList<>();
+        Map<Long, UserVO> userMap = new HashMap<>();
+
+        for (QuotaCreditsVO credit : credits) {
+            UserVO creditor = getCreditorForQuotaCreditsList(credit, userMap);
+            QuotaCreditsResponse response = createQuotaCreditsResponse(credit, creditor);
+            creditResponses.add(response);
+        }
+
+        return new Pair<>(creditResponses, creditResponses.size());
+    }
+
+    protected List<QuotaCreditsVO> getCreditsForQuotaCreditsList(QuotaCreditsListCmd cmd) {
+        Long accountId = cmd.getAccountId();
+        Long domainId = cmd.getDomainId();
+        Date startDate = cmd.getStartDate();
+        Date endDate = cmd.getEndDate();
+        boolean isRecursive = cmd.getRecursive();
+
+        if (ObjectUtils.allNull(accountId, domainId)) {
+            throw new InvalidParameterValueException("Please provide either account ID or domain ID.");
+        }
+
+        if (startDate.after(endDate)) {
+            throw new InvalidParameterValueException("The start date must be before the end date.");
+        }
+
+        Account caller = CallContext.current().getCallingAccount();
+        if (domainId != null && _accountMgr.isNormalUser(caller.getAccountId())) {
+            throw new PermissionDeniedException("Regular users are not allowed to generate domain statements.");
+        }
+
+        return quotaCreditsDao.findCredits(accountId, domainId, startDate, endDate, isRecursive);
+    }
+
+    /**
+     * Returns the creditor user of a <code>QuotaCreditsVO</code>. If <code>userMap</code> contains the user, returns the
+     * user from the map; otherwise, obtains the user from the database and adds it to the map.
+     */
+    protected UserVO getCreditorForQuotaCreditsList(QuotaCreditsVO credit, Map<Long, UserVO> userMap) {
+        Long creditorUserId = credit.getUpdatedBy();
+
+        UserVO userVo = userMap.get(creditorUserId);
+        if (userVo != null) {
+            return userVo;
+        }
+
+        userVo = getCreditorForQuotaCredits(credit);
+        userMap.put(creditorUserId, userVo);
+        return userVo;
+    }
+
+    /**
+     * Returns the creditor user of a <code>QuotaCreditsVO</code> by obtaining it from the database.
+     */
+    protected UserVO getCreditorForQuotaCredits(QuotaCreditsVO credit) {
+        Long creditorUserId = credit.getUpdatedBy();
+        UserVO userVo = _userDao.findByIdIncludingRemoved(creditorUserId);
+        if (userVo == null) {
+            logger.error("Could not find creditor user with ID [{}] for credit [{}].", creditorUserId, credit.toString());
+            throw new CloudRuntimeException("Could not find creditor user.");
+        }
+        return userVo;
+    }
+
+    protected QuotaCreditsResponse createQuotaCreditsResponse(QuotaCreditsVO credit, UserVO creditor) {
+        QuotaCreditsResponse response = new QuotaCreditsResponse();
+
+        if (credit != null) {
+            response.setCredit(credit.getCredit());
+            response.setCreditedOn(credit.getUpdatedOn());
+            response.setCurrency(QuotaConfig.QuotaCurrencySymbol.value());
+        }
+        if (creditor != null) {
+            response.setCreditorUserId(creditor.getUuid());
+            response.setCreditorUsername(creditor.getUsername());
+        }
+        response.setObjectName("credit");
+        return response;
+    }
+
     @Override
     public QuotaValidateActivationRuleResponse validateActivationRule(QuotaValidateActivationRuleCmd cmd) {
         String message;
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 97d77b8aa22..e02f260c142 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
@@ -29,6 +29,7 @@ import javax.naming.ConfigurationException;
 import org.apache.cloudstack.api.command.QuotaBalanceCmd;
 import org.apache.cloudstack.api.command.QuotaConfigureEmailCmd;
 import org.apache.cloudstack.api.command.QuotaCreditsCmd;
+import org.apache.cloudstack.api.command.QuotaCreditsListCmd;
 import org.apache.cloudstack.api.command.QuotaEmailTemplateListCmd;
 import org.apache.cloudstack.api.command.QuotaEmailTemplateUpdateCmd;
 import org.apache.cloudstack.api.command.QuotaEnabledCmd;
@@ -115,6 +116,7 @@ public class QuotaServiceImpl extends ManagerBase implements QuotaService, Confi
         cmdList.add(QuotaTariffListCmd.class);
         cmdList.add(QuotaTariffUpdateCmd.class);
         cmdList.add(QuotaCreditsCmd.class);
+        cmdList.add(QuotaCreditsListCmd.class);
         cmdList.add(QuotaEmailTemplateListCmd.class);
         cmdList.add(QuotaEmailTemplateUpdateCmd.class);
         cmdList.add(QuotaTariffCreateCmd.class);
diff --git a/plugins/database/quota/src/test/java/org/apache/cloudstack/api/command/QuotaCreditsListCmdTest.java b/plugins/database/quota/src/test/java/org/apache/cloudstack/api/command/QuotaCreditsListCmdTest.java
new file mode 100644
index 00000000000..26621a52dbc
--- /dev/null
+++ b/plugins/database/quota/src/test/java/org/apache/cloudstack/api/command/QuotaCreditsListCmdTest.java
@@ -0,0 +1,79 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package org.apache.cloudstack.api.command;
+
+import java.util.Calendar;
+import java.util.Date;
+
+import org.apache.commons.lang3.time.DateUtils;
+import org.junit.Assert;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.MockedConstruction;
+import org.mockito.Mockito;
+import org.mockito.Spy;
+import org.mockito.junit.MockitoJUnitRunner;
+
+@RunWith(MockitoJUnitRunner.class)
+public class QuotaCreditsListCmdTest {
+
+    @Spy
+    QuotaCreditsListCmd quotaCreditsListCmdSpy;
+
+    @Test
+    public void getEndDateTestReturnsNewDateWhenEndDateIsNull() {
+        quotaCreditsListCmdSpy.setEndDate(null);
+
+        try (MockedConstruction<Date> dateMockedConstruction = Mockito.mockConstruction(Date.class)) {
+            Date result = quotaCreditsListCmdSpy.getEndDate();
+
+            Assert.assertEquals(1, dateMockedConstruction.constructed().size());
+            Assert.assertEquals(dateMockedConstruction.constructed().get(0), result);
+        }
+    }
+
+    @Test
+    public void getEndDateTestReturnsEndDateWhenItIsNotNull() {
+        Date expected = new Date();
+        quotaCreditsListCmdSpy.setEndDate(expected);
+
+        Date result = quotaCreditsListCmdSpy.getEndDate();
+
+        Assert.assertEquals(expected, result);
+    }
+
+    @Test
+    public void getStartDateTestReturnsFirstDayOfTheCurrentMonthWhenStartDateIsNull() {
+        quotaCreditsListCmdSpy.setStartDate(null);
+        Date expected = DateUtils.truncate(new Date(), Calendar.MONTH);
+
+        Date result = quotaCreditsListCmdSpy.getStartDate();
+
+        Assert.assertEquals(expected, result);
+    }
+
+    @Test
+    public void getStartDateTestReturnsStartDateWhenItIsNotNull() {
+        Date expected = new Date();
+        quotaCreditsListCmdSpy.setStartDate(expected);
+
+        Date result = quotaCreditsListCmdSpy.getStartDate();
+
+        Assert.assertEquals(expected, result);
+    }
+}
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 a26e6c0476d..1f5480404e4 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
@@ -24,6 +24,7 @@ import java.time.ZoneId;
 import java.util.ArrayList;
 import java.util.Calendar;
 import java.util.Date;
+import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
@@ -32,12 +33,18 @@ import java.util.function.Consumer;
 
 import com.cloud.domain.DomainVO;
 import com.cloud.domain.dao.DomainDao;
+import com.cloud.exception.PermissionDeniedException;
+import com.cloud.user.AccountManager;
+import com.cloud.user.UserVO;
 import com.cloud.utils.Pair;
+import com.cloud.utils.exception.CloudRuntimeException;
 import org.apache.cloudstack.api.ServerApiException;
 import org.apache.cloudstack.api.command.QuotaConfigureEmailCmd;
+import org.apache.cloudstack.api.command.QuotaCreditsListCmd;
 import org.apache.cloudstack.api.command.QuotaEmailTemplateListCmd;
 import org.apache.cloudstack.api.command.QuotaEmailTemplateUpdateCmd;
 import org.apache.cloudstack.api.command.QuotaValidateActivationRuleCmd;
+import org.apache.cloudstack.context.CallContext;
 import org.apache.cloudstack.discovery.ApiDiscoveryService;
 import org.apache.cloudstack.framework.config.ConfigKey;
 import org.apache.cloudstack.jsinterpreter.JsInterpreterHelper;
@@ -66,6 +73,7 @@ import org.apache.cloudstack.utils.jsinterpreter.JsInterpreter;
 import org.apache.commons.lang3.time.DateUtils;
 
 import org.junit.Assert;
+import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.InjectMocks;
@@ -81,6 +89,7 @@ import com.cloud.user.dao.UserDao;
 import com.cloud.user.User;
 
 import junit.framework.TestCase;
+import org.mockito.Spy;
 import org.mockito.junit.MockitoJUnitRunner;
 
 @RunWith(MockitoJUnitRunner.class)
@@ -135,7 +144,8 @@ public class QuotaResponseBuilderImplTest extends TestCase {
     QuotaEmailConfigurationDao quotaEmailConfigurationDaoMock;
 
     @InjectMocks
-    QuotaResponseBuilderImpl quotaResponseBuilderSpy = Mockito.spy(QuotaResponseBuilderImpl.class);
+    @Spy
+    QuotaResponseBuilderImpl quotaResponseBuilderSpy;
 
     Date date = new Date();
 
@@ -154,6 +164,26 @@ public class QuotaResponseBuilderImplTest extends TestCase {
     @Mock
     QuotaEmailTemplatesVO quotaEmailTemplatesVoMock;
 
+    @Mock
+    QuotaCreditsVO quotaCreditsVoMock;
+
+    @Mock
+    UserVO userVoMock;
+
+    @Mock
+    AccountManager accountManagerMock;
+
+    @Mock
+    Account callerAccountMock;
+
+    @Mock
+    User callerUserMock;
+
+    @Before
+    public void setup() {
+        CallContext.register(callerUserMock, callerAccountMock);
+    }
+
     private void overrideDefaultQuotaEnabledConfigValue(final Object value) throws IllegalAccessException, NoSuchFieldException {
         Field f = ConfigKey.class.getDeclaredField("_defaultValue");
         f.setAccessible(true);
@@ -221,13 +251,14 @@ public class QuotaResponseBuilderImplTest extends TestCase {
 
         Mockito.when(quotaCreditsDaoMock.saveCredits(Mockito.any(QuotaCreditsVO.class))).thenReturn(credit);
         Mockito.when(quotaBalanceDaoMock.lastQuotaBalance(Mockito.anyLong(), Mockito.anyLong(), Mockito.any(Date.class))).thenReturn(new BigDecimal(111));
+        Mockito.doReturn(userVoMock).when(quotaResponseBuilderSpy).getCreditorForQuotaCredits(credit);
 
         AccountVO account = new AccountVO();
         account.setState(Account.State.LOCKED);
         Mockito.when(accountDaoMock.findById(Mockito.anyLong())).thenReturn(account);
 
         QuotaCreditsResponse resp = quotaResponseBuilderSpy.addQuotaCredits(accountId, domainId, amount, updatedBy, true);
-        assertTrue(resp.getCredits().compareTo(credit.getCredit()) == 0);
+        assertTrue(resp.getCredit().compareTo(credit.getCredit()) == 0);
     }
 
     @Test
@@ -658,6 +689,136 @@ public class QuotaResponseBuilderImplTest extends TestCase {
         assertFalse(quotaResponseBuilderSpy.isUserAllowedToSeeActivationRules(userMock));
     }
 
+    @Test
+    public void createQuotaCreditsListResponseTestReturnsObject() {
+        List<QuotaCreditsVO> credits = new ArrayList<>();
+        credits.add(new QuotaCreditsVO());
+        QuotaCreditsResponse expectedQuotaCreditsResponse = new QuotaCreditsResponse();
+
+        Mockito.doReturn(credits).when(quotaResponseBuilderSpy).getCreditsForQuotaCreditsList(Mockito.any());
+        Mockito.doReturn(userVoMock).when(quotaResponseBuilderSpy).getCreditorForQuotaCreditsList(Mockito.any(), Mockito.any());
+        Mockito.doReturn(expectedQuotaCreditsResponse).when(quotaResponseBuilderSpy).createQuotaCreditsResponse(credits.get(0), userVoMock);
+
+        Pair<List<QuotaCreditsResponse>, Integer> result = quotaResponseBuilderSpy.createQuotaCreditsListResponse(createQuotaCreditsListCmdForTests());
+
+        Assert.assertEquals(expectedQuotaCreditsResponse, result.first().get(0));
+        Assert.assertEquals(1, (int) result.second());
+    }
+
+    private QuotaCreditsListCmd createQuotaCreditsListCmdForTests() {
+        Mockito.doReturn(false).when(accountManagerMock).isNormalUser(Mockito.anyLong());
+        QuotaCreditsListCmd cmd = new QuotaCreditsListCmd();
+        cmd.setAccountId(1L);
+        cmd.setDomainId(2L);
+        return cmd;
+    }
+
+    @Test(expected = InvalidParameterValueException.class)
+    public void getCreditsForQuotaCreditsListTestThrowsInvalidParameterValueExceptionWhenBothAccountIdAndDomainIdAreNull() {
+        QuotaCreditsListCmd cmd = new QuotaCreditsListCmd();
+
+        quotaResponseBuilderSpy.getCreditsForQuotaCreditsList(cmd);
+    }
+
+    @Test(expected = InvalidParameterValueException.class)
+    public void getCreditsForQuotaCreditsListTestThrowsInvalidParameterValueExceptionWhenStartDateIsAfterEndDate() {
+        QuotaCreditsListCmd cmd = createQuotaCreditsListCmdForTests();
+        cmd.setStartDate(new Date());
+        cmd.setEndDate(DateUtils.addDays(new Date(), -1));
+
+        quotaResponseBuilderSpy.getCreditsForQuotaCreditsList(cmd);
+    }
+
+    @Test(expected = PermissionDeniedException.class)
+    public void getCreditsForQuotaCreditsListTestThrowsPermissionDeniedExceptionWhenDomainIdIsProvidedAndCallerIsNormalUser() {
+        QuotaCreditsListCmd cmd = createQuotaCreditsListCmdForTests();
+        Mockito.doReturn(true).when(accountManagerMock).isNormalUser(Mockito.anyLong());
+
+        quotaResponseBuilderSpy.getCreditsForQuotaCreditsList(cmd);
+    }
+
+    @Test
+    public void getCreditsForQuotaCreditsListTestReturnsData() {
+        QuotaCreditsListCmd cmd = createQuotaCreditsListCmdForTests();
+        List<QuotaCreditsVO> expected = new ArrayList<>();
+        expected.add(new QuotaCreditsVO());
+
+        Mockito.doReturn(expected).when(quotaCreditsDaoMock).findCredits(Mockito.anyLong(), Mockito.anyLong(), Mockito.any(), Mockito.any(), Mockito.anyBoolean());
+
+        List<QuotaCreditsVO> result = quotaResponseBuilderSpy.getCreditsForQuotaCreditsList(cmd);
+
+        Assert.assertEquals(expected, result);
+    }
+
+    @Test
+    public void getCreditorForQuotaCreditsListTestReturnsUserFromMapWhenMapHasCreditor() {
+        Long creditorId = 1L;
+        Map<Long, UserVO> userMap = new HashMap<>();
+
+        userMap.put(creditorId, userVoMock);
+        Mockito.doReturn(creditorId).when(quotaCreditsVoMock).getUpdatedBy();
+
+        UserVO result = quotaResponseBuilderSpy.getCreditorForQuotaCreditsList(quotaCreditsVoMock, userMap);
+
+        Assert.assertEquals(userVoMock, result);
+    }
+
+    @Test
+    public void getCreditorForQuotaCreditsListTestGetsCreditorFromDatabaseAndAddsItToMapWhenMapDoesNotHaveCreditor() {
+        Long creditorId = 1L;
+        Map<Long, UserVO> userMap = new HashMap<>();
+
+        Mockito.doReturn(creditorId).when(quotaCreditsVoMock).getUpdatedBy();
+        Mockito.doReturn(userVoMock).when(userDaoMock).findByIdIncludingRemoved(creditorId);
+
+        UserVO result = quotaResponseBuilderSpy.getCreditorForQuotaCreditsList(quotaCreditsVoMock, userMap);
+
+        Assert.assertEquals(userVoMock, result);
+        Assert.assertEquals(userVoMock, userMap.get(creditorId));
+    }
+
+    @Test
+    public void getCreditorForQuotaCreditsTestReturnsCreditorWhenCreditorExists() {
+        Long creditorId = 1L;
+
+        Mockito.when(quotaCreditsVoMock.getUpdatedBy()).thenReturn(creditorId);
+        Mockito.doReturn(userVoMock).when(userDaoMock).findByIdIncludingRemoved(creditorId);
+
+        UserVO result = quotaResponseBuilderSpy.getCreditorForQuotaCredits(quotaCreditsVoMock);
+
+        Assert.assertEquals(userVoMock, result);
+    }
+
+    @Test(expected = CloudRuntimeException.class)
+    public void getCreditorForQuotaCreditsTestThrowsCloudRuntimeExceptionWhenCreditorDoesNotExist() {
+        quotaResponseBuilderSpy.getCreditorForQuotaCredits(quotaCreditsVoMock);
+    }
+
+    @Test
+    public void createQuotaCreditsResponseTestReturnsObject() {
+        QuotaCreditsResponse expected = new QuotaCreditsResponse();
+        expected.setCreditorUserId("test_uuid");
+        expected.setCreditorUsername("test_name");
+        expected.setCredit(new BigDecimal(41.5));
+        expected.setCreditedOn(new Date());
+        expected.setCurrency(QuotaConfig.QuotaCurrencySymbol.value());
+        expected.setObjectName("credit");
+
+        Mockito.when(userVoMock.getUuid()).thenReturn(expected.getCreditorUserId());
+        Mockito.when(userVoMock.getUsername()).thenReturn(expected.getCreditorUsername());
+        Mockito.when(quotaCreditsVoMock.getCredit()).thenReturn(expected.getCredit());
+        Mockito.when(quotaCreditsVoMock.getUpdatedOn()).thenReturn(expected.getCreditedOn());
+
+        QuotaCreditsResponse result = quotaResponseBuilderSpy.createQuotaCreditsResponse(quotaCreditsVoMock, userVoMock);
+
+        Assert.assertEquals(expected.getCreditorUserId(), result.getCreditorUserId());
+        Assert.assertEquals(expected.getCreditorUsername(), result.getCreditorUsername());
+        Assert.assertEquals(expected.getCredit(), result.getCredit());
+        Assert.assertEquals(expected.getCreditedOn(), result.getCreditedOn());
+        Assert.assertEquals(expected.getCurrency(), result.getCurrency());
+        Assert.assertEquals(expected.getObjectName(), result.getObjectName());
+    }
+
     @Test
     public void validateActivationRuleTestValidateActivationRuleReturnValidScriptResponse() {
         Mockito.doReturn("if (account.name == 'test') { true } else { false }").when(quotaValidateActivationRuleCmdMock).getActivationRule();