You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@cloudstack.apache.org by "JoaoJandre (via GitHub)" <gi...@apache.org> on 2023/12/05 13:57:01 UTC

[PR] Quota email configuration [cloudstack]

JoaoJandre opened a new pull request, #8307:
URL: https://github.com/apache/cloudstack/pull/8307

   ### Description
   
   Currently, users have no control on whether they receive Quota emails or not. If the Quota plugin is enabled, they will receive all applicable emails.
   
   Furthermore, there is currently no way to set the (`min_balance`) configuration without adding credits to the account. That value acts as a threshold for the QUOTA_LOW email template. However, there should be a way to easily configure it.
   
   To address the described problems, this PR creates a new API (`quotaConfigureEmail`) which will has the following parameters: (`accountid:String, emailTemplateName:String, enable:Boolean, minBalance:Integer`).    
   
   *    Regarding the API parameters:
        *    (`accountid`) is required, since all configurations will only be in the accounts scope.
        *    (`emailTemplateName`) is not required, unless the (`enable`) parameter is informed.
        *    (`enable`) is not required, unless the (`emailTemplateName`) parameter is informed.
        *    (`minBalance`) is not required. When informed, it will set the (`minBalance`) attribute in the (`quota_account`) table.
   *    Regarding the API functionality:
        *    When the (`accountid, emailTemplateName, enable`) parameters are informed, the email referenced by (`emailTemplateName`) will be enabled/disabled based on (`enable`) for account referenced by (`accountid`). 
        *    When the (`accountid, minBalance`) parameters are informed, the (`min_balance`) of the account referenced by (`accountid`) will be set.
        *    While a email template is not configured for a given account using this API, the default behavior will be maintained, that is, the email template will be considered as enabled.
   
   Furthermore,  a second API (`quotaListEmailConfiguration`) was created, which has a single parameter: (`accountid:String`).    
   
   *    Regarding the API functionality:
        *    The API lists all the email template configurations for the given account.
   
   For accounts that are not configured using the API, the default behavior will be maintained, that is, they will still receive emails.
   
   ### Types of changes
   
   - [ ] Breaking change (fix or feature that would cause existing functionality to change)
   - [X] New feature (non-breaking change which adds functionality)
   - [ ] Bug fix (non-breaking change which fixes an issue)
   - [ ] Enhancement (improves an existing feature and functionality)
   - [ ] Cleanup (Code refactoring and cleanup, that may add test cases)
   - [ ] build/CI
   
   ### Feature/Enhancement Scale or Bug Severity
   
   #### Feature/Enhancement Scale
   
   - [ ] Major
   - [X] Minor
   
   
   ### How Has This Been Tested?
   
   Testing the `quotaConfigureEmail` API
   | No. | Test | Result | Expected result? |
   | ------ | ------ | ------ | ------ |
   | 1 | Call the API passing `accountid` | Exception thrown | yes |
   | 2 | Call the API passing `accountid` and `templatename` | Exception thrown | yes |
   | 3 | Call the API passing `accountid`, `templatename` and `enable` for an account that does not have a `Quota account` | Exception thrown | yes |
   | 4 | Call the API passing `accountid`, `templatename`, `enable` and `minbalance` for an account that does not have a `Quota account` | Exception thrown | yes |
   | 5 | Call the API passing `accountid`, `templatename` and `enable` | Configuration generated and persisted in the database | yes |
   | 6 | Call the API passing `accountid` and `minbalance` | `minbalance` updated | yes |
   | 7 | Call the API passing `accountid`, `templatename`, `enable` and `minbalance` | Configuration generated and persisted in the database, and `minbalance` updated | yes |
   ---
   Testing the `quotaListConfiguration` API
   | No. | Test | Result | Expected result? |
   | ------ | ------ | ------ | ------ |
   | 1 | Call the API passing `accountid` of an account that does not have email settings | Empty Response | yes |
   | 2 | Call the API passing `accountid` of an account that has email settings | Response with email settings | yes |
   ---
   Feature tests:
   - Without configuring the emails, I tested reaching all alert email thresholds, all emails were sent correctly.
   - Enabling all emails, I tested reaching all alert email thresholds, all emails were sent correctly.
   - Disabling all emails, I tested reaching all alert email thresholds, none of the emails were sent.
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@cloudstack.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Quota email configuration [cloudstack]

Posted by "github-actions[bot] (via GitHub)" <gi...@apache.org>.
github-actions[bot] commented on PR #8307:
URL: https://github.com/apache/cloudstack/pull/8307#issuecomment-1985459373

   This pull request has merge conflicts. Dear author, please fix the conflicts and sync your branch with the base branch.


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@cloudstack.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Quota email configuration [cloudstack]

Posted by "github-actions[bot] (via GitHub)" <gi...@apache.org>.
github-actions[bot] commented on PR #8307:
URL: https://github.com/apache/cloudstack/pull/8307#issuecomment-1882870947

   This pull request has merge conflicts. Dear author, please fix the conflicts and sync your branch with the base branch.


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@cloudstack.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Quota email configuration [cloudstack]

Posted by "blueorangutan (via GitHub)" <gi...@apache.org>.
blueorangutan commented on PR #8307:
URL: https://github.com/apache/cloudstack/pull/8307#issuecomment-1954942158

   @JoaoJandre a [SL] Jenkins job has been kicked to build packages. It will be bundled with  KVM, XenServer and VMware SystemVM templates. I'll keep you posted as I make progress.


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@cloudstack.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Quota email configuration [cloudstack]

Posted by "JoaoJandre (via GitHub)" <gi...@apache.org>.
JoaoJandre merged PR #8307:
URL: https://github.com/apache/cloudstack/pull/8307


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@cloudstack.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Quota email configuration [cloudstack]

Posted by "GutoVeronezi (via GitHub)" <gi...@apache.org>.
GutoVeronezi commented on code in PR #8307:
URL: https://github.com/apache/cloudstack/pull/8307#discussion_r1479862147


##########
framework/quota/src/main/java/org/apache/cloudstack/quota/QuotaAlertManagerImpl.java:
##########
@@ -143,52 +148,68 @@ public boolean stop() {
     @Override
     public void checkAndSendQuotaAlertEmails() {
         List<DeferredQuotaEmail> deferredQuotaEmailList = new ArrayList<DeferredQuotaEmail>();
-        final BigDecimal zeroBalance = new BigDecimal(0);
+
+        s_logger.info("Checking and sending quota alert emails.");
         for (final QuotaAccountVO quotaAccount : _quotaAcc.listAllQuotaAccount()) {
-            if (s_logger.isDebugEnabled()) {
-                s_logger.debug("checkAndSendQuotaAlertEmails accId=" + quotaAccount.getId());
-            }
-            BigDecimal accountBalance = quotaAccount.getQuotaBalance();
-            Date balanceDate = quotaAccount.getQuotaBalanceDate();
-            Date alertDate = quotaAccount.getQuotaAlertDate();
-            int lockable = quotaAccount.getQuotaEnforce();
-            BigDecimal thresholdBalance = quotaAccount.getQuotaMinBalance();
-            if (accountBalance != null) {
-                AccountVO account = _accountDao.findById(quotaAccount.getId());
-                if (account == null) {
-                    continue; // the account is removed
-                }
-                if (s_logger.isDebugEnabled()) {
-                    s_logger.debug("checkAndSendQuotaAlertEmails: Check id=" + account.getId() + " bal=" + accountBalance + ", alertDate=" + alertDate + ", lockable=" + lockable);
-                }
-                if (accountBalance.compareTo(zeroBalance) < 0) {
-                    if (_lockAccountEnforcement && (lockable == 1)) {
-                        if (_quotaManager.isLockable(account)) {
-                            s_logger.info("Locking account " + account.getAccountName() + " due to quota < 0.");
-                            lockAccount(account.getId());
-                        }
-                    }
-                    if (alertDate == null || (balanceDate.after(alertDate) && getDifferenceDays(alertDate, new Date()) > 1)) {
-                        s_logger.info("Sending alert " + account.getAccountName() + " due to quota < 0.");
-                        deferredQuotaEmailList.add(new DeferredQuotaEmail(account, quotaAccount, QuotaConfig.QuotaEmailTemplateTypes.QUOTA_EMPTY));
-                    }
-                } else if (accountBalance.compareTo(thresholdBalance) < 0) {
-                    if (alertDate == null || (balanceDate.after(alertDate) && getDifferenceDays(alertDate, new Date()) > 1)) {
-                        s_logger.info("Sending alert " + account.getAccountName() + " due to quota below threshold.");
-                        deferredQuotaEmailList.add(new DeferredQuotaEmail(account, quotaAccount, QuotaConfig.QuotaEmailTemplateTypes.QUOTA_LOW));
-                    }
-                }
-            }
+            checkQuotaAlertEmailForAccount(deferredQuotaEmailList, quotaAccount);
         }
 
         for (DeferredQuotaEmail emailToBeSent : deferredQuotaEmailList) {
-            if (s_logger.isDebugEnabled()) {
-                s_logger.debug("checkAndSendQuotaAlertEmails: Attempting to send quota alert email to users of account: " + emailToBeSent.getAccount().getAccountName());
-            }
+            s_logger.debug(String.format("Attempting to send a quota alert email to users of account [%s].", emailToBeSent.getAccount().getAccountName()));
             sendQuotaAlert(emailToBeSent);
         }
     }
 
+    /**
+     * Checks a given quota account to see if they should receive any emails. First by checking if it has any balance at all, if its account can be found, then checks
+     * if they should receive either QUOTA_EMPTY or QUOTA_LOW emails, taking into account if these email templates are disabled or not for that account.
+     * */
+    protected void checkQuotaAlertEmailForAccount(List<DeferredQuotaEmail> deferredQuotaEmailList, QuotaAccountVO quotaAccount) {

Review Comment:
   This method is quite long, we could break it in small pieces.



##########
framework/quota/src/main/java/org/apache/cloudstack/quota/dao/QuotaEmailConfigurationDaoImpl.java:
##########
@@ -0,0 +1,132 @@
+// 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 com.cloud.utils.db.GenericDaoBase;
+import com.cloud.utils.db.JoinBuilder;
+import com.cloud.utils.db.SearchBuilder;
+import com.cloud.utils.db.SearchCriteria;
+import com.cloud.utils.db.Transaction;
+import com.cloud.utils.db.TransactionCallback;
+import com.cloud.utils.db.TransactionCallbackNoReturn;
+import com.cloud.utils.db.TransactionLegacy;
+import com.cloud.utils.db.TransactionStatus;
+import org.apache.cloudstack.quota.constant.QuotaConfig;
+import org.apache.cloudstack.quota.vo.QuotaEmailConfigurationVO;
+import org.apache.cloudstack.quota.vo.QuotaEmailTemplatesVO;
+import org.springframework.stereotype.Component;
+
+import javax.annotation.PostConstruct;
+import javax.inject.Inject;
+import java.util.List;
+
+@Component
+public class QuotaEmailConfigurationDaoImpl extends GenericDaoBase<QuotaEmailConfigurationVO, Long> implements QuotaEmailConfigurationDao {
+
+    @Inject
+    private QuotaEmailTemplatesDao quotaEmailTemplatesDao;
+
+    private SearchBuilder<QuotaEmailConfigurationVO> searchBuilderFindByIds;
+
+    private SearchBuilder<QuotaEmailTemplatesVO> searchBuilderFindByTemplateName;
+
+    private SearchBuilder<QuotaEmailConfigurationVO> searchBuilderFindByTemplateTypeAndAccountId;
+
+    @PostConstruct
+    public void init() {
+        searchBuilderFindByIds = createSearchBuilder();
+        searchBuilderFindByIds.and("account_id", searchBuilderFindByIds.entity().getAccountId(), SearchCriteria.Op.EQ);
+        searchBuilderFindByIds.and("email_template_id", searchBuilderFindByIds.entity().getEmailTemplateId(), SearchCriteria.Op.EQ);
+        searchBuilderFindByIds.done();
+
+        searchBuilderFindByTemplateName = quotaEmailTemplatesDao.createSearchBuilder();
+        searchBuilderFindByTemplateName.and("template_name", searchBuilderFindByTemplateName.entity().getTemplateName(), SearchCriteria.Op.EQ);
+
+        searchBuilderFindByTemplateTypeAndAccountId = createSearchBuilder();
+        searchBuilderFindByTemplateTypeAndAccountId.and("account_id", searchBuilderFindByTemplateTypeAndAccountId.entity().getAccountId(), SearchCriteria.Op.EQ);
+        searchBuilderFindByTemplateTypeAndAccountId.join("email_template_id", searchBuilderFindByTemplateName, searchBuilderFindByTemplateName.entity().getId(),
+                searchBuilderFindByTemplateTypeAndAccountId.entity().getEmailTemplateId(), JoinBuilder.JoinType.INNER);
+
+        searchBuilderFindByTemplateName.done();
+        searchBuilderFindByTemplateTypeAndAccountId.done();
+    }
+
+    @Override
+    public QuotaEmailConfigurationVO findByAccountIdAndEmailTemplateId(long accountId, long emailTemplateId) {
+        SearchCriteria<QuotaEmailConfigurationVO> sc = searchBuilderFindByIds.create();
+        sc.setParameters("account_id", accountId);
+        sc.setParameters("email_template_id", emailTemplateId);
+        return Transaction.execute(TransactionLegacy.USAGE_DB, new TransactionCallback<QuotaEmailConfigurationVO>() {
+            @Override
+            public QuotaEmailConfigurationVO doInTransaction(TransactionStatus status) {
+                return findOneBy(sc);
+            }
+        });

Review Comment:
   IMO, we could increment readability here with lambda.



##########
framework/quota/src/main/java/org/apache/cloudstack/quota/dao/QuotaEmailConfigurationDaoImpl.java:
##########
@@ -0,0 +1,132 @@
+// 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 com.cloud.utils.db.GenericDaoBase;
+import com.cloud.utils.db.JoinBuilder;
+import com.cloud.utils.db.SearchBuilder;
+import com.cloud.utils.db.SearchCriteria;
+import com.cloud.utils.db.Transaction;
+import com.cloud.utils.db.TransactionCallback;
+import com.cloud.utils.db.TransactionCallbackNoReturn;
+import com.cloud.utils.db.TransactionLegacy;
+import com.cloud.utils.db.TransactionStatus;
+import org.apache.cloudstack.quota.constant.QuotaConfig;
+import org.apache.cloudstack.quota.vo.QuotaEmailConfigurationVO;
+import org.apache.cloudstack.quota.vo.QuotaEmailTemplatesVO;
+import org.springframework.stereotype.Component;
+
+import javax.annotation.PostConstruct;
+import javax.inject.Inject;
+import java.util.List;
+
+@Component
+public class QuotaEmailConfigurationDaoImpl extends GenericDaoBase<QuotaEmailConfigurationVO, Long> implements QuotaEmailConfigurationDao {
+
+    @Inject
+    private QuotaEmailTemplatesDao quotaEmailTemplatesDao;
+
+    private SearchBuilder<QuotaEmailConfigurationVO> searchBuilderFindByIds;
+
+    private SearchBuilder<QuotaEmailTemplatesVO> searchBuilderFindByTemplateName;
+
+    private SearchBuilder<QuotaEmailConfigurationVO> searchBuilderFindByTemplateTypeAndAccountId;
+
+    @PostConstruct
+    public void init() {
+        searchBuilderFindByIds = createSearchBuilder();
+        searchBuilderFindByIds.and("account_id", searchBuilderFindByIds.entity().getAccountId(), SearchCriteria.Op.EQ);
+        searchBuilderFindByIds.and("email_template_id", searchBuilderFindByIds.entity().getEmailTemplateId(), SearchCriteria.Op.EQ);
+        searchBuilderFindByIds.done();
+
+        searchBuilderFindByTemplateName = quotaEmailTemplatesDao.createSearchBuilder();
+        searchBuilderFindByTemplateName.and("template_name", searchBuilderFindByTemplateName.entity().getTemplateName(), SearchCriteria.Op.EQ);
+
+        searchBuilderFindByTemplateTypeAndAccountId = createSearchBuilder();
+        searchBuilderFindByTemplateTypeAndAccountId.and("account_id", searchBuilderFindByTemplateTypeAndAccountId.entity().getAccountId(), SearchCriteria.Op.EQ);
+        searchBuilderFindByTemplateTypeAndAccountId.join("email_template_id", searchBuilderFindByTemplateName, searchBuilderFindByTemplateName.entity().getId(),
+                searchBuilderFindByTemplateTypeAndAccountId.entity().getEmailTemplateId(), JoinBuilder.JoinType.INNER);
+
+        searchBuilderFindByTemplateName.done();
+        searchBuilderFindByTemplateTypeAndAccountId.done();
+    }
+
+    @Override
+    public QuotaEmailConfigurationVO findByAccountIdAndEmailTemplateId(long accountId, long emailTemplateId) {
+        SearchCriteria<QuotaEmailConfigurationVO> sc = searchBuilderFindByIds.create();
+        sc.setParameters("account_id", accountId);
+        sc.setParameters("email_template_id", emailTemplateId);
+        return Transaction.execute(TransactionLegacy.USAGE_DB, new TransactionCallback<QuotaEmailConfigurationVO>() {
+            @Override
+            public QuotaEmailConfigurationVO doInTransaction(TransactionStatus status) {
+                return findOneBy(sc);
+            }
+        });
+    }
+
+    @Override
+    public QuotaEmailConfigurationVO updateQuotaEmailConfiguration(QuotaEmailConfigurationVO quotaEmailConfigurationVO) {
+        SearchCriteria<QuotaEmailConfigurationVO> sc = searchBuilderFindByIds.create();
+        sc.setParameters("account_id", quotaEmailConfigurationVO.getAccountId());
+        sc.setParameters("email_template_id", quotaEmailConfigurationVO.getEmailTemplateId());
+        Transaction.execute(TransactionLegacy.USAGE_DB, new TransactionCallbackNoReturn() {
+            @Override
+            public void doInTransactionWithoutResult(TransactionStatus status) {
+                update(quotaEmailConfigurationVO, sc);
+            }
+        });
+
+        return quotaEmailConfigurationVO;
+    }
+
+    @Override
+    public void persistQuotaEmailConfiguration(QuotaEmailConfigurationVO quotaEmailConfigurationVO) {
+        Transaction.execute(TransactionLegacy.USAGE_DB, new TransactionCallbackNoReturn() {
+            @Override
+            public void doInTransactionWithoutResult(TransactionStatus status) {
+                persist(quotaEmailConfigurationVO);
+            }
+        });
+    }
+
+    @Override
+    public List<QuotaEmailConfigurationVO> listByAccount(long accountId) {
+        SearchCriteria<QuotaEmailConfigurationVO> sc = searchBuilderFindByIds.create();
+        sc.setParameters("account_id", accountId);
+
+        return Transaction.execute(TransactionLegacy.USAGE_DB, new TransactionCallback<List<QuotaEmailConfigurationVO>>() {
+            @Override
+            public List<QuotaEmailConfigurationVO> doInTransaction(TransactionStatus status) {
+                return listBy(sc);
+            }
+        });
+    }
+
+    @Override
+    public QuotaEmailConfigurationVO findByAccountIdAndEmailTemplateType(long accountId, QuotaConfig.QuotaEmailTemplateTypes quotaEmailTemplateType) {
+        SearchCriteria<QuotaEmailConfigurationVO> sc = searchBuilderFindByTemplateTypeAndAccountId.create();
+        sc.setParameters("account_id", accountId);
+        sc.setJoinParameters("email_template_id", "template_name", quotaEmailTemplateType.toString());
+
+        return Transaction.execute(TransactionLegacy.USAGE_DB, new TransactionCallback<QuotaEmailConfigurationVO>() {
+            @Override
+            public QuotaEmailConfigurationVO doInTransaction(TransactionStatus status) {
+                return findOneBy(sc);
+            }
+        });

Review Comment:
   IMO, we could increment readability here with lambda.



##########
framework/quota/src/main/java/org/apache/cloudstack/quota/dao/QuotaEmailConfigurationDaoImpl.java:
##########
@@ -0,0 +1,132 @@
+// 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 com.cloud.utils.db.GenericDaoBase;
+import com.cloud.utils.db.JoinBuilder;
+import com.cloud.utils.db.SearchBuilder;
+import com.cloud.utils.db.SearchCriteria;
+import com.cloud.utils.db.Transaction;
+import com.cloud.utils.db.TransactionCallback;
+import com.cloud.utils.db.TransactionCallbackNoReturn;
+import com.cloud.utils.db.TransactionLegacy;
+import com.cloud.utils.db.TransactionStatus;
+import org.apache.cloudstack.quota.constant.QuotaConfig;
+import org.apache.cloudstack.quota.vo.QuotaEmailConfigurationVO;
+import org.apache.cloudstack.quota.vo.QuotaEmailTemplatesVO;
+import org.springframework.stereotype.Component;
+
+import javax.annotation.PostConstruct;
+import javax.inject.Inject;
+import java.util.List;
+
+@Component
+public class QuotaEmailConfigurationDaoImpl extends GenericDaoBase<QuotaEmailConfigurationVO, Long> implements QuotaEmailConfigurationDao {
+
+    @Inject
+    private QuotaEmailTemplatesDao quotaEmailTemplatesDao;
+
+    private SearchBuilder<QuotaEmailConfigurationVO> searchBuilderFindByIds;
+
+    private SearchBuilder<QuotaEmailTemplatesVO> searchBuilderFindByTemplateName;
+
+    private SearchBuilder<QuotaEmailConfigurationVO> searchBuilderFindByTemplateTypeAndAccountId;
+
+    @PostConstruct
+    public void init() {
+        searchBuilderFindByIds = createSearchBuilder();
+        searchBuilderFindByIds.and("account_id", searchBuilderFindByIds.entity().getAccountId(), SearchCriteria.Op.EQ);
+        searchBuilderFindByIds.and("email_template_id", searchBuilderFindByIds.entity().getEmailTemplateId(), SearchCriteria.Op.EQ);
+        searchBuilderFindByIds.done();
+
+        searchBuilderFindByTemplateName = quotaEmailTemplatesDao.createSearchBuilder();
+        searchBuilderFindByTemplateName.and("template_name", searchBuilderFindByTemplateName.entity().getTemplateName(), SearchCriteria.Op.EQ);
+
+        searchBuilderFindByTemplateTypeAndAccountId = createSearchBuilder();
+        searchBuilderFindByTemplateTypeAndAccountId.and("account_id", searchBuilderFindByTemplateTypeAndAccountId.entity().getAccountId(), SearchCriteria.Op.EQ);
+        searchBuilderFindByTemplateTypeAndAccountId.join("email_template_id", searchBuilderFindByTemplateName, searchBuilderFindByTemplateName.entity().getId(),
+                searchBuilderFindByTemplateTypeAndAccountId.entity().getEmailTemplateId(), JoinBuilder.JoinType.INNER);
+
+        searchBuilderFindByTemplateName.done();
+        searchBuilderFindByTemplateTypeAndAccountId.done();
+    }
+
+    @Override
+    public QuotaEmailConfigurationVO findByAccountIdAndEmailTemplateId(long accountId, long emailTemplateId) {
+        SearchCriteria<QuotaEmailConfigurationVO> sc = searchBuilderFindByIds.create();
+        sc.setParameters("account_id", accountId);
+        sc.setParameters("email_template_id", emailTemplateId);
+        return Transaction.execute(TransactionLegacy.USAGE_DB, new TransactionCallback<QuotaEmailConfigurationVO>() {
+            @Override
+            public QuotaEmailConfigurationVO doInTransaction(TransactionStatus status) {
+                return findOneBy(sc);
+            }
+        });
+    }
+
+    @Override
+    public QuotaEmailConfigurationVO updateQuotaEmailConfiguration(QuotaEmailConfigurationVO quotaEmailConfigurationVO) {
+        SearchCriteria<QuotaEmailConfigurationVO> sc = searchBuilderFindByIds.create();
+        sc.setParameters("account_id", quotaEmailConfigurationVO.getAccountId());
+        sc.setParameters("email_template_id", quotaEmailConfigurationVO.getEmailTemplateId());
+        Transaction.execute(TransactionLegacy.USAGE_DB, new TransactionCallbackNoReturn() {
+            @Override
+            public void doInTransactionWithoutResult(TransactionStatus status) {
+                update(quotaEmailConfigurationVO, sc);
+            }
+        });
+
+        return quotaEmailConfigurationVO;
+    }
+
+    @Override
+    public void persistQuotaEmailConfiguration(QuotaEmailConfigurationVO quotaEmailConfigurationVO) {
+        Transaction.execute(TransactionLegacy.USAGE_DB, new TransactionCallbackNoReturn() {
+            @Override
+            public void doInTransactionWithoutResult(TransactionStatus status) {
+                persist(quotaEmailConfigurationVO);
+            }
+        });
+    }
+
+    @Override
+    public List<QuotaEmailConfigurationVO> listByAccount(long accountId) {
+        SearchCriteria<QuotaEmailConfigurationVO> sc = searchBuilderFindByIds.create();
+        sc.setParameters("account_id", accountId);
+
+        return Transaction.execute(TransactionLegacy.USAGE_DB, new TransactionCallback<List<QuotaEmailConfigurationVO>>() {
+            @Override
+            public List<QuotaEmailConfigurationVO> doInTransaction(TransactionStatus status) {
+                return listBy(sc);
+            }
+        });

Review Comment:
   IMO, we could increment readability here with lambda.



##########
plugins/database/quota/src/main/java/org/apache/cloudstack/api/command/QuotaListEmailConfigurationCmd.java:
##########
@@ -0,0 +1,61 @@
+//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.user.Account;
+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.ListResponse;
+import org.apache.cloudstack.api.response.QuotaConfigureEmailResponse;
+import org.apache.cloudstack.api.response.QuotaResponseBuilder;
+
+import javax.inject.Inject;
+
+@APICommand(name = QuotaListEmailConfigurationCmd.API_NAME, responseObject = QuotaConfigureEmailResponse.class, description = "List quota email template configurations", since = "4.19.0.0",
+        requestHasSensitiveInfo = false, responseHasSensitiveInfo = false)
+public class QuotaListEmailConfigurationCmd extends BaseCmd {
+
+    public static final String API_NAME = "quotaListEmailConfiguration";

Review Comment:
   This is not the pattern anymore; the API name goes directly in the annotation.
   
   



##########
framework/quota/src/main/java/org/apache/cloudstack/quota/dao/QuotaEmailConfigurationDaoImpl.java:
##########
@@ -0,0 +1,132 @@
+// 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 com.cloud.utils.db.GenericDaoBase;
+import com.cloud.utils.db.JoinBuilder;
+import com.cloud.utils.db.SearchBuilder;
+import com.cloud.utils.db.SearchCriteria;
+import com.cloud.utils.db.Transaction;
+import com.cloud.utils.db.TransactionCallback;
+import com.cloud.utils.db.TransactionCallbackNoReturn;
+import com.cloud.utils.db.TransactionLegacy;
+import com.cloud.utils.db.TransactionStatus;
+import org.apache.cloudstack.quota.constant.QuotaConfig;
+import org.apache.cloudstack.quota.vo.QuotaEmailConfigurationVO;
+import org.apache.cloudstack.quota.vo.QuotaEmailTemplatesVO;
+import org.springframework.stereotype.Component;
+
+import javax.annotation.PostConstruct;
+import javax.inject.Inject;
+import java.util.List;
+
+@Component
+public class QuotaEmailConfigurationDaoImpl extends GenericDaoBase<QuotaEmailConfigurationVO, Long> implements QuotaEmailConfigurationDao {
+
+    @Inject
+    private QuotaEmailTemplatesDao quotaEmailTemplatesDao;
+
+    private SearchBuilder<QuotaEmailConfigurationVO> searchBuilderFindByIds;
+
+    private SearchBuilder<QuotaEmailTemplatesVO> searchBuilderFindByTemplateName;
+
+    private SearchBuilder<QuotaEmailConfigurationVO> searchBuilderFindByTemplateTypeAndAccountId;
+
+    @PostConstruct
+    public void init() {
+        searchBuilderFindByIds = createSearchBuilder();
+        searchBuilderFindByIds.and("account_id", searchBuilderFindByIds.entity().getAccountId(), SearchCriteria.Op.EQ);
+        searchBuilderFindByIds.and("email_template_id", searchBuilderFindByIds.entity().getEmailTemplateId(), SearchCriteria.Op.EQ);
+        searchBuilderFindByIds.done();
+
+        searchBuilderFindByTemplateName = quotaEmailTemplatesDao.createSearchBuilder();
+        searchBuilderFindByTemplateName.and("template_name", searchBuilderFindByTemplateName.entity().getTemplateName(), SearchCriteria.Op.EQ);
+
+        searchBuilderFindByTemplateTypeAndAccountId = createSearchBuilder();
+        searchBuilderFindByTemplateTypeAndAccountId.and("account_id", searchBuilderFindByTemplateTypeAndAccountId.entity().getAccountId(), SearchCriteria.Op.EQ);
+        searchBuilderFindByTemplateTypeAndAccountId.join("email_template_id", searchBuilderFindByTemplateName, searchBuilderFindByTemplateName.entity().getId(),
+                searchBuilderFindByTemplateTypeAndAccountId.entity().getEmailTemplateId(), JoinBuilder.JoinType.INNER);
+
+        searchBuilderFindByTemplateName.done();
+        searchBuilderFindByTemplateTypeAndAccountId.done();

Review Comment:
   If the logical operator are the same, we do not need to create several search builders; we could create just one and set the parameters on demand. ACS will create the queries based on the parameters set.



##########
framework/quota/src/main/java/org/apache/cloudstack/quota/dao/QuotaEmailConfigurationDaoImpl.java:
##########
@@ -0,0 +1,132 @@
+// 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 com.cloud.utils.db.GenericDaoBase;
+import com.cloud.utils.db.JoinBuilder;
+import com.cloud.utils.db.SearchBuilder;
+import com.cloud.utils.db.SearchCriteria;
+import com.cloud.utils.db.Transaction;
+import com.cloud.utils.db.TransactionCallback;
+import com.cloud.utils.db.TransactionCallbackNoReturn;
+import com.cloud.utils.db.TransactionLegacy;
+import com.cloud.utils.db.TransactionStatus;
+import org.apache.cloudstack.quota.constant.QuotaConfig;
+import org.apache.cloudstack.quota.vo.QuotaEmailConfigurationVO;
+import org.apache.cloudstack.quota.vo.QuotaEmailTemplatesVO;
+import org.springframework.stereotype.Component;
+
+import javax.annotation.PostConstruct;
+import javax.inject.Inject;
+import java.util.List;
+
+@Component
+public class QuotaEmailConfigurationDaoImpl extends GenericDaoBase<QuotaEmailConfigurationVO, Long> implements QuotaEmailConfigurationDao {
+
+    @Inject
+    private QuotaEmailTemplatesDao quotaEmailTemplatesDao;
+
+    private SearchBuilder<QuotaEmailConfigurationVO> searchBuilderFindByIds;
+
+    private SearchBuilder<QuotaEmailTemplatesVO> searchBuilderFindByTemplateName;
+
+    private SearchBuilder<QuotaEmailConfigurationVO> searchBuilderFindByTemplateTypeAndAccountId;
+
+    @PostConstruct
+    public void init() {
+        searchBuilderFindByIds = createSearchBuilder();
+        searchBuilderFindByIds.and("account_id", searchBuilderFindByIds.entity().getAccountId(), SearchCriteria.Op.EQ);
+        searchBuilderFindByIds.and("email_template_id", searchBuilderFindByIds.entity().getEmailTemplateId(), SearchCriteria.Op.EQ);
+        searchBuilderFindByIds.done();
+
+        searchBuilderFindByTemplateName = quotaEmailTemplatesDao.createSearchBuilder();
+        searchBuilderFindByTemplateName.and("template_name", searchBuilderFindByTemplateName.entity().getTemplateName(), SearchCriteria.Op.EQ);
+
+        searchBuilderFindByTemplateTypeAndAccountId = createSearchBuilder();
+        searchBuilderFindByTemplateTypeAndAccountId.and("account_id", searchBuilderFindByTemplateTypeAndAccountId.entity().getAccountId(), SearchCriteria.Op.EQ);
+        searchBuilderFindByTemplateTypeAndAccountId.join("email_template_id", searchBuilderFindByTemplateName, searchBuilderFindByTemplateName.entity().getId(),
+                searchBuilderFindByTemplateTypeAndAccountId.entity().getEmailTemplateId(), JoinBuilder.JoinType.INNER);
+
+        searchBuilderFindByTemplateName.done();
+        searchBuilderFindByTemplateTypeAndAccountId.done();
+    }
+
+    @Override
+    public QuotaEmailConfigurationVO findByAccountIdAndEmailTemplateId(long accountId, long emailTemplateId) {
+        SearchCriteria<QuotaEmailConfigurationVO> sc = searchBuilderFindByIds.create();
+        sc.setParameters("account_id", accountId);
+        sc.setParameters("email_template_id", emailTemplateId);
+        return Transaction.execute(TransactionLegacy.USAGE_DB, new TransactionCallback<QuotaEmailConfigurationVO>() {
+            @Override
+            public QuotaEmailConfigurationVO doInTransaction(TransactionStatus status) {
+                return findOneBy(sc);
+            }
+        });
+    }
+
+    @Override
+    public QuotaEmailConfigurationVO updateQuotaEmailConfiguration(QuotaEmailConfigurationVO quotaEmailConfigurationVO) {
+        SearchCriteria<QuotaEmailConfigurationVO> sc = searchBuilderFindByIds.create();
+        sc.setParameters("account_id", quotaEmailConfigurationVO.getAccountId());
+        sc.setParameters("email_template_id", quotaEmailConfigurationVO.getEmailTemplateId());
+        Transaction.execute(TransactionLegacy.USAGE_DB, new TransactionCallbackNoReturn() {
+            @Override
+            public void doInTransactionWithoutResult(TransactionStatus status) {
+                update(quotaEmailConfigurationVO, sc);
+            }
+        });

Review Comment:
   IMO, we could increment readability here with lambda.



##########
plugins/database/quota/src/main/java/org/apache/cloudstack/api/command/QuotaConfigureEmailCmd.java:
##########
@@ -0,0 +1,86 @@
+//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.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.QuotaConfigureEmailResponse;
+import org.apache.cloudstack.api.response.QuotaResponseBuilder;
+import org.apache.cloudstack.quota.vo.QuotaEmailConfigurationVO;
+
+import javax.inject.Inject;
+
+@APICommand(name = QuotaConfigureEmailCmd.API_NAME, responseObject = QuotaConfigureEmailResponse.class, description = "Configure a quota email template", since = "4.19.0.0",
+        requestHasSensitiveInfo = false, responseHasSensitiveInfo = false)
+public class QuotaConfigureEmailCmd extends BaseCmd {
+
+    public static final String API_NAME = "quotaConfigureEmail";

Review Comment:
   This is not the pattern anymore; the API name goes directly in the annotation.



##########
framework/quota/src/main/java/org/apache/cloudstack/quota/dao/QuotaEmailTemplatesDaoImpl.java:
##########
@@ -68,4 +68,14 @@ public Boolean doInTransaction(final TransactionStatus status) {
             }
         });
     }
+
+    @Override
+    public QuotaEmailTemplatesVO findById(long id) {
+        return Transaction.execute(TransactionLegacy.USAGE_DB, new TransactionCallback<QuotaEmailTemplatesVO>() {
+            @Override
+            public QuotaEmailTemplatesVO doInTransaction(final TransactionStatus status) {
+                return QuotaEmailTemplatesDaoImpl.super.findById(id);
+            }
+        });

Review Comment:
   IMO, we could increment readability here with lambda.



##########
plugins/database/quota/src/main/java/org/apache/cloudstack/api/command/QuotaConfigureEmailCmd.java:
##########
@@ -0,0 +1,86 @@
+//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.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.QuotaConfigureEmailResponse;
+import org.apache.cloudstack.api.response.QuotaResponseBuilder;
+import org.apache.cloudstack.quota.vo.QuotaEmailConfigurationVO;
+
+import javax.inject.Inject;
+
+@APICommand(name = QuotaConfigureEmailCmd.API_NAME, responseObject = QuotaConfigureEmailResponse.class, description = "Configure a quota email template", since = "4.19.0.0",

Review Comment:
   Change `since`.



##########
plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaResponseBuilderImpl.java:
##########
@@ -653,4 +658,93 @@ public boolean deleteQuotaTariff(String quotaTariffUuid) {
         quotaTariff.setRemoved(_quotaService.computeAdjustedTime(new Date()));
         return _quotaTariffDao.updateQuotaTariff(quotaTariff);
     }
+
+    @Override
+    public Pair<QuotaEmailConfigurationVO, Double> configureQuotaEmail(QuotaConfigureEmailCmd cmd) {
+        validateQuotaConfigureEmailCmdParameters(cmd);
+
+        Double minBalance = cmd.getMinBalance();
+
+        if (minBalance != null) {
+            _quotaService.setMinBalance(cmd.getAccountId(), cmd.getMinBalance());
+        }
+
+        if (cmd.getTemplateName() != null) {
+            List<QuotaEmailTemplatesVO> templateVO = _quotaEmailTemplateDao.listAllQuotaEmailTemplates(cmd.getTemplateName());
+            if (templateVO.isEmpty()) {
+                throw new InvalidParameterValueException(String.format("Could not find template with name [%s].", cmd.getTemplateName()));
+            }
+            long templateId = templateVO.get(0).getId();
+            QuotaEmailConfigurationVO configurationVO = quotaEmailConfigurationDao.findByAccountIdAndEmailTemplateId(cmd.getAccountId(), templateId);
+
+            if (configurationVO == null) {
+                configurationVO = new QuotaEmailConfigurationVO(cmd.getAccountId(), templateId, cmd.getEnable());
+                quotaEmailConfigurationDao.persistQuotaEmailConfiguration(configurationVO);
+                return new Pair<>(configurationVO, minBalance);
+            }
+
+            configurationVO.setEnabled(cmd.getEnable());
+            return new Pair<>(quotaEmailConfigurationDao.updateQuotaEmailConfiguration(configurationVO), minBalance);
+        }

Review Comment:
   This block could be extracted to a method.



##########
plugins/database/quota/src/main/java/org/apache/cloudstack/api/command/QuotaConfigureEmailCmd.java:
##########
@@ -0,0 +1,86 @@
+//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.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.QuotaConfigureEmailResponse;
+import org.apache.cloudstack.api.response.QuotaResponseBuilder;
+import org.apache.cloudstack.quota.vo.QuotaEmailConfigurationVO;
+
+import javax.inject.Inject;
+
+@APICommand(name = QuotaConfigureEmailCmd.API_NAME, responseObject = QuotaConfigureEmailResponse.class, description = "Configure a quota email template", since = "4.19.0.0",
+        requestHasSensitiveInfo = false, responseHasSensitiveInfo = false)
+public class QuotaConfigureEmailCmd extends BaseCmd {
+
+    public static final String API_NAME = "quotaConfigureEmail";
+
+    @Parameter(name = ApiConstants.ACCOUNT_ID, type = CommandType.UUID, entityType = AccountResponse.class, required = true,
+            description = "Account ID for which to configure quota template email or min balance")
+    private long accountId;
+
+    @Parameter(name = ApiConstants.TEMPLATE_NAME, type = CommandType.STRING, description = "Quota email template name which should be configured")
+    private String templateName;
+
+    @Parameter(name = ApiConstants.ENABLE, type = CommandType.BOOLEAN, description = "If the quota email template should be enabled")
+    private Boolean enable;
+
+    @Parameter(name = "minbalance", type = CommandType.DOUBLE, description = "New quota account min balance")
+    private Double minBalance;
+
+    @Inject
+    private QuotaResponseBuilder responseBuilder;
+
+    @Override
+    public void execute() {
+        Pair<QuotaEmailConfigurationVO, Double> result = responseBuilder.configureQuotaEmail(this);
+        QuotaConfigureEmailResponse quotaConfigureEmailResponse = responseBuilder.createQuotaConfigureEmailResponse(result.first(), result.second(), accountId);
+        quotaConfigureEmailResponse.setResponseName(getCommandName());
+        this.setResponseObject(quotaConfigureEmailResponse);
+    }
+
+    @Override
+    public String getCommandName() {
+        return API_NAME.toLowerCase() + BaseCmd.RESPONSE_SUFFIX;
+    }

Review Comment:
   Unnecessary.



##########
plugins/database/quota/src/main/java/org/apache/cloudstack/api/command/QuotaListEmailConfigurationCmd.java:
##########
@@ -0,0 +1,61 @@
+//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.user.Account;
+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.ListResponse;
+import org.apache.cloudstack.api.response.QuotaConfigureEmailResponse;
+import org.apache.cloudstack.api.response.QuotaResponseBuilder;
+
+import javax.inject.Inject;
+
+@APICommand(name = QuotaListEmailConfigurationCmd.API_NAME, responseObject = QuotaConfigureEmailResponse.class, description = "List quota email template configurations", since = "4.19.0.0",
+        requestHasSensitiveInfo = false, responseHasSensitiveInfo = false)
+public class QuotaListEmailConfigurationCmd extends BaseCmd {
+
+    public static final String API_NAME = "quotaListEmailConfiguration";
+
+    @Parameter(name = ApiConstants.ACCOUNT_ID, type = BaseCmd.CommandType.UUID, entityType = AccountResponse.class, required = true,
+            description = "Account ID for which to list quota template email configurations")
+    private long accountId;
+
+    @Inject
+    private QuotaResponseBuilder responseBuilder;
+
+    @Override
+    public void execute() {
+        ListResponse<QuotaConfigureEmailResponse> response = new ListResponse<>();
+        response.setResponses(responseBuilder.listEmailConfiguration(accountId));
+        response.setResponseName(getCommandName());
+        setResponseObject(response);
+    }
+
+    @Override
+    public String getCommandName() {
+        return API_NAME.toLowerCase() + BaseCmd.RESPONSE_SUFFIX;
+    }

Review Comment:
   Unnecessary.



##########
framework/quota/src/main/java/org/apache/cloudstack/quota/dao/QuotaEmailConfigurationDaoImpl.java:
##########
@@ -0,0 +1,132 @@
+// 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 com.cloud.utils.db.GenericDaoBase;
+import com.cloud.utils.db.JoinBuilder;
+import com.cloud.utils.db.SearchBuilder;
+import com.cloud.utils.db.SearchCriteria;
+import com.cloud.utils.db.Transaction;
+import com.cloud.utils.db.TransactionCallback;
+import com.cloud.utils.db.TransactionCallbackNoReturn;
+import com.cloud.utils.db.TransactionLegacy;
+import com.cloud.utils.db.TransactionStatus;
+import org.apache.cloudstack.quota.constant.QuotaConfig;
+import org.apache.cloudstack.quota.vo.QuotaEmailConfigurationVO;
+import org.apache.cloudstack.quota.vo.QuotaEmailTemplatesVO;
+import org.springframework.stereotype.Component;
+
+import javax.annotation.PostConstruct;
+import javax.inject.Inject;
+import java.util.List;
+
+@Component
+public class QuotaEmailConfigurationDaoImpl extends GenericDaoBase<QuotaEmailConfigurationVO, Long> implements QuotaEmailConfigurationDao {
+
+    @Inject
+    private QuotaEmailTemplatesDao quotaEmailTemplatesDao;
+
+    private SearchBuilder<QuotaEmailConfigurationVO> searchBuilderFindByIds;
+
+    private SearchBuilder<QuotaEmailTemplatesVO> searchBuilderFindByTemplateName;
+
+    private SearchBuilder<QuotaEmailConfigurationVO> searchBuilderFindByTemplateTypeAndAccountId;
+
+    @PostConstruct
+    public void init() {
+        searchBuilderFindByIds = createSearchBuilder();
+        searchBuilderFindByIds.and("account_id", searchBuilderFindByIds.entity().getAccountId(), SearchCriteria.Op.EQ);
+        searchBuilderFindByIds.and("email_template_id", searchBuilderFindByIds.entity().getEmailTemplateId(), SearchCriteria.Op.EQ);
+        searchBuilderFindByIds.done();
+
+        searchBuilderFindByTemplateName = quotaEmailTemplatesDao.createSearchBuilder();
+        searchBuilderFindByTemplateName.and("template_name", searchBuilderFindByTemplateName.entity().getTemplateName(), SearchCriteria.Op.EQ);
+
+        searchBuilderFindByTemplateTypeAndAccountId = createSearchBuilder();
+        searchBuilderFindByTemplateTypeAndAccountId.and("account_id", searchBuilderFindByTemplateTypeAndAccountId.entity().getAccountId(), SearchCriteria.Op.EQ);
+        searchBuilderFindByTemplateTypeAndAccountId.join("email_template_id", searchBuilderFindByTemplateName, searchBuilderFindByTemplateName.entity().getId(),
+                searchBuilderFindByTemplateTypeAndAccountId.entity().getEmailTemplateId(), JoinBuilder.JoinType.INNER);
+
+        searchBuilderFindByTemplateName.done();
+        searchBuilderFindByTemplateTypeAndAccountId.done();
+    }
+
+    @Override
+    public QuotaEmailConfigurationVO findByAccountIdAndEmailTemplateId(long accountId, long emailTemplateId) {
+        SearchCriteria<QuotaEmailConfigurationVO> sc = searchBuilderFindByIds.create();
+        sc.setParameters("account_id", accountId);
+        sc.setParameters("email_template_id", emailTemplateId);
+        return Transaction.execute(TransactionLegacy.USAGE_DB, new TransactionCallback<QuotaEmailConfigurationVO>() {
+            @Override
+            public QuotaEmailConfigurationVO doInTransaction(TransactionStatus status) {
+                return findOneBy(sc);
+            }
+        });
+    }
+
+    @Override
+    public QuotaEmailConfigurationVO updateQuotaEmailConfiguration(QuotaEmailConfigurationVO quotaEmailConfigurationVO) {
+        SearchCriteria<QuotaEmailConfigurationVO> sc = searchBuilderFindByIds.create();
+        sc.setParameters("account_id", quotaEmailConfigurationVO.getAccountId());
+        sc.setParameters("email_template_id", quotaEmailConfigurationVO.getEmailTemplateId());
+        Transaction.execute(TransactionLegacy.USAGE_DB, new TransactionCallbackNoReturn() {
+            @Override
+            public void doInTransactionWithoutResult(TransactionStatus status) {
+                update(quotaEmailConfigurationVO, sc);
+            }
+        });
+
+        return quotaEmailConfigurationVO;
+    }
+
+    @Override
+    public void persistQuotaEmailConfiguration(QuotaEmailConfigurationVO quotaEmailConfigurationVO) {
+        Transaction.execute(TransactionLegacy.USAGE_DB, new TransactionCallbackNoReturn() {
+            @Override
+            public void doInTransactionWithoutResult(TransactionStatus status) {
+                persist(quotaEmailConfigurationVO);
+            }
+        });

Review Comment:
   IMO, we could increment readability here with lambda.



##########
plugins/database/quota/src/main/java/org/apache/cloudstack/api/command/QuotaListEmailConfigurationCmd.java:
##########
@@ -0,0 +1,61 @@
+//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.user.Account;
+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.ListResponse;
+import org.apache.cloudstack.api.response.QuotaConfigureEmailResponse;
+import org.apache.cloudstack.api.response.QuotaResponseBuilder;
+
+import javax.inject.Inject;
+
+@APICommand(name = QuotaListEmailConfigurationCmd.API_NAME, responseObject = QuotaConfigureEmailResponse.class, description = "List quota email template configurations", since = "4.19.0.0",

Review Comment:
   Change since.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@cloudstack.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Quota email configuration [cloudstack]

Posted by "JoaoJandre (via GitHub)" <gi...@apache.org>.
JoaoJandre commented on PR #8307:
URL: https://github.com/apache/cloudstack/pull/8307#issuecomment-1997851926

   @DaanHoogland @rohityadavcloud @shwstppr could we run the CI on this one?


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@cloudstack.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Quota email configuration [cloudstack]

Posted by "GutoVeronezi (via GitHub)" <gi...@apache.org>.
GutoVeronezi commented on code in PR #8307:
URL: https://github.com/apache/cloudstack/pull/8307#discussion_r1491715284


##########
framework/quota/src/main/java/org/apache/cloudstack/quota/QuotaAlertManagerImpl.java:
##########
@@ -141,52 +146,72 @@ public boolean stop() {
     @Override
     public void checkAndSendQuotaAlertEmails() {
         List<DeferredQuotaEmail> deferredQuotaEmailList = new ArrayList<DeferredQuotaEmail>();
-        final BigDecimal zeroBalance = new BigDecimal(0);
+
+        logger.info("Checking and sending quota alert emails.");
         for (final QuotaAccountVO quotaAccount : _quotaAcc.listAllQuotaAccount()) {
-            if (logger.isDebugEnabled()) {
-                logger.debug("checkAndSendQuotaAlertEmails accId=" + quotaAccount.getId());
-            }
-            BigDecimal accountBalance = quotaAccount.getQuotaBalance();
-            Date balanceDate = quotaAccount.getQuotaBalanceDate();
-            Date alertDate = quotaAccount.getQuotaAlertDate();
-            int lockable = quotaAccount.getQuotaEnforce();
-            BigDecimal thresholdBalance = quotaAccount.getQuotaMinBalance();
-            if (accountBalance != null) {
-                AccountVO account = _accountDao.findById(quotaAccount.getId());
-                if (account == null) {
-                    continue; // the account is removed
-                }
-                if (logger.isDebugEnabled()) {
-                    logger.debug("checkAndSendQuotaAlertEmails: Check id=" + account.getId() + " bal=" + accountBalance + ", alertDate=" + alertDate + ", lockable=" + lockable);
-                }
-                if (accountBalance.compareTo(zeroBalance) < 0) {
-                    if (_lockAccountEnforcement && (lockable == 1)) {
-                        if (_quotaManager.isLockable(account)) {
-                            logger.info("Locking account " + account.getAccountName() + " due to quota < 0.");
-                            lockAccount(account.getId());
-                        }
-                    }
-                    if (alertDate == null || (balanceDate.after(alertDate) && getDifferenceDays(alertDate, new Date()) > 1)) {
-                        logger.info("Sending alert " + account.getAccountName() + " due to quota < 0.");
-                        deferredQuotaEmailList.add(new DeferredQuotaEmail(account, quotaAccount, QuotaConfig.QuotaEmailTemplateTypes.QUOTA_EMPTY));
-                    }
-                } else if (accountBalance.compareTo(thresholdBalance) < 0) {
-                    if (alertDate == null || (balanceDate.after(alertDate) && getDifferenceDays(alertDate, new Date()) > 1)) {
-                        logger.info("Sending alert " + account.getAccountName() + " due to quota below threshold.");
-                        deferredQuotaEmailList.add(new DeferredQuotaEmail(account, quotaAccount, QuotaConfig.QuotaEmailTemplateTypes.QUOTA_LOW));
-                    }
-                }
-            }
+            checkQuotaAlertEmailForAccount(deferredQuotaEmailList, quotaAccount);
         }
 
         for (DeferredQuotaEmail emailToBeSent : deferredQuotaEmailList) {
-            if (logger.isDebugEnabled()) {
-                logger.debug("checkAndSendQuotaAlertEmails: Attempting to send quota alert email to users of account: " + emailToBeSent.getAccount().getAccountName());
-            }
+            logger.debug(String.format("Attempting to send a quota alert email to users of account [%s].", emailToBeSent.getAccount().getAccountName()));
             sendQuotaAlert(emailToBeSent);
         }
     }
 
+    /**
+     * Checks a given quota account to see if they should receive any emails. First by checking if it has any balance at all, if its account can be found, then checks
+     * if they should receive either QUOTA_EMPTY or QUOTA_LOW emails, taking into account if these email templates are disabled or not for that account.
+     * */
+    protected void checkQuotaAlertEmailForAccount(List<DeferredQuotaEmail> deferredQuotaEmailList, QuotaAccountVO quotaAccount) {
+        logger.debug(String.format("Checking %s for email alerts.", quotaAccount));
+        BigDecimal accountBalance = quotaAccount.getQuotaBalance();
+
+        if (accountBalance == null) {
+            logger.debug(String.format("%s has a null balance, therefore it will not receive quota alert emails.", quotaAccount));

Review Comment:
   Use new log format.



##########
framework/quota/src/main/java/org/apache/cloudstack/quota/QuotaAlertManagerImpl.java:
##########
@@ -141,52 +146,72 @@ public boolean stop() {
     @Override
     public void checkAndSendQuotaAlertEmails() {
         List<DeferredQuotaEmail> deferredQuotaEmailList = new ArrayList<DeferredQuotaEmail>();
-        final BigDecimal zeroBalance = new BigDecimal(0);
+
+        logger.info("Checking and sending quota alert emails.");
         for (final QuotaAccountVO quotaAccount : _quotaAcc.listAllQuotaAccount()) {
-            if (logger.isDebugEnabled()) {
-                logger.debug("checkAndSendQuotaAlertEmails accId=" + quotaAccount.getId());
-            }
-            BigDecimal accountBalance = quotaAccount.getQuotaBalance();
-            Date balanceDate = quotaAccount.getQuotaBalanceDate();
-            Date alertDate = quotaAccount.getQuotaAlertDate();
-            int lockable = quotaAccount.getQuotaEnforce();
-            BigDecimal thresholdBalance = quotaAccount.getQuotaMinBalance();
-            if (accountBalance != null) {
-                AccountVO account = _accountDao.findById(quotaAccount.getId());
-                if (account == null) {
-                    continue; // the account is removed
-                }
-                if (logger.isDebugEnabled()) {
-                    logger.debug("checkAndSendQuotaAlertEmails: Check id=" + account.getId() + " bal=" + accountBalance + ", alertDate=" + alertDate + ", lockable=" + lockable);
-                }
-                if (accountBalance.compareTo(zeroBalance) < 0) {
-                    if (_lockAccountEnforcement && (lockable == 1)) {
-                        if (_quotaManager.isLockable(account)) {
-                            logger.info("Locking account " + account.getAccountName() + " due to quota < 0.");
-                            lockAccount(account.getId());
-                        }
-                    }
-                    if (alertDate == null || (balanceDate.after(alertDate) && getDifferenceDays(alertDate, new Date()) > 1)) {
-                        logger.info("Sending alert " + account.getAccountName() + " due to quota < 0.");
-                        deferredQuotaEmailList.add(new DeferredQuotaEmail(account, quotaAccount, QuotaConfig.QuotaEmailTemplateTypes.QUOTA_EMPTY));
-                    }
-                } else if (accountBalance.compareTo(thresholdBalance) < 0) {
-                    if (alertDate == null || (balanceDate.after(alertDate) && getDifferenceDays(alertDate, new Date()) > 1)) {
-                        logger.info("Sending alert " + account.getAccountName() + " due to quota below threshold.");
-                        deferredQuotaEmailList.add(new DeferredQuotaEmail(account, quotaAccount, QuotaConfig.QuotaEmailTemplateTypes.QUOTA_LOW));
-                    }
-                }
-            }
+            checkQuotaAlertEmailForAccount(deferredQuotaEmailList, quotaAccount);
         }
 
         for (DeferredQuotaEmail emailToBeSent : deferredQuotaEmailList) {
-            if (logger.isDebugEnabled()) {
-                logger.debug("checkAndSendQuotaAlertEmails: Attempting to send quota alert email to users of account: " + emailToBeSent.getAccount().getAccountName());
-            }
+            logger.debug(String.format("Attempting to send a quota alert email to users of account [%s].", emailToBeSent.getAccount().getAccountName()));
             sendQuotaAlert(emailToBeSent);
         }
     }
 
+    /**
+     * Checks a given quota account to see if they should receive any emails. First by checking if it has any balance at all, if its account can be found, then checks
+     * if they should receive either QUOTA_EMPTY or QUOTA_LOW emails, taking into account if these email templates are disabled or not for that account.
+     * */
+    protected void checkQuotaAlertEmailForAccount(List<DeferredQuotaEmail> deferredQuotaEmailList, QuotaAccountVO quotaAccount) {
+        logger.debug(String.format("Checking %s for email alerts.", quotaAccount));
+        BigDecimal accountBalance = quotaAccount.getQuotaBalance();
+
+        if (accountBalance == null) {
+            logger.debug(String.format("%s has a null balance, therefore it will not receive quota alert emails.", quotaAccount));
+            return;
+        }
+
+        AccountVO account = _accountDao.findById(quotaAccount.getId());
+        if (account == null) {
+            logger.debug(String.format("Account of %s is removed, thus it will not receive quota alert emails.", quotaAccount));
+            return;
+        }
+
+        checkBalanceAndAddToEmailList(deferredQuotaEmailList, quotaAccount, account, accountBalance);
+    }
+
+    private void checkBalanceAndAddToEmailList(List<DeferredQuotaEmail> deferredQuotaEmailList, QuotaAccountVO quotaAccount, AccountVO account, BigDecimal accountBalance) {
+        Date balanceDate = quotaAccount.getQuotaBalanceDate();
+        Date alertDate = quotaAccount.getQuotaAlertDate();
+        int lockable = quotaAccount.getQuotaEnforce();
+        BigDecimal thresholdBalance = quotaAccount.getQuotaMinBalance();
+
+        logger.debug(String.format("Checking %s with accountBalance [%s], alertDate [%s] and lockable [%s] to see if a quota alert email should be sent.", account,
+                accountBalance, alertDate, lockable));
+
+        QuotaEmailConfigurationVO quotaEmpty = quotaEmailConfigurationDao.findByAccountIdAndEmailTemplateType(account.getAccountId(), QuotaEmailTemplateTypes.QUOTA_EMPTY);
+        QuotaEmailConfigurationVO quotaLow = quotaEmailConfigurationDao.findByAccountIdAndEmailTemplateType(account.getAccountId(), QuotaEmailTemplateTypes.QUOTA_LOW);
+
+        boolean shouldSendEmail = alertDate == null || (balanceDate.after(alertDate) && getDifferenceDays(alertDate, new Date()) > 1);
+
+        if (accountBalance.compareTo(BigDecimal.ZERO) < 0) {
+            if (_lockAccountEnforcement && lockable == 1 && _quotaManager.isLockable(account)) {
+                logger.info(String.format("Locking %s, as quota balance is lower than 0.", account));
+                lockAccount(account.getId());
+            }
+            if (quotaEmpty != null && quotaEmpty.isEnabled() && shouldSendEmail) {
+                logger.debug(String.format("Adding %s to the deferred emails list, as quota balance is lower than 0.", account));
+                deferredQuotaEmailList.add(new DeferredQuotaEmail(account, quotaAccount, QuotaEmailTemplateTypes.QUOTA_EMPTY));
+                return;
+            }
+        } else if (accountBalance.compareTo(thresholdBalance) < 0 && quotaLow != null && quotaLow.isEnabled() && shouldSendEmail) {
+            logger.debug(String.format("Adding %s to the deferred emails list, as quota balance [%s] is below the threshold [%s].", account, accountBalance, thresholdBalance));
+            deferredQuotaEmailList.add(new DeferredQuotaEmail(account, quotaAccount, QuotaEmailTemplateTypes.QUOTA_LOW));
+            return;
+        }
+        logger.debug(String.format("%s will not receive any quota alert emails in this round.", account));

Review Comment:
   Use new log format.



##########
framework/quota/src/main/java/org/apache/cloudstack/quota/QuotaAlertManagerImpl.java:
##########
@@ -141,52 +146,72 @@ public boolean stop() {
     @Override
     public void checkAndSendQuotaAlertEmails() {
         List<DeferredQuotaEmail> deferredQuotaEmailList = new ArrayList<DeferredQuotaEmail>();
-        final BigDecimal zeroBalance = new BigDecimal(0);
+
+        logger.info("Checking and sending quota alert emails.");
         for (final QuotaAccountVO quotaAccount : _quotaAcc.listAllQuotaAccount()) {
-            if (logger.isDebugEnabled()) {
-                logger.debug("checkAndSendQuotaAlertEmails accId=" + quotaAccount.getId());
-            }
-            BigDecimal accountBalance = quotaAccount.getQuotaBalance();
-            Date balanceDate = quotaAccount.getQuotaBalanceDate();
-            Date alertDate = quotaAccount.getQuotaAlertDate();
-            int lockable = quotaAccount.getQuotaEnforce();
-            BigDecimal thresholdBalance = quotaAccount.getQuotaMinBalance();
-            if (accountBalance != null) {
-                AccountVO account = _accountDao.findById(quotaAccount.getId());
-                if (account == null) {
-                    continue; // the account is removed
-                }
-                if (logger.isDebugEnabled()) {
-                    logger.debug("checkAndSendQuotaAlertEmails: Check id=" + account.getId() + " bal=" + accountBalance + ", alertDate=" + alertDate + ", lockable=" + lockable);
-                }
-                if (accountBalance.compareTo(zeroBalance) < 0) {
-                    if (_lockAccountEnforcement && (lockable == 1)) {
-                        if (_quotaManager.isLockable(account)) {
-                            logger.info("Locking account " + account.getAccountName() + " due to quota < 0.");
-                            lockAccount(account.getId());
-                        }
-                    }
-                    if (alertDate == null || (balanceDate.after(alertDate) && getDifferenceDays(alertDate, new Date()) > 1)) {
-                        logger.info("Sending alert " + account.getAccountName() + " due to quota < 0.");
-                        deferredQuotaEmailList.add(new DeferredQuotaEmail(account, quotaAccount, QuotaConfig.QuotaEmailTemplateTypes.QUOTA_EMPTY));
-                    }
-                } else if (accountBalance.compareTo(thresholdBalance) < 0) {
-                    if (alertDate == null || (balanceDate.after(alertDate) && getDifferenceDays(alertDate, new Date()) > 1)) {
-                        logger.info("Sending alert " + account.getAccountName() + " due to quota below threshold.");
-                        deferredQuotaEmailList.add(new DeferredQuotaEmail(account, quotaAccount, QuotaConfig.QuotaEmailTemplateTypes.QUOTA_LOW));
-                    }
-                }
-            }
+            checkQuotaAlertEmailForAccount(deferredQuotaEmailList, quotaAccount);
         }
 
         for (DeferredQuotaEmail emailToBeSent : deferredQuotaEmailList) {
-            if (logger.isDebugEnabled()) {
-                logger.debug("checkAndSendQuotaAlertEmails: Attempting to send quota alert email to users of account: " + emailToBeSent.getAccount().getAccountName());
-            }
+            logger.debug(String.format("Attempting to send a quota alert email to users of account [%s].", emailToBeSent.getAccount().getAccountName()));

Review Comment:
   Use new log format.



##########
framework/quota/src/main/java/org/apache/cloudstack/quota/QuotaAlertManagerImpl.java:
##########
@@ -141,52 +146,72 @@ public boolean stop() {
     @Override
     public void checkAndSendQuotaAlertEmails() {
         List<DeferredQuotaEmail> deferredQuotaEmailList = new ArrayList<DeferredQuotaEmail>();
-        final BigDecimal zeroBalance = new BigDecimal(0);
+
+        logger.info("Checking and sending quota alert emails.");
         for (final QuotaAccountVO quotaAccount : _quotaAcc.listAllQuotaAccount()) {
-            if (logger.isDebugEnabled()) {
-                logger.debug("checkAndSendQuotaAlertEmails accId=" + quotaAccount.getId());
-            }
-            BigDecimal accountBalance = quotaAccount.getQuotaBalance();
-            Date balanceDate = quotaAccount.getQuotaBalanceDate();
-            Date alertDate = quotaAccount.getQuotaAlertDate();
-            int lockable = quotaAccount.getQuotaEnforce();
-            BigDecimal thresholdBalance = quotaAccount.getQuotaMinBalance();
-            if (accountBalance != null) {
-                AccountVO account = _accountDao.findById(quotaAccount.getId());
-                if (account == null) {
-                    continue; // the account is removed
-                }
-                if (logger.isDebugEnabled()) {
-                    logger.debug("checkAndSendQuotaAlertEmails: Check id=" + account.getId() + " bal=" + accountBalance + ", alertDate=" + alertDate + ", lockable=" + lockable);
-                }
-                if (accountBalance.compareTo(zeroBalance) < 0) {
-                    if (_lockAccountEnforcement && (lockable == 1)) {
-                        if (_quotaManager.isLockable(account)) {
-                            logger.info("Locking account " + account.getAccountName() + " due to quota < 0.");
-                            lockAccount(account.getId());
-                        }
-                    }
-                    if (alertDate == null || (balanceDate.after(alertDate) && getDifferenceDays(alertDate, new Date()) > 1)) {
-                        logger.info("Sending alert " + account.getAccountName() + " due to quota < 0.");
-                        deferredQuotaEmailList.add(new DeferredQuotaEmail(account, quotaAccount, QuotaConfig.QuotaEmailTemplateTypes.QUOTA_EMPTY));
-                    }
-                } else if (accountBalance.compareTo(thresholdBalance) < 0) {
-                    if (alertDate == null || (balanceDate.after(alertDate) && getDifferenceDays(alertDate, new Date()) > 1)) {
-                        logger.info("Sending alert " + account.getAccountName() + " due to quota below threshold.");
-                        deferredQuotaEmailList.add(new DeferredQuotaEmail(account, quotaAccount, QuotaConfig.QuotaEmailTemplateTypes.QUOTA_LOW));
-                    }
-                }
-            }
+            checkQuotaAlertEmailForAccount(deferredQuotaEmailList, quotaAccount);
         }
 
         for (DeferredQuotaEmail emailToBeSent : deferredQuotaEmailList) {
-            if (logger.isDebugEnabled()) {
-                logger.debug("checkAndSendQuotaAlertEmails: Attempting to send quota alert email to users of account: " + emailToBeSent.getAccount().getAccountName());
-            }
+            logger.debug(String.format("Attempting to send a quota alert email to users of account [%s].", emailToBeSent.getAccount().getAccountName()));
             sendQuotaAlert(emailToBeSent);
         }
     }
 
+    /**
+     * Checks a given quota account to see if they should receive any emails. First by checking if it has any balance at all, if its account can be found, then checks
+     * if they should receive either QUOTA_EMPTY or QUOTA_LOW emails, taking into account if these email templates are disabled or not for that account.
+     * */
+    protected void checkQuotaAlertEmailForAccount(List<DeferredQuotaEmail> deferredQuotaEmailList, QuotaAccountVO quotaAccount) {
+        logger.debug(String.format("Checking %s for email alerts.", quotaAccount));
+        BigDecimal accountBalance = quotaAccount.getQuotaBalance();
+
+        if (accountBalance == null) {
+            logger.debug(String.format("%s has a null balance, therefore it will not receive quota alert emails.", quotaAccount));
+            return;
+        }
+
+        AccountVO account = _accountDao.findById(quotaAccount.getId());
+        if (account == null) {
+            logger.debug(String.format("Account of %s is removed, thus it will not receive quota alert emails.", quotaAccount));
+            return;
+        }
+
+        checkBalanceAndAddToEmailList(deferredQuotaEmailList, quotaAccount, account, accountBalance);
+    }
+
+    private void checkBalanceAndAddToEmailList(List<DeferredQuotaEmail> deferredQuotaEmailList, QuotaAccountVO quotaAccount, AccountVO account, BigDecimal accountBalance) {
+        Date balanceDate = quotaAccount.getQuotaBalanceDate();
+        Date alertDate = quotaAccount.getQuotaAlertDate();
+        int lockable = quotaAccount.getQuotaEnforce();
+        BigDecimal thresholdBalance = quotaAccount.getQuotaMinBalance();
+
+        logger.debug(String.format("Checking %s with accountBalance [%s], alertDate [%s] and lockable [%s] to see if a quota alert email should be sent.", account,
+                accountBalance, alertDate, lockable));
+
+        QuotaEmailConfigurationVO quotaEmpty = quotaEmailConfigurationDao.findByAccountIdAndEmailTemplateType(account.getAccountId(), QuotaEmailTemplateTypes.QUOTA_EMPTY);
+        QuotaEmailConfigurationVO quotaLow = quotaEmailConfigurationDao.findByAccountIdAndEmailTemplateType(account.getAccountId(), QuotaEmailTemplateTypes.QUOTA_LOW);
+
+        boolean shouldSendEmail = alertDate == null || (balanceDate.after(alertDate) && getDifferenceDays(alertDate, new Date()) > 1);
+
+        if (accountBalance.compareTo(BigDecimal.ZERO) < 0) {
+            if (_lockAccountEnforcement && lockable == 1 && _quotaManager.isLockable(account)) {
+                logger.info(String.format("Locking %s, as quota balance is lower than 0.", account));

Review Comment:
   Use new log format.



##########
plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaResponseBuilderImpl.java:
##########
@@ -654,4 +659,98 @@ public boolean deleteQuotaTariff(String quotaTariffUuid) {
         quotaTariff.setRemoved(_quotaService.computeAdjustedTime(new Date()));
         return _quotaTariffDao.updateQuotaTariff(quotaTariff);
     }
+
+    @Override
+    public Pair<QuotaEmailConfigurationVO, Double> configureQuotaEmail(QuotaConfigureEmailCmd cmd) {
+        validateQuotaConfigureEmailCmdParameters(cmd);
+
+        Double minBalance = cmd.getMinBalance();
+
+        if (minBalance != null) {
+            _quotaService.setMinBalance(cmd.getAccountId(), cmd.getMinBalance());
+        }
+
+        QuotaEmailConfigurationVO configurationVO = getQuotaEmailConfigurationVo(cmd);
+        return new Pair<>(configurationVO, minBalance);
+    }
+
+    private QuotaEmailConfigurationVO getQuotaEmailConfigurationVo(QuotaConfigureEmailCmd cmd) {

Review Comment:
   We could turn to `protected` and add unit tests.



##########
framework/quota/src/main/java/org/apache/cloudstack/quota/QuotaAlertManagerImpl.java:
##########
@@ -141,52 +146,72 @@ public boolean stop() {
     @Override
     public void checkAndSendQuotaAlertEmails() {
         List<DeferredQuotaEmail> deferredQuotaEmailList = new ArrayList<DeferredQuotaEmail>();
-        final BigDecimal zeroBalance = new BigDecimal(0);
+
+        logger.info("Checking and sending quota alert emails.");
         for (final QuotaAccountVO quotaAccount : _quotaAcc.listAllQuotaAccount()) {
-            if (logger.isDebugEnabled()) {
-                logger.debug("checkAndSendQuotaAlertEmails accId=" + quotaAccount.getId());
-            }
-            BigDecimal accountBalance = quotaAccount.getQuotaBalance();
-            Date balanceDate = quotaAccount.getQuotaBalanceDate();
-            Date alertDate = quotaAccount.getQuotaAlertDate();
-            int lockable = quotaAccount.getQuotaEnforce();
-            BigDecimal thresholdBalance = quotaAccount.getQuotaMinBalance();
-            if (accountBalance != null) {
-                AccountVO account = _accountDao.findById(quotaAccount.getId());
-                if (account == null) {
-                    continue; // the account is removed
-                }
-                if (logger.isDebugEnabled()) {
-                    logger.debug("checkAndSendQuotaAlertEmails: Check id=" + account.getId() + " bal=" + accountBalance + ", alertDate=" + alertDate + ", lockable=" + lockable);
-                }
-                if (accountBalance.compareTo(zeroBalance) < 0) {
-                    if (_lockAccountEnforcement && (lockable == 1)) {
-                        if (_quotaManager.isLockable(account)) {
-                            logger.info("Locking account " + account.getAccountName() + " due to quota < 0.");
-                            lockAccount(account.getId());
-                        }
-                    }
-                    if (alertDate == null || (balanceDate.after(alertDate) && getDifferenceDays(alertDate, new Date()) > 1)) {
-                        logger.info("Sending alert " + account.getAccountName() + " due to quota < 0.");
-                        deferredQuotaEmailList.add(new DeferredQuotaEmail(account, quotaAccount, QuotaConfig.QuotaEmailTemplateTypes.QUOTA_EMPTY));
-                    }
-                } else if (accountBalance.compareTo(thresholdBalance) < 0) {
-                    if (alertDate == null || (balanceDate.after(alertDate) && getDifferenceDays(alertDate, new Date()) > 1)) {
-                        logger.info("Sending alert " + account.getAccountName() + " due to quota below threshold.");
-                        deferredQuotaEmailList.add(new DeferredQuotaEmail(account, quotaAccount, QuotaConfig.QuotaEmailTemplateTypes.QUOTA_LOW));
-                    }
-                }
-            }
+            checkQuotaAlertEmailForAccount(deferredQuotaEmailList, quotaAccount);
         }
 
         for (DeferredQuotaEmail emailToBeSent : deferredQuotaEmailList) {
-            if (logger.isDebugEnabled()) {
-                logger.debug("checkAndSendQuotaAlertEmails: Attempting to send quota alert email to users of account: " + emailToBeSent.getAccount().getAccountName());
-            }
+            logger.debug(String.format("Attempting to send a quota alert email to users of account [%s].", emailToBeSent.getAccount().getAccountName()));
             sendQuotaAlert(emailToBeSent);
         }
     }
 
+    /**
+     * Checks a given quota account to see if they should receive any emails. First by checking if it has any balance at all, if its account can be found, then checks
+     * if they should receive either QUOTA_EMPTY or QUOTA_LOW emails, taking into account if these email templates are disabled or not for that account.
+     * */
+    protected void checkQuotaAlertEmailForAccount(List<DeferredQuotaEmail> deferredQuotaEmailList, QuotaAccountVO quotaAccount) {
+        logger.debug(String.format("Checking %s for email alerts.", quotaAccount));
+        BigDecimal accountBalance = quotaAccount.getQuotaBalance();
+
+        if (accountBalance == null) {
+            logger.debug(String.format("%s has a null balance, therefore it will not receive quota alert emails.", quotaAccount));
+            return;
+        }
+
+        AccountVO account = _accountDao.findById(quotaAccount.getId());
+        if (account == null) {
+            logger.debug(String.format("Account of %s is removed, thus it will not receive quota alert emails.", quotaAccount));

Review Comment:
   Use new log format.



##########
plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaResponseBuilderImpl.java:
##########
@@ -654,4 +659,98 @@ public boolean deleteQuotaTariff(String quotaTariffUuid) {
         quotaTariff.setRemoved(_quotaService.computeAdjustedTime(new Date()));
         return _quotaTariffDao.updateQuotaTariff(quotaTariff);
     }
+
+    @Override
+    public Pair<QuotaEmailConfigurationVO, Double> configureQuotaEmail(QuotaConfigureEmailCmd cmd) {
+        validateQuotaConfigureEmailCmdParameters(cmd);
+
+        Double minBalance = cmd.getMinBalance();
+
+        if (minBalance != null) {
+            _quotaService.setMinBalance(cmd.getAccountId(), cmd.getMinBalance());
+        }
+
+        QuotaEmailConfigurationVO configurationVO = getQuotaEmailConfigurationVo(cmd);
+        return new Pair<>(configurationVO, minBalance);
+    }
+
+    private QuotaEmailConfigurationVO getQuotaEmailConfigurationVo(QuotaConfigureEmailCmd cmd) {
+        if (cmd.getTemplateName() != null) {
+            List<QuotaEmailTemplatesVO> templateVO = _quotaEmailTemplateDao.listAllQuotaEmailTemplates(cmd.getTemplateName());
+            if (templateVO.isEmpty()) {
+                throw new InvalidParameterValueException(String.format("Could not find template with name [%s].", cmd.getTemplateName()));
+            }
+            long templateId = templateVO.get(0).getId();
+            QuotaEmailConfigurationVO configurationVO = quotaEmailConfigurationDao.findByAccountIdAndEmailTemplateId(cmd.getAccountId(), templateId);
+
+            if (configurationVO == null) {
+                configurationVO = new QuotaEmailConfigurationVO(cmd.getAccountId(), templateId, cmd.getEnable());
+                quotaEmailConfigurationDao.persistQuotaEmailConfiguration(configurationVO);
+                return configurationVO;
+            }
+
+            configurationVO.setEnabled(cmd.getEnable());
+            return quotaEmailConfigurationDao.updateQuotaEmailConfiguration(configurationVO);
+        }

Review Comment:
   We could invert the if to reduce indentation.
   
   



##########
framework/quota/src/main/java/org/apache/cloudstack/quota/QuotaAlertManagerImpl.java:
##########
@@ -141,52 +146,72 @@ public boolean stop() {
     @Override
     public void checkAndSendQuotaAlertEmails() {
         List<DeferredQuotaEmail> deferredQuotaEmailList = new ArrayList<DeferredQuotaEmail>();
-        final BigDecimal zeroBalance = new BigDecimal(0);
+
+        logger.info("Checking and sending quota alert emails.");
         for (final QuotaAccountVO quotaAccount : _quotaAcc.listAllQuotaAccount()) {
-            if (logger.isDebugEnabled()) {
-                logger.debug("checkAndSendQuotaAlertEmails accId=" + quotaAccount.getId());
-            }
-            BigDecimal accountBalance = quotaAccount.getQuotaBalance();
-            Date balanceDate = quotaAccount.getQuotaBalanceDate();
-            Date alertDate = quotaAccount.getQuotaAlertDate();
-            int lockable = quotaAccount.getQuotaEnforce();
-            BigDecimal thresholdBalance = quotaAccount.getQuotaMinBalance();
-            if (accountBalance != null) {
-                AccountVO account = _accountDao.findById(quotaAccount.getId());
-                if (account == null) {
-                    continue; // the account is removed
-                }
-                if (logger.isDebugEnabled()) {
-                    logger.debug("checkAndSendQuotaAlertEmails: Check id=" + account.getId() + " bal=" + accountBalance + ", alertDate=" + alertDate + ", lockable=" + lockable);
-                }
-                if (accountBalance.compareTo(zeroBalance) < 0) {
-                    if (_lockAccountEnforcement && (lockable == 1)) {
-                        if (_quotaManager.isLockable(account)) {
-                            logger.info("Locking account " + account.getAccountName() + " due to quota < 0.");
-                            lockAccount(account.getId());
-                        }
-                    }
-                    if (alertDate == null || (balanceDate.after(alertDate) && getDifferenceDays(alertDate, new Date()) > 1)) {
-                        logger.info("Sending alert " + account.getAccountName() + " due to quota < 0.");
-                        deferredQuotaEmailList.add(new DeferredQuotaEmail(account, quotaAccount, QuotaConfig.QuotaEmailTemplateTypes.QUOTA_EMPTY));
-                    }
-                } else if (accountBalance.compareTo(thresholdBalance) < 0) {
-                    if (alertDate == null || (balanceDate.after(alertDate) && getDifferenceDays(alertDate, new Date()) > 1)) {
-                        logger.info("Sending alert " + account.getAccountName() + " due to quota below threshold.");
-                        deferredQuotaEmailList.add(new DeferredQuotaEmail(account, quotaAccount, QuotaConfig.QuotaEmailTemplateTypes.QUOTA_LOW));
-                    }
-                }
-            }
+            checkQuotaAlertEmailForAccount(deferredQuotaEmailList, quotaAccount);
         }
 
         for (DeferredQuotaEmail emailToBeSent : deferredQuotaEmailList) {
-            if (logger.isDebugEnabled()) {
-                logger.debug("checkAndSendQuotaAlertEmails: Attempting to send quota alert email to users of account: " + emailToBeSent.getAccount().getAccountName());
-            }
+            logger.debug(String.format("Attempting to send a quota alert email to users of account [%s].", emailToBeSent.getAccount().getAccountName()));
             sendQuotaAlert(emailToBeSent);
         }
     }
 
+    /**
+     * Checks a given quota account to see if they should receive any emails. First by checking if it has any balance at all, if its account can be found, then checks
+     * if they should receive either QUOTA_EMPTY or QUOTA_LOW emails, taking into account if these email templates are disabled or not for that account.
+     * */
+    protected void checkQuotaAlertEmailForAccount(List<DeferredQuotaEmail> deferredQuotaEmailList, QuotaAccountVO quotaAccount) {
+        logger.debug(String.format("Checking %s for email alerts.", quotaAccount));
+        BigDecimal accountBalance = quotaAccount.getQuotaBalance();
+
+        if (accountBalance == null) {
+            logger.debug(String.format("%s has a null balance, therefore it will not receive quota alert emails.", quotaAccount));
+            return;
+        }
+
+        AccountVO account = _accountDao.findById(quotaAccount.getId());
+        if (account == null) {
+            logger.debug(String.format("Account of %s is removed, thus it will not receive quota alert emails.", quotaAccount));
+            return;
+        }
+
+        checkBalanceAndAddToEmailList(deferredQuotaEmailList, quotaAccount, account, accountBalance);
+    }
+
+    private void checkBalanceAndAddToEmailList(List<DeferredQuotaEmail> deferredQuotaEmailList, QuotaAccountVO quotaAccount, AccountVO account, BigDecimal accountBalance) {
+        Date balanceDate = quotaAccount.getQuotaBalanceDate();
+        Date alertDate = quotaAccount.getQuotaAlertDate();
+        int lockable = quotaAccount.getQuotaEnforce();
+        BigDecimal thresholdBalance = quotaAccount.getQuotaMinBalance();
+
+        logger.debug(String.format("Checking %s with accountBalance [%s], alertDate [%s] and lockable [%s] to see if a quota alert email should be sent.", account,
+                accountBalance, alertDate, lockable));
+
+        QuotaEmailConfigurationVO quotaEmpty = quotaEmailConfigurationDao.findByAccountIdAndEmailTemplateType(account.getAccountId(), QuotaEmailTemplateTypes.QUOTA_EMPTY);
+        QuotaEmailConfigurationVO quotaLow = quotaEmailConfigurationDao.findByAccountIdAndEmailTemplateType(account.getAccountId(), QuotaEmailTemplateTypes.QUOTA_LOW);
+
+        boolean shouldSendEmail = alertDate == null || (balanceDate.after(alertDate) && getDifferenceDays(alertDate, new Date()) > 1);
+
+        if (accountBalance.compareTo(BigDecimal.ZERO) < 0) {
+            if (_lockAccountEnforcement && lockable == 1 && _quotaManager.isLockable(account)) {
+                logger.info(String.format("Locking %s, as quota balance is lower than 0.", account));
+                lockAccount(account.getId());
+            }
+            if (quotaEmpty != null && quotaEmpty.isEnabled() && shouldSendEmail) {
+                logger.debug(String.format("Adding %s to the deferred emails list, as quota balance is lower than 0.", account));
+                deferredQuotaEmailList.add(new DeferredQuotaEmail(account, quotaAccount, QuotaEmailTemplateTypes.QUOTA_EMPTY));
+                return;
+            }
+        } else if (accountBalance.compareTo(thresholdBalance) < 0 && quotaLow != null && quotaLow.isEnabled() && shouldSendEmail) {
+            logger.debug(String.format("Adding %s to the deferred emails list, as quota balance [%s] is below the threshold [%s].", account, accountBalance, thresholdBalance));

Review Comment:
   Use new log format.



##########
framework/quota/src/main/java/org/apache/cloudstack/quota/QuotaAlertManagerImpl.java:
##########
@@ -141,52 +146,72 @@ public boolean stop() {
     @Override
     public void checkAndSendQuotaAlertEmails() {
         List<DeferredQuotaEmail> deferredQuotaEmailList = new ArrayList<DeferredQuotaEmail>();
-        final BigDecimal zeroBalance = new BigDecimal(0);
+
+        logger.info("Checking and sending quota alert emails.");
         for (final QuotaAccountVO quotaAccount : _quotaAcc.listAllQuotaAccount()) {
-            if (logger.isDebugEnabled()) {
-                logger.debug("checkAndSendQuotaAlertEmails accId=" + quotaAccount.getId());
-            }
-            BigDecimal accountBalance = quotaAccount.getQuotaBalance();
-            Date balanceDate = quotaAccount.getQuotaBalanceDate();
-            Date alertDate = quotaAccount.getQuotaAlertDate();
-            int lockable = quotaAccount.getQuotaEnforce();
-            BigDecimal thresholdBalance = quotaAccount.getQuotaMinBalance();
-            if (accountBalance != null) {
-                AccountVO account = _accountDao.findById(quotaAccount.getId());
-                if (account == null) {
-                    continue; // the account is removed
-                }
-                if (logger.isDebugEnabled()) {
-                    logger.debug("checkAndSendQuotaAlertEmails: Check id=" + account.getId() + " bal=" + accountBalance + ", alertDate=" + alertDate + ", lockable=" + lockable);
-                }
-                if (accountBalance.compareTo(zeroBalance) < 0) {
-                    if (_lockAccountEnforcement && (lockable == 1)) {
-                        if (_quotaManager.isLockable(account)) {
-                            logger.info("Locking account " + account.getAccountName() + " due to quota < 0.");
-                            lockAccount(account.getId());
-                        }
-                    }
-                    if (alertDate == null || (balanceDate.after(alertDate) && getDifferenceDays(alertDate, new Date()) > 1)) {
-                        logger.info("Sending alert " + account.getAccountName() + " due to quota < 0.");
-                        deferredQuotaEmailList.add(new DeferredQuotaEmail(account, quotaAccount, QuotaConfig.QuotaEmailTemplateTypes.QUOTA_EMPTY));
-                    }
-                } else if (accountBalance.compareTo(thresholdBalance) < 0) {
-                    if (alertDate == null || (balanceDate.after(alertDate) && getDifferenceDays(alertDate, new Date()) > 1)) {
-                        logger.info("Sending alert " + account.getAccountName() + " due to quota below threshold.");
-                        deferredQuotaEmailList.add(new DeferredQuotaEmail(account, quotaAccount, QuotaConfig.QuotaEmailTemplateTypes.QUOTA_LOW));
-                    }
-                }
-            }
+            checkQuotaAlertEmailForAccount(deferredQuotaEmailList, quotaAccount);
         }
 
         for (DeferredQuotaEmail emailToBeSent : deferredQuotaEmailList) {
-            if (logger.isDebugEnabled()) {
-                logger.debug("checkAndSendQuotaAlertEmails: Attempting to send quota alert email to users of account: " + emailToBeSent.getAccount().getAccountName());
-            }
+            logger.debug(String.format("Attempting to send a quota alert email to users of account [%s].", emailToBeSent.getAccount().getAccountName()));
             sendQuotaAlert(emailToBeSent);
         }
     }
 
+    /**
+     * Checks a given quota account to see if they should receive any emails. First by checking if it has any balance at all, if its account can be found, then checks
+     * if they should receive either QUOTA_EMPTY or QUOTA_LOW emails, taking into account if these email templates are disabled or not for that account.
+     * */
+    protected void checkQuotaAlertEmailForAccount(List<DeferredQuotaEmail> deferredQuotaEmailList, QuotaAccountVO quotaAccount) {
+        logger.debug(String.format("Checking %s for email alerts.", quotaAccount));
+        BigDecimal accountBalance = quotaAccount.getQuotaBalance();
+
+        if (accountBalance == null) {
+            logger.debug(String.format("%s has a null balance, therefore it will not receive quota alert emails.", quotaAccount));
+            return;
+        }
+
+        AccountVO account = _accountDao.findById(quotaAccount.getId());
+        if (account == null) {
+            logger.debug(String.format("Account of %s is removed, thus it will not receive quota alert emails.", quotaAccount));
+            return;
+        }
+
+        checkBalanceAndAddToEmailList(deferredQuotaEmailList, quotaAccount, account, accountBalance);
+    }
+
+    private void checkBalanceAndAddToEmailList(List<DeferredQuotaEmail> deferredQuotaEmailList, QuotaAccountVO quotaAccount, AccountVO account, BigDecimal accountBalance) {
+        Date balanceDate = quotaAccount.getQuotaBalanceDate();
+        Date alertDate = quotaAccount.getQuotaAlertDate();
+        int lockable = quotaAccount.getQuotaEnforce();
+        BigDecimal thresholdBalance = quotaAccount.getQuotaMinBalance();
+
+        logger.debug(String.format("Checking %s with accountBalance [%s], alertDate [%s] and lockable [%s] to see if a quota alert email should be sent.", account,

Review Comment:
   Use new log format.



##########
framework/quota/src/main/java/org/apache/cloudstack/quota/QuotaStatementImpl.java:
##########
@@ -107,10 +117,16 @@ public boolean stop() {
     public void sendStatement() {
 
         List<DeferredQuotaEmail> deferredQuotaEmailList = new ArrayList<DeferredQuotaEmail>();
+        QuotaEmailTemplatesVO templateVO = quotaEmailTemplatesDao.listAllQuotaEmailTemplates(QuotaConfig.QuotaEmailTemplateTypes.QUOTA_STATEMENT.toString()).get(0);
         for (final QuotaAccountVO quotaAccount : _quotaAcc.listAllQuotaAccount()) {
             if (quotaAccount.getQuotaBalance() == null) {
                 continue; // no quota usage for this account ever, ignore
             }
+            QuotaEmailConfigurationVO quotaEmailConfigurationVO = quotaEmailConfigurationDao.findByAccountIdAndEmailTemplateId(quotaAccount.getAccountId(), templateVO.getId());
+            if (quotaEmailConfigurationVO != null && !quotaEmailConfigurationVO.isEnabled()) {
+                logger.debug(String.format("%s has [%s] email disabled. Therefore the email will not be sent.", quotaAccount, QuotaConfig.QuotaEmailTemplateTypes.QUOTA_STATEMENT));

Review Comment:
   Use new log format.



##########
framework/quota/src/main/java/org/apache/cloudstack/quota/QuotaAlertManagerImpl.java:
##########
@@ -141,52 +146,72 @@ public boolean stop() {
     @Override
     public void checkAndSendQuotaAlertEmails() {
         List<DeferredQuotaEmail> deferredQuotaEmailList = new ArrayList<DeferredQuotaEmail>();
-        final BigDecimal zeroBalance = new BigDecimal(0);
+
+        logger.info("Checking and sending quota alert emails.");
         for (final QuotaAccountVO quotaAccount : _quotaAcc.listAllQuotaAccount()) {
-            if (logger.isDebugEnabled()) {
-                logger.debug("checkAndSendQuotaAlertEmails accId=" + quotaAccount.getId());
-            }
-            BigDecimal accountBalance = quotaAccount.getQuotaBalance();
-            Date balanceDate = quotaAccount.getQuotaBalanceDate();
-            Date alertDate = quotaAccount.getQuotaAlertDate();
-            int lockable = quotaAccount.getQuotaEnforce();
-            BigDecimal thresholdBalance = quotaAccount.getQuotaMinBalance();
-            if (accountBalance != null) {
-                AccountVO account = _accountDao.findById(quotaAccount.getId());
-                if (account == null) {
-                    continue; // the account is removed
-                }
-                if (logger.isDebugEnabled()) {
-                    logger.debug("checkAndSendQuotaAlertEmails: Check id=" + account.getId() + " bal=" + accountBalance + ", alertDate=" + alertDate + ", lockable=" + lockable);
-                }
-                if (accountBalance.compareTo(zeroBalance) < 0) {
-                    if (_lockAccountEnforcement && (lockable == 1)) {
-                        if (_quotaManager.isLockable(account)) {
-                            logger.info("Locking account " + account.getAccountName() + " due to quota < 0.");
-                            lockAccount(account.getId());
-                        }
-                    }
-                    if (alertDate == null || (balanceDate.after(alertDate) && getDifferenceDays(alertDate, new Date()) > 1)) {
-                        logger.info("Sending alert " + account.getAccountName() + " due to quota < 0.");
-                        deferredQuotaEmailList.add(new DeferredQuotaEmail(account, quotaAccount, QuotaConfig.QuotaEmailTemplateTypes.QUOTA_EMPTY));
-                    }
-                } else if (accountBalance.compareTo(thresholdBalance) < 0) {
-                    if (alertDate == null || (balanceDate.after(alertDate) && getDifferenceDays(alertDate, new Date()) > 1)) {
-                        logger.info("Sending alert " + account.getAccountName() + " due to quota below threshold.");
-                        deferredQuotaEmailList.add(new DeferredQuotaEmail(account, quotaAccount, QuotaConfig.QuotaEmailTemplateTypes.QUOTA_LOW));
-                    }
-                }
-            }
+            checkQuotaAlertEmailForAccount(deferredQuotaEmailList, quotaAccount);
         }
 
         for (DeferredQuotaEmail emailToBeSent : deferredQuotaEmailList) {
-            if (logger.isDebugEnabled()) {
-                logger.debug("checkAndSendQuotaAlertEmails: Attempting to send quota alert email to users of account: " + emailToBeSent.getAccount().getAccountName());
-            }
+            logger.debug(String.format("Attempting to send a quota alert email to users of account [%s].", emailToBeSent.getAccount().getAccountName()));
             sendQuotaAlert(emailToBeSent);
         }
     }
 
+    /**
+     * Checks a given quota account to see if they should receive any emails. First by checking if it has any balance at all, if its account can be found, then checks
+     * if they should receive either QUOTA_EMPTY or QUOTA_LOW emails, taking into account if these email templates are disabled or not for that account.
+     * */
+    protected void checkQuotaAlertEmailForAccount(List<DeferredQuotaEmail> deferredQuotaEmailList, QuotaAccountVO quotaAccount) {
+        logger.debug(String.format("Checking %s for email alerts.", quotaAccount));
+        BigDecimal accountBalance = quotaAccount.getQuotaBalance();
+
+        if (accountBalance == null) {
+            logger.debug(String.format("%s has a null balance, therefore it will not receive quota alert emails.", quotaAccount));
+            return;
+        }
+
+        AccountVO account = _accountDao.findById(quotaAccount.getId());
+        if (account == null) {
+            logger.debug(String.format("Account of %s is removed, thus it will not receive quota alert emails.", quotaAccount));
+            return;
+        }
+
+        checkBalanceAndAddToEmailList(deferredQuotaEmailList, quotaAccount, account, accountBalance);
+    }
+
+    private void checkBalanceAndAddToEmailList(List<DeferredQuotaEmail> deferredQuotaEmailList, QuotaAccountVO quotaAccount, AccountVO account, BigDecimal accountBalance) {
+        Date balanceDate = quotaAccount.getQuotaBalanceDate();
+        Date alertDate = quotaAccount.getQuotaAlertDate();
+        int lockable = quotaAccount.getQuotaEnforce();
+        BigDecimal thresholdBalance = quotaAccount.getQuotaMinBalance();
+
+        logger.debug(String.format("Checking %s with accountBalance [%s], alertDate [%s] and lockable [%s] to see if a quota alert email should be sent.", account,
+                accountBalance, alertDate, lockable));
+
+        QuotaEmailConfigurationVO quotaEmpty = quotaEmailConfigurationDao.findByAccountIdAndEmailTemplateType(account.getAccountId(), QuotaEmailTemplateTypes.QUOTA_EMPTY);
+        QuotaEmailConfigurationVO quotaLow = quotaEmailConfigurationDao.findByAccountIdAndEmailTemplateType(account.getAccountId(), QuotaEmailTemplateTypes.QUOTA_LOW);
+
+        boolean shouldSendEmail = alertDate == null || (balanceDate.after(alertDate) && getDifferenceDays(alertDate, new Date()) > 1);
+
+        if (accountBalance.compareTo(BigDecimal.ZERO) < 0) {
+            if (_lockAccountEnforcement && lockable == 1 && _quotaManager.isLockable(account)) {
+                logger.info(String.format("Locking %s, as quota balance is lower than 0.", account));
+                lockAccount(account.getId());
+            }
+            if (quotaEmpty != null && quotaEmpty.isEnabled() && shouldSendEmail) {
+                logger.debug(String.format("Adding %s to the deferred emails list, as quota balance is lower than 0.", account));

Review Comment:
   Use new log format.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@cloudstack.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Quota email configuration [cloudstack]

Posted by "github-actions[bot] (via GitHub)" <gi...@apache.org>.
github-actions[bot] commented on PR #8307:
URL: https://github.com/apache/cloudstack/pull/8307#issuecomment-1859726092

   This pull request has merge conflicts. Dear author, please fix the conflicts and sync your branch with the base branch.


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@cloudstack.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Quota email configuration [cloudstack]

Posted by "blueorangutan (via GitHub)" <gi...@apache.org>.
blueorangutan commented on PR #8307:
URL: https://github.com/apache/cloudstack/pull/8307#issuecomment-1955078669

   Packaging result [SF]: ✔️ el7 ✔️ el8 ✔️ el9 ✔️ debian ✔️ suse15. SL-JID 8726


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@cloudstack.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Quota email configuration [cloudstack]

Posted by "JoaoJandre (via GitHub)" <gi...@apache.org>.
JoaoJandre commented on PR #8307:
URL: https://github.com/apache/cloudstack/pull/8307#issuecomment-2003657468

   Merging based on approvals, manual test results and CI results.


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@cloudstack.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Quota email configuration [cloudstack]

Posted by "JoaoJandre (via GitHub)" <gi...@apache.org>.
JoaoJandre commented on PR #8307:
URL: https://github.com/apache/cloudstack/pull/8307#issuecomment-1971833414

   @GutoVeronezi @BryanMLima @winterhazel Could someone review this PR?


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@cloudstack.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Quota email configuration [cloudstack]

Posted by "JoaoJandre (via GitHub)" <gi...@apache.org>.
JoaoJandre commented on PR #8307:
URL: https://github.com/apache/cloudstack/pull/8307#issuecomment-1954939754

   @blueorangutan package


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@cloudstack.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Quota email configuration [cloudstack]

Posted by "github-actions[bot] (via GitHub)" <gi...@apache.org>.
github-actions[bot] commented on PR #8307:
URL: https://github.com/apache/cloudstack/pull/8307#issuecomment-1964318715

   This pull request has merge conflicts. Dear author, please fix the conflicts and sync your branch with the base branch.


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@cloudstack.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Quota email configuration [cloudstack]

Posted by "JoaoJandre (via GitHub)" <gi...@apache.org>.
JoaoJandre commented on code in PR #8307:
URL: https://github.com/apache/cloudstack/pull/8307#discussion_r1487733347


##########
framework/quota/src/main/java/org/apache/cloudstack/quota/dao/QuotaEmailConfigurationDaoImpl.java:
##########
@@ -0,0 +1,132 @@
+// 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 com.cloud.utils.db.GenericDaoBase;
+import com.cloud.utils.db.JoinBuilder;
+import com.cloud.utils.db.SearchBuilder;
+import com.cloud.utils.db.SearchCriteria;
+import com.cloud.utils.db.Transaction;
+import com.cloud.utils.db.TransactionCallback;
+import com.cloud.utils.db.TransactionCallbackNoReturn;
+import com.cloud.utils.db.TransactionLegacy;
+import com.cloud.utils.db.TransactionStatus;
+import org.apache.cloudstack.quota.constant.QuotaConfig;
+import org.apache.cloudstack.quota.vo.QuotaEmailConfigurationVO;
+import org.apache.cloudstack.quota.vo.QuotaEmailTemplatesVO;
+import org.springframework.stereotype.Component;
+
+import javax.annotation.PostConstruct;
+import javax.inject.Inject;
+import java.util.List;
+
+@Component
+public class QuotaEmailConfigurationDaoImpl extends GenericDaoBase<QuotaEmailConfigurationVO, Long> implements QuotaEmailConfigurationDao {
+
+    @Inject
+    private QuotaEmailTemplatesDao quotaEmailTemplatesDao;
+
+    private SearchBuilder<QuotaEmailConfigurationVO> searchBuilderFindByIds;
+
+    private SearchBuilder<QuotaEmailTemplatesVO> searchBuilderFindByTemplateName;
+
+    private SearchBuilder<QuotaEmailConfigurationVO> searchBuilderFindByTemplateTypeAndAccountId;
+
+    @PostConstruct
+    public void init() {
+        searchBuilderFindByIds = createSearchBuilder();
+        searchBuilderFindByIds.and("account_id", searchBuilderFindByIds.entity().getAccountId(), SearchCriteria.Op.EQ);
+        searchBuilderFindByIds.and("email_template_id", searchBuilderFindByIds.entity().getEmailTemplateId(), SearchCriteria.Op.EQ);
+        searchBuilderFindByIds.done();
+
+        searchBuilderFindByTemplateName = quotaEmailTemplatesDao.createSearchBuilder();
+        searchBuilderFindByTemplateName.and("template_name", searchBuilderFindByTemplateName.entity().getTemplateName(), SearchCriteria.Op.EQ);
+
+        searchBuilderFindByTemplateTypeAndAccountId = createSearchBuilder();
+        searchBuilderFindByTemplateTypeAndAccountId.and("account_id", searchBuilderFindByTemplateTypeAndAccountId.entity().getAccountId(), SearchCriteria.Op.EQ);
+        searchBuilderFindByTemplateTypeAndAccountId.join("email_template_id", searchBuilderFindByTemplateName, searchBuilderFindByTemplateName.entity().getId(),
+                searchBuilderFindByTemplateTypeAndAccountId.entity().getEmailTemplateId(), JoinBuilder.JoinType.INNER);
+
+        searchBuilderFindByTemplateName.done();
+        searchBuilderFindByTemplateTypeAndAccountId.done();

Review Comment:
   `searchBuilderFindByTemplateName` and `searchBuilderFindByTemplateTypeAndAccountId` are used for a join, while `searchBuilderFindByIds` is for a simple query. We could always use the join but I it would be wasteful, I'd rather keep them separate. 



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@cloudstack.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Quota email configuration [cloudstack]

Posted by "blueorangutan (via GitHub)" <gi...@apache.org>.
blueorangutan commented on PR #8307:
URL: https://github.com/apache/cloudstack/pull/8307#issuecomment-1997393526

   @JoaoJandre a [SL] Jenkins job has been kicked to build packages. It will be bundled with  KVM, XenServer and VMware SystemVM templates. I'll keep you posted as I make progress.


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@cloudstack.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Quota email configuration [cloudstack]

Posted by "github-actions[bot] (via GitHub)" <gi...@apache.org>.
github-actions[bot] commented on PR #8307:
URL: https://github.com/apache/cloudstack/pull/8307#issuecomment-1994391343

   This pull request has merge conflicts. Dear author, please fix the conflicts and sync your branch with the base branch.


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@cloudstack.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Quota email configuration [cloudstack]

Posted by "codecov[bot] (via GitHub)" <gi...@apache.org>.
codecov[bot] commented on PR #8307:
URL: https://github.com/apache/cloudstack/pull/8307#issuecomment-1840856710

   ## [Codecov](https://app.codecov.io/gh/apache/cloudstack/pull/8307?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache) Report
   All modified and coverable lines are covered by tests :white_check_mark:
   > Comparison is base [(`5651eab`)](https://app.codecov.io/gh/apache/cloudstack/commit/5651eab49cf3eab58b8d3d14dcc067be5d0e676c?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache) 23.44% compared to head [(`07a28d3`)](https://app.codecov.io/gh/apache/cloudstack/pull/8307?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache) 4.44%.
   > Report is 8 commits behind head on main.
   
   
   <details><summary>Additional details and impacted files</summary>
   
   
   ```diff
   @@             Coverage Diff              @@
   ##               main   #8307       +/-   ##
   ============================================
   - Coverage     23.44%   4.44%   -19.00%     
   ============================================
     Files          5100     360     -4740     
     Lines        346393   28231   -318162     
     Branches      49768    4918    -44850     
   ============================================
   - Hits          81219    1256    -79963     
   + Misses       253491   26837   -226654     
   + Partials      11683     138    -11545     
   ```
   
   | [Flag](https://app.codecov.io/gh/apache/cloudstack/pull/8307/flags?src=pr&el=flags&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache) | Coverage Δ | |
   |---|---|---|
   | [simulator-marvin-tests](https://app.codecov.io/gh/apache/cloudstack/pull/8307/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache) | `?` | |
   | [uitests](https://app.codecov.io/gh/apache/cloudstack/pull/8307/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache) | `4.44% <ø> (ø)` | |
   
   Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache#carryforward-flags-in-the-pull-request-comment) to find out more.
   
   
   </details>
   
   [:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/apache/cloudstack/pull/8307?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache).   
   :loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache).
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@cloudstack.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Quota email configuration [cloudstack]

Posted by "BryanMLima (via GitHub)" <gi...@apache.org>.
BryanMLima commented on code in PR #8307:
URL: https://github.com/apache/cloudstack/pull/8307#discussion_r1508141919


##########
plugins/database/quota/src/main/java/org/apache/cloudstack/api/command/QuotaConfigureEmailCmd.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 com.cloud.utils.Pair;
+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.QuotaConfigureEmailResponse;
+import org.apache.cloudstack.api.response.QuotaResponseBuilder;
+import org.apache.cloudstack.quota.vo.QuotaEmailConfigurationVO;
+
+import javax.inject.Inject;
+
+@APICommand(name = "quotaConfigureEmail", responseObject = QuotaConfigureEmailResponse.class, description = "Configure a quota email template", since = "4.20.0.0",

Review Comment:
   Should this be changed to `20.0.0`?
   ```suggestion
   @APICommand(name = "quotaConfigureEmail", responseObject = QuotaConfigureEmailResponse.class, description = "Configure a quota email template", since = "20.0.0",
   ```



##########
plugins/database/quota/src/main/java/org/apache/cloudstack/api/command/QuotaListEmailConfigurationCmd.java:
##########
@@ -0,0 +1,54 @@
+//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.user.Account;
+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.ListResponse;
+import org.apache.cloudstack.api.response.QuotaConfigureEmailResponse;
+import org.apache.cloudstack.api.response.QuotaResponseBuilder;
+
+import javax.inject.Inject;
+
+@APICommand(name = "quotaListEmailConfiguration", responseObject = QuotaConfigureEmailResponse.class, description = "List quota email template configurations", since = "4.20.0.0",

Review Comment:
   Should this be changed to 20.0.0?
   ```suggestion
   @APICommand(name = "quotaListEmailConfiguration", responseObject = QuotaConfigureEmailResponse.class, description = "List quota email template configurations", since = "20.0.0",
   ```



##########
framework/quota/src/main/java/org/apache/cloudstack/quota/QuotaAlertManagerImpl.java:
##########
@@ -138,55 +143,102 @@ public boolean stop() {
         return true;
     }
 
+    /**
+     * Returns whether a Quota email type is enabled or not for the provided account.
+     */
+    @Override
+    public boolean isQuotaEmailTypeEnabledForAccount(AccountVO account, QuotaEmailTemplateTypes quotaEmailTemplateType) {
+        boolean quotaEmailsEnabled = QuotaConfig.QuotaEnableEmails.valueIn(account.getAccountId());
+        if (!quotaEmailsEnabled) {
+            logger.debug("Configuration [{}] is disabled for account [{}]. Therefore, the account will not receive Quota email of type [{}].", QuotaConfig.QuotaEnableEmails.key(), account, quotaEmailTemplateType);
+            return false;
+        }
+
+        QuotaEmailConfigurationVO quotaEmail = quotaEmailConfigurationDao.findByAccountIdAndEmailTemplateType(account.getAccountId(), quotaEmailTemplateType);
+
+        boolean emailEnabled = quotaEmail == null || quotaEmail.isEnabled();
+        if (emailEnabled) {
+            logger.debug("Quota email [{}] is enabled for account [{}].", quotaEmailTemplateType, account);
+        } else {
+            logger.debug("Quota email [{}] has been manually disabled for account [{}] through the API quotaConfigureEmail.", quotaEmailTemplateType, account);
+        }
+        return emailEnabled;
+    }
+
+
     @Override
     public void checkAndSendQuotaAlertEmails() {
         List<DeferredQuotaEmail> deferredQuotaEmailList = new ArrayList<DeferredQuotaEmail>();
-        final BigDecimal zeroBalance = new BigDecimal(0);
+
+        logger.info("Checking and sending quota alert emails.");
         for (final QuotaAccountVO quotaAccount : _quotaAcc.listAllQuotaAccount()) {
-            if (logger.isDebugEnabled()) {
-                logger.debug("checkAndSendQuotaAlertEmails accId=" + quotaAccount.getId());
-            }
-            BigDecimal accountBalance = quotaAccount.getQuotaBalance();
-            Date balanceDate = quotaAccount.getQuotaBalanceDate();
-            Date alertDate = quotaAccount.getQuotaAlertDate();
-            int lockable = quotaAccount.getQuotaEnforce();
-            BigDecimal thresholdBalance = quotaAccount.getQuotaMinBalance();
-            if (accountBalance != null) {
-                AccountVO account = _accountDao.findById(quotaAccount.getId());
-                if (account == null) {
-                    continue; // the account is removed
-                }
-                if (logger.isDebugEnabled()) {
-                    logger.debug("checkAndSendQuotaAlertEmails: Check id=" + account.getId() + " bal=" + accountBalance + ", alertDate=" + alertDate + ", lockable=" + lockable);
-                }
-                if (accountBalance.compareTo(zeroBalance) < 0) {
-                    if (_lockAccountEnforcement && (lockable == 1)) {
-                        if (_quotaManager.isLockable(account)) {
-                            logger.info("Locking account " + account.getAccountName() + " due to quota < 0.");
-                            lockAccount(account.getId());
-                        }
-                    }
-                    if (alertDate == null || (balanceDate.after(alertDate) && getDifferenceDays(alertDate, new Date()) > 1)) {
-                        logger.info("Sending alert " + account.getAccountName() + " due to quota < 0.");
-                        deferredQuotaEmailList.add(new DeferredQuotaEmail(account, quotaAccount, QuotaConfig.QuotaEmailTemplateTypes.QUOTA_EMPTY));
-                    }
-                } else if (accountBalance.compareTo(thresholdBalance) < 0) {
-                    if (alertDate == null || (balanceDate.after(alertDate) && getDifferenceDays(alertDate, new Date()) > 1)) {
-                        logger.info("Sending alert " + account.getAccountName() + " due to quota below threshold.");
-                        deferredQuotaEmailList.add(new DeferredQuotaEmail(account, quotaAccount, QuotaConfig.QuotaEmailTemplateTypes.QUOTA_LOW));
-                    }
-                }
-            }
+            checkQuotaAlertEmailForAccount(deferredQuotaEmailList, quotaAccount);
         }
 
         for (DeferredQuotaEmail emailToBeSent : deferredQuotaEmailList) {
-            if (logger.isDebugEnabled()) {
-                logger.debug("checkAndSendQuotaAlertEmails: Attempting to send quota alert email to users of account: " + emailToBeSent.getAccount().getAccountName());
-            }
+            logger.debug("Attempting to send a quota alert email to users of account [{}].", emailToBeSent.getAccount().getAccountName());
             sendQuotaAlert(emailToBeSent);
         }
     }
 
+    /**
+     * Checks a given quota account to see if they should receive any emails. First by checking if it has any balance at all, if its account can be found, then checks
+     * if they should receive either QUOTA_EMPTY or QUOTA_LOW emails, taking into account if these email templates are disabled or not for that account.
+     * */
+    protected void checkQuotaAlertEmailForAccount(List<DeferredQuotaEmail> deferredQuotaEmailList, QuotaAccountVO quotaAccount) {
+        logger.debug("Checking {} for email alerts.", quotaAccount);
+        BigDecimal accountBalance = quotaAccount.getQuotaBalance();
+
+        if (accountBalance == null) {
+            logger.debug("{} has a null balance, therefore it will not receive quota alert emails.", quotaAccount);
+            return;
+        }
+
+        AccountVO account = _accountDao.findById(quotaAccount.getId());
+        if (account == null) {
+            logger.debug("Account of {} is removed, thus it will not receive quota alert emails.", quotaAccount);
+            return;
+        }
+
+        checkBalanceAndAddToEmailList(deferredQuotaEmailList, quotaAccount, account, accountBalance);
+    }
+
+    private void checkBalanceAndAddToEmailList(List<DeferredQuotaEmail> deferredQuotaEmailList, QuotaAccountVO quotaAccount, AccountVO account, BigDecimal accountBalance) {
+        Date balanceDate = quotaAccount.getQuotaBalanceDate();
+        Date alertDate = quotaAccount.getQuotaAlertDate();
+        int lockable = quotaAccount.getQuotaEnforce();
+        BigDecimal thresholdBalance = quotaAccount.getQuotaMinBalance();
+
+        logger.debug("Checking {} with accountBalance [{}], alertDate [{}] and lockable [{}] to see if a quota alert email should be sent.", account,
+                accountBalance, alertDate, lockable);
+
+

Review Comment:
   ```suggestion
   ```



##########
framework/quota/src/main/java/org/apache/cloudstack/quota/dao/QuotaEmailTemplatesDaoImpl.java:
##########
@@ -68,4 +68,14 @@ public Boolean doInTransaction(final TransactionStatus status) {
             }
         });
     }
+
+    @Override
+    public QuotaEmailTemplatesVO findById(long id) {
+        return Transaction.execute(TransactionLegacy.USAGE_DB, new TransactionCallback<QuotaEmailTemplatesVO>() {
+            @Override
+            public QuotaEmailTemplatesVO doInTransaction(final TransactionStatus status) {
+                return QuotaEmailTemplatesDaoImpl.super.findById(id);
+            }
+        });

Review Comment:
   Why was this marked as resolved? It is not using lambda.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@cloudstack.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Quota email configuration [cloudstack]

Posted by "JoaoJandre (via GitHub)" <gi...@apache.org>.
JoaoJandre commented on PR #8307:
URL: https://github.com/apache/cloudstack/pull/8307#issuecomment-1997392362

   @blueorangutan package
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@cloudstack.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Quota email configuration [cloudstack]

Posted by "github-actions[bot] (via GitHub)" <gi...@apache.org>.
github-actions[bot] commented on PR #8307:
URL: https://github.com/apache/cloudstack/pull/8307#issuecomment-1952018339

   This pull request has merge conflicts. Dear author, please fix the conflicts and sync your branch with the base branch.


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@cloudstack.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Quota email configuration [cloudstack]

Posted by "BryanMLima (via GitHub)" <gi...@apache.org>.
BryanMLima commented on code in PR #8307:
URL: https://github.com/apache/cloudstack/pull/8307#discussion_r1508883903


##########
plugins/database/quota/src/main/java/org/apache/cloudstack/api/command/QuotaConfigureEmailCmd.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 com.cloud.utils.Pair;
+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.QuotaConfigureEmailResponse;
+import org.apache.cloudstack.api.response.QuotaResponseBuilder;
+import org.apache.cloudstack.quota.vo.QuotaEmailConfigurationVO;
+
+import javax.inject.Inject;
+
+@APICommand(name = "quotaConfigureEmail", responseObject = QuotaConfigureEmailResponse.class, description = "Configure a quota email template", since = "4.20.0.0",

Review Comment:
   Disregard this comment as the vote was not concluded.



##########
plugins/database/quota/src/main/java/org/apache/cloudstack/api/command/QuotaListEmailConfigurationCmd.java:
##########
@@ -0,0 +1,54 @@
+//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.user.Account;
+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.ListResponse;
+import org.apache.cloudstack.api.response.QuotaConfigureEmailResponse;
+import org.apache.cloudstack.api.response.QuotaResponseBuilder;
+
+import javax.inject.Inject;
+
+@APICommand(name = "quotaListEmailConfiguration", responseObject = QuotaConfigureEmailResponse.class, description = "List quota email template configurations", since = "4.20.0.0",

Review Comment:
   Disregard this comment as the vote was not concluded.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@cloudstack.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Quota email configuration [cloudstack]

Posted by "winterhazel (via GitHub)" <gi...@apache.org>.
winterhazel commented on code in PR #8307:
URL: https://github.com/apache/cloudstack/pull/8307#discussion_r1509247066


##########
engine/schema/src/main/resources/META-INF/db/schema-41900to42000.sql:
##########
@@ -34,3 +34,12 @@ UPDATE `cloud`.`service_offering` SET ram_size = 512 WHERE unique_name IN ("Clou
                                                                            "Cloud.Com-InternalLBVm", "Cloud.Com-InternalLBVm-Local",
                                                                            "Cloud.Com-ElasticLBVm", "Cloud.Com-ElasticLBVm-Local")
                                                     AND system_use = 1 AND ram_size < 512;
+
+-- Create table to persist quota email template configurations
+CREATE TABLE IF NOT EXISTS `cloud_usage`.`quota_email_configuration`(
+    `account_id` int(11) NOT NULL,
+    `email_template_id` bigint(20) NOT NULL,
+    `enabled` int(1) UNSIGNED NOT NULL,
+    PRIMARY KEY (`account_id`, `email_template_id`),
+    CONSTRAINT `FK_quota_email_configuration_account_id` FOREIGN KEY (`account_id`) REFERENCES `cloud_usage`.`quota_account`(`account_id`),
+    CONSTRAINT `FK_quota_email_configuration_email_te1mplate_id` FOREIGN KEY (`email_template_id`) REFERENCES `cloud_usage`.`quota_email_templates`(`id`));

Review Comment:
   ```suggestion
       CONSTRAINT `FK_quota_email_configuration_email_template_id` FOREIGN KEY (`email_template_id`) REFERENCES `cloud_usage`.`quota_email_templates`(`id`));
   ```



##########
framework/quota/src/main/java/org/apache/cloudstack/quota/QuotaAlertManagerImpl.java:
##########
@@ -138,55 +143,100 @@ public boolean stop() {
         return true;
     }
 
+    /**
+     * Returns whether a Quota email type is enabled or not for the provided account.
+     */
+    @Override
+    public boolean isQuotaEmailTypeEnabledForAccount(AccountVO account, QuotaEmailTemplateTypes quotaEmailTemplateType) {
+        boolean quotaEmailsEnabled = QuotaConfig.QuotaEnableEmails.valueIn(account.getAccountId());
+        if (!quotaEmailsEnabled) {
+            logger.debug("Configuration [{}] is disabled for account [{}]. Therefore, the account will not receive Quota email of type [{}].", QuotaConfig.QuotaEnableEmails.key(), account, quotaEmailTemplateType);
+            return false;
+        }
+
+        QuotaEmailConfigurationVO quotaEmail = quotaEmailConfigurationDao.findByAccountIdAndEmailTemplateType(account.getAccountId(), quotaEmailTemplateType);
+
+        boolean emailEnabled = quotaEmail == null || quotaEmail.isEnabled();
+        if (emailEnabled) {
+            logger.debug("Quota email [{}] is enabled for account [{}].", quotaEmailTemplateType, account);
+        } else {
+            logger.debug("Quota email [{}] has been manually disabled for account [{}] through the API quotaConfigureEmail.", quotaEmailTemplateType, account);
+        }
+        return emailEnabled;
+    }
+
+
     @Override
     public void checkAndSendQuotaAlertEmails() {
         List<DeferredQuotaEmail> deferredQuotaEmailList = new ArrayList<DeferredQuotaEmail>();
-        final BigDecimal zeroBalance = new BigDecimal(0);
+
+        logger.info("Checking and sending quota alert emails.");
         for (final QuotaAccountVO quotaAccount : _quotaAcc.listAllQuotaAccount()) {
-            if (logger.isDebugEnabled()) {
-                logger.debug("checkAndSendQuotaAlertEmails accId=" + quotaAccount.getId());
-            }
-            BigDecimal accountBalance = quotaAccount.getQuotaBalance();
-            Date balanceDate = quotaAccount.getQuotaBalanceDate();
-            Date alertDate = quotaAccount.getQuotaAlertDate();
-            int lockable = quotaAccount.getQuotaEnforce();
-            BigDecimal thresholdBalance = quotaAccount.getQuotaMinBalance();
-            if (accountBalance != null) {
-                AccountVO account = _accountDao.findById(quotaAccount.getId());
-                if (account == null) {
-                    continue; // the account is removed
-                }
-                if (logger.isDebugEnabled()) {
-                    logger.debug("checkAndSendQuotaAlertEmails: Check id=" + account.getId() + " bal=" + accountBalance + ", alertDate=" + alertDate + ", lockable=" + lockable);
-                }
-                if (accountBalance.compareTo(zeroBalance) < 0) {
-                    if (_lockAccountEnforcement && (lockable == 1)) {
-                        if (_quotaManager.isLockable(account)) {
-                            logger.info("Locking account " + account.getAccountName() + " due to quota < 0.");
-                            lockAccount(account.getId());
-                        }
-                    }
-                    if (alertDate == null || (balanceDate.after(alertDate) && getDifferenceDays(alertDate, new Date()) > 1)) {
-                        logger.info("Sending alert " + account.getAccountName() + " due to quota < 0.");
-                        deferredQuotaEmailList.add(new DeferredQuotaEmail(account, quotaAccount, QuotaConfig.QuotaEmailTemplateTypes.QUOTA_EMPTY));
-                    }
-                } else if (accountBalance.compareTo(thresholdBalance) < 0) {
-                    if (alertDate == null || (balanceDate.after(alertDate) && getDifferenceDays(alertDate, new Date()) > 1)) {
-                        logger.info("Sending alert " + account.getAccountName() + " due to quota below threshold.");
-                        deferredQuotaEmailList.add(new DeferredQuotaEmail(account, quotaAccount, QuotaConfig.QuotaEmailTemplateTypes.QUOTA_LOW));
-                    }
-                }
-            }
+            checkQuotaAlertEmailForAccount(deferredQuotaEmailList, quotaAccount);
         }
 
         for (DeferredQuotaEmail emailToBeSent : deferredQuotaEmailList) {
-            if (logger.isDebugEnabled()) {
-                logger.debug("checkAndSendQuotaAlertEmails: Attempting to send quota alert email to users of account: " + emailToBeSent.getAccount().getAccountName());
-            }
+            logger.debug("Attempting to send a quota alert email to users of account [{}].", emailToBeSent.getAccount().getAccountName());
             sendQuotaAlert(emailToBeSent);
         }
     }
 
+    /**
+     * Checks a given quota account to see if they should receive any emails. First by checking if it has any balance at all, if its account can be found, then checks
+     * if they should receive either QUOTA_EMPTY or QUOTA_LOW emails, taking into account if these email templates are disabled or not for that account.
+     * */
+    protected void checkQuotaAlertEmailForAccount(List<DeferredQuotaEmail> deferredQuotaEmailList, QuotaAccountVO quotaAccount) {
+        logger.debug("Checking {} for email alerts.", quotaAccount);
+        BigDecimal accountBalance = quotaAccount.getQuotaBalance();
+
+        if (accountBalance == null) {
+            logger.debug("{} has a null balance, therefore it will not receive quota alert emails.", quotaAccount);
+            return;
+        }
+
+        AccountVO account = _accountDao.findById(quotaAccount.getId());
+        if (account == null) {
+            logger.debug("Account of {} is removed, thus it will not receive quota alert emails.", quotaAccount);
+            return;
+        }
+
+        checkBalanceAndAddToEmailList(deferredQuotaEmailList, quotaAccount, account, accountBalance);
+    }
+
+    private void checkBalanceAndAddToEmailList(List<DeferredQuotaEmail> deferredQuotaEmailList, QuotaAccountVO quotaAccount, AccountVO account, BigDecimal accountBalance) {
+        Date balanceDate = quotaAccount.getQuotaBalanceDate();
+        Date alertDate = quotaAccount.getQuotaAlertDate();
+        int lockable = quotaAccount.getQuotaEnforce();
+        BigDecimal thresholdBalance = quotaAccount.getQuotaMinBalance();
+
+        logger.debug("Checking {} with accountBalance [{}], alertDate [{}] and lockable [{}] to see if a quota alert email should be sent.", account,
+                accountBalance, alertDate, lockable);
+
+        boolean shouldSendEmail = alertDate == null || (balanceDate.after(alertDate) && getDifferenceDays(alertDate, new Date()) > 1);
+
+        if (accountBalance.compareTo(BigDecimal.ZERO) < 0) {
+            if (_lockAccountEnforcement && lockable == 1 && _quotaManager.isLockable(account)) {
+                logger.info("Locking {}, as quota balance is lower than 0.", account);
+                lockAccount(account.getId());
+            }
+
+            boolean quotaEmptyEmailEnabled = isQuotaEmailTypeEnabledForAccount(account, QuotaEmailTemplateTypes.QUOTA_EMPTY);
+            if (quotaEmptyEmailEnabled && shouldSendEmail) {
+                logger.debug("Adding {} to the deferred emails list, as quota balance is lower than 0.", account);
+                deferredQuotaEmailList.add(new DeferredQuotaEmail(account, quotaAccount, QuotaEmailTemplateTypes.QUOTA_EMPTY));
+                return;
+            }
+        } else if (accountBalance.compareTo(thresholdBalance) < 0) {
+            boolean quotaLowEmailEnabled = isQuotaEmailTypeEnabledForAccount(account, QuotaEmailTemplateTypes.QUOTA_LOW);
+            if (quotaLowEmailEnabled && shouldSendEmail) {
+                logger.debug("Adding {} to the deferred emails list, as quota balance [{}] is below the threshold [{}].", account, accountBalance, thresholdBalance);
+                deferredQuotaEmailList.add(new DeferredQuotaEmail(account, quotaAccount, QuotaEmailTemplateTypes.QUOTA_LOW));
+                return;
+            }
+        }
+            logger.debug("{} will not receive any quota alert emails in this round.", account);

Review Comment:
   ```suggestion
           logger.debug("{} will not receive any quota alert emails in this round.", account);
   ```



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@cloudstack.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Quota email configuration [cloudstack]

Posted by "github-actions[bot] (via GitHub)" <gi...@apache.org>.
github-actions[bot] commented on PR #8307:
URL: https://github.com/apache/cloudstack/pull/8307#issuecomment-1934110374

   This pull request has merge conflicts. Dear author, please fix the conflicts and sync your branch with the base branch.


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@cloudstack.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Quota email configuration [cloudstack]

Posted by "github-actions[bot] (via GitHub)" <gi...@apache.org>.
github-actions[bot] commented on PR #8307:
URL: https://github.com/apache/cloudstack/pull/8307#issuecomment-1976512797

   This pull request has merge conflicts. Dear author, please fix the conflicts and sync your branch with the base branch.


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@cloudstack.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Quota email configuration [cloudstack]

Posted by "blueorangutan (via GitHub)" <gi...@apache.org>.
blueorangutan commented on PR #8307:
URL: https://github.com/apache/cloudstack/pull/8307#issuecomment-2000349897

   <b>[SF] Trillian test result (tid-9489)</b>
   Environment: kvm-centos7 (x2), Advanced Networking with Mgmt server 7
   Total time taken: 53759 seconds
   Marvin logs: https://github.com/blueorangutan/acs-prs/releases/download/trillian/pr8307-t9489-kvm-centos7.zip
   Smoke tests completed. 129 look OK, 0 have errors, 0 did not run
   Only failed and skipped tests results shown below:
   
   
   Test | Result | Time (s) | Test File
   --- | --- | --- | ---
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@cloudstack.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Quota email configuration [cloudstack]

Posted by "JoaoJandre (via GitHub)" <gi...@apache.org>.
JoaoJandre commented on PR #8307:
URL: https://github.com/apache/cloudstack/pull/8307#issuecomment-1980852597

   Thank you for testing @winterhazel 
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@cloudstack.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Quota email configuration [cloudstack]

Posted by "winterhazel (via GitHub)" <gi...@apache.org>.
winterhazel commented on PR #8307:
URL: https://github.com/apache/cloudstack/pull/8307#issuecomment-1979546446

   I tested the feature introduced in this PR. It seems to be working as intended.
   
   First, I configured Quota to send e-mails. I also created an account with my e-mail, added 9 credits to it in order to enable Quota and configured its minimum balance to 10. To receive the e-mails at any time, I would set both `cloud_usage.quota_account.` `quota_alert_date`/`quota_statement_date` to `NULL` and execute `quotaUpdate`.
   
   1. I set `quota.enable.emails` to `true`.
   
   2. I enabled all e-mails for the account through the API `quotaConfigureEmail`.
   
       <details>
       
       <summary>
       API Response
       </summary>
       
       ```
       (admin) 🐱 > quota configureemail enable=true accountid=0ee4c6d5-f3b0-479b-8a16-40a57f22525f templatename=QUOTA_EMPTY
       {
         "quotaconfigureemail": {
           "account": "0ee4c6d5-f3b0-479b-8a16-40a57f22525f",
           "enabled": true,
           "templatename": "QUOTA_EMPTY"
         }
       }
       
       (admin) 🐱 > quota configureemail enable=true accountid=0ee4c6d5-f3b0-479b-8a16-40a57f22525f templatename=QUOTA_LOW
       {
         "quotaconfigureemail": {
           "account": "0ee4c6d5-f3b0-479b-8a16-40a57f22525f",
           "enabled": true,
           "templatename": "QUOTA_LOW"
         }
       }
       
       (admin) 🐱 > quota configureemail enable=true accountid=0ee4c6d5-f3b0-479b-8a16-40a57f22525f templatename=QUOTA_STATEMENT
       {
         "quotaconfigureemail": {
           "account": "0ee4c6d5-f3b0-479b-8a16-40a57f22525f",
           "enabled": true,
           "templatename": "QUOTA_STATEMENT"
         }
       }
       
       (admin) 🐱 > quota configureemail enable=true accountid=0ee4c6d5-f3b0-479b-8a16-40a57f22525f templatename=QUOTA_UNLOCK_ACCOUNT
       {
         "quotaconfigureemail": {
           "account": "0ee4c6d5-f3b0-479b-8a16-40a57f22525f",
           "enabled": true,
           "templatename": "QUOTA_UNLOCK_ACCOUNT"
         }
       }
       ```
       
       </details>
   
   3.  Using `quotaListEmailConfiguration`, I verified that the e-mails had been enabled for the account.
   
       <details>
       
       <summary>
       API Response
       </summary>
       
       ```
       (admin) 🐱 > quota listemailconfiguration accountid=0ee4c6d5-f3b0-479b-8a16-40a57f22525f 
       {
         "count": 4,
         "quotaconfigureemail": [
           {
             "account": "0ee4c6d5-f3b0-479b-8a16-40a57f22525f",
             "enabled": true,
             "minbalance": 10,
             "templatename": "QUOTA_LOW"
           },
           {
             "account": "0ee4c6d5-f3b0-479b-8a16-40a57f22525f",
             "enabled": true,
             "minbalance": 10,
             "templatename": "QUOTA_EMPTY"
           },
           {
             "account": "0ee4c6d5-f3b0-479b-8a16-40a57f22525f",
             "enabled": true,
             "minbalance": 10,
             "templatename": "QUOTA_UNLOCK_ACCOUNT"
           },
           {
             "account": "0ee4c6d5-f3b0-479b-8a16-40a57f22525f",
             "enabled": true,
             "minbalance": 10,
             "templatename": "QUOTA_STATEMENT"
           }
         ]
       }
       ```
       
       </details>
   
   4. I executed `quotaUpdate` and verified that I received `QUOTA_LOW` and `QUOTA_STATEMENT` e-mails.
   
   5. I emptied the account's credits (by adding -9 credits to it) and executed `quotaUpdate`. I verified that I received `QUOTA_EMPTY` and `QUOTA_STATEMENT` e-mails.
   
   6. I disabled the `QUOTA_EMPTY` e-mail through the API `quotaConfigureEmail`.
   
       <details>
       
       <summary>
       API Response
       </summary>
       
       ```
       (admin) 🐱 > quota configureemail enable=false accountid=0ee4c6d5-f3b0-479b-8a16-40a57f22525f templatename=QUOTA_EMPTY
       {
         "quotaconfigureemail": {
           "account": "0ee4c6d5-f3b0-479b-8a16-40a57f22525f",
           "enabled": false,
           "templatename": "QUOTA_EMPTY"
         }
       }
       ```
       
       </details>
   
   7. Using `quotaListEmailConfiguration`, I verified that `QUOTA_EMPTY` had been disabled.
   
       <details>
       
       <summary>
       API Response
       </summary>
       
       ```
       (admin) 🐱 > quota listemailconfiguration accountid=0ee4c6d5-f3b0-479b-8a16-40a57f22525f 
       {
         "count": 4,
         "quotaconfigureemail": [
           {
             "account": "0ee4c6d5-f3b0-479b-8a16-40a57f22525f",
             "enabled": true,
             "minbalance": 10,
             "templatename": "QUOTA_LOW"
           },
           {
             "account": "0ee4c6d5-f3b0-479b-8a16-40a57f22525f",
             "enabled": false,
             "minbalance": 10,
             "templatename": "QUOTA_EMPTY"
           },
           {
             "account": "0ee4c6d5-f3b0-479b-8a16-40a57f22525f",
             "enabled": true,
             "minbalance": 10,
             "templatename": "QUOTA_UNLOCK_ACCOUNT"
           },
           {
             "account": "0ee4c6d5-f3b0-479b-8a16-40a57f22525f",
             "enabled": true,
             "minbalance": 10,
             "templatename": "QUOTA_STATEMENT"
           }
         ]
       }
       ```
       
       </details>
   
   9. I executed `quotaUpdate` and verified that I only received the `QUOTA_STATEMENT` e-mail.
   
   10. I set `quota.enable.emails` to `false`. Then, I executed `quotaUpdate` and verified that I did not receive any e-mails.


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@cloudstack.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Quota email configuration [cloudstack]

Posted by "blueorangutan (via GitHub)" <gi...@apache.org>.
blueorangutan commented on PR #8307:
URL: https://github.com/apache/cloudstack/pull/8307#issuecomment-1998913442

   @shwstppr a [SL] Trillian-Jenkins test job (centos7 mgmt + kvm-centos7) has been kicked to run smoke tests


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@cloudstack.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Quota email configuration [cloudstack]

Posted by "shwstppr (via GitHub)" <gi...@apache.org>.
shwstppr commented on PR #8307:
URL: https://github.com/apache/cloudstack/pull/8307#issuecomment-1998913121

   @blueorangutan test


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@cloudstack.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Quota email configuration [cloudstack]

Posted by "blueorangutan (via GitHub)" <gi...@apache.org>.
blueorangutan commented on PR #8307:
URL: https://github.com/apache/cloudstack/pull/8307#issuecomment-1997540260

   Packaging result [SF]: ✔️ el7 ✔️ el8 ✔️ el9 ✔️ debian ✔️ suse15. SL-JID 8937


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@cloudstack.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org