diff --git a/backend/internal/api/app/app_dependencies.go b/backend/internal/api/app/app_dependencies.go
index d8e87415..bd55ab69 100644
--- a/backend/internal/api/app/app_dependencies.go
+++ b/backend/internal/api/app/app_dependencies.go
@@ -392,27 +392,27 @@ func (app *App) createWorkers() workers.Workers {
invoiceRepo := corepersistence.NewGormInvoiceRepository(app.core.db)
contractsRepo := corepersistence.NewGormUserContractDataRepository(app.core.db)
+ billingService := services.NewBillingService(
+ userRepo, contractsRepo, transferRecordsRepo, clusterRepo,
+ app.infra.graphql, app.infra.gridClient,
+ uint64(app.config.MinimumTFTAmountInWallet), services.Discount(app.config.AppliedDiscount),
+ )
+
workersService := services.NewWorkersService(
app.core.appCtx, userRepo, contractsRepo, invoiceRepo, clusterRepo, transferRecordsRepo,
app.communication.mailService, app.infra.gridClient, app.core.ewfEngine,
app.communication.notificationDispatcher, app.infra.graphql, app.infra.firesquidClient,
- app.config.Invoice, app.config.SystemAccount.Mnemonic,
+ billingService, app.config.Invoice,
app.config.Currency, app.config.ClusterHealthCheckIntervalInHours,
app.config.NodeHealthCheck.ReservedNodeHealthCheckIntervalInHours,
app.config.NodeHealthCheck.ReservedNodeHealthCheckTimeoutInMinutes,
app.config.NodeHealthCheck.ReservedNodeHealthCheckWorkersNum,
app.config.SettleTransferRecordsIntervalInMinutes,
app.config.NotifyAdminsForPendingRecordsInHours,
- app.config.MinimumTFTAmountInWallet, services.Discount(app.config.AppliedDiscount),
+ app.config.MinimumTFTAmountInWallet,
app.config.UsersBalanceCheckIntervalInHours,
app.config.CheckUserDebtIntervalInHours,
)
- billingService := services.NewBillingService(
- userRepo, contractsRepo, transferRecordsRepo, clusterRepo,
- app.infra.graphql, app.infra.gridClient,
- uint64(app.config.MinimumTFTAmountInWallet), services.Discount(app.config.AppliedDiscount),
- )
-
return workers.NewWorkers(app.core.appCtx, workersService, billingService, app.core.metrics, app.core.db)
}
diff --git a/backend/internal/api/handlers/deployment_handler.go b/backend/internal/api/handlers/deployment_handler.go
index 536d6f1c..1757f51f 100644
--- a/backend/internal/api/handlers/deployment_handler.go
+++ b/backend/internal/api/handlers/deployment_handler.go
@@ -224,23 +224,6 @@ func (h *DeploymentHandler) HandleDeployCluster(c *gin.Context) {
return
}
- user, err := h.svc.GetUserByID(config.UserID)
- if err != nil {
- if errors.Is(err, models.ErrUserNotFound) {
- NotFound(c, "User not found")
- return
- }
- reqLog.Error().Err(err).Send()
- InternalServerError(c)
- return
- }
-
- if err := h.billingService.FundUserToFulfillDiscount(c.Request.Context(), &user, nil, cluster.Nodes); err != nil {
- reqLog.Error().Err(err).Send()
- InternalServerError(c)
- return
- }
-
projectName := kubedeployer.GetProjectName(config.UserID, cluster.Name)
logWithProject := reqLog.With().Str("project_name", projectName).Logger()
reqLog = &logWithProject
@@ -428,23 +411,6 @@ func (h *DeploymentHandler) HandleAddNode(c *gin.Context) {
}
}
- user, err := h.svc.GetUserByID(config.UserID)
- if err != nil {
- if errors.Is(err, models.ErrUserNotFound) {
- NotFound(c, "User not found")
- return
- }
- reqLog.Error().Err(err).Send()
- InternalServerError(c)
- return
- }
-
- if err := h.billingService.FundUserToFulfillDiscount(c.Request.Context(), &user, nil, cluster.Nodes); err != nil {
- reqLog.Error().Err(err).Send()
- InternalServerError(c)
- return
- }
-
wfUUID, wfStatus, err := h.svc.AsyncAddNode(config, cl, cluster.Nodes[0])
if err != nil {
reqLog.Error().Err(err).Msg("failed to start add node workflow")
diff --git a/backend/internal/api/handlers/node_handler.go b/backend/internal/api/handlers/node_handler.go
index bb9571eb..8e699f7a 100644
--- a/backend/internal/api/handlers/node_handler.go
+++ b/backend/internal/api/handlers/node_handler.go
@@ -292,12 +292,6 @@ func (h *NodeHandler) ReserveNodeHandler(c *gin.Context) {
return
}
- if err := h.billingService.FundUserToFulfillDiscount(c.Request.Context(), &user, nil, nil); err != nil {
- reqLog.Error().Err(err).Send()
- InternalServerError(c)
- return
- }
-
wfUUID, err := h.svc.AsyncReserveNode(userID, user.Mnemonic, nodeID)
if err != nil {
reqLog.Error().Err(err).Msg("failed to start workflow to reserve node")
diff --git a/backend/internal/core/services/billing_service.go b/backend/internal/core/services/billing_service.go
index 91644fa9..1725db04 100644
--- a/backend/internal/core/services/billing_service.go
+++ b/backend/internal/core/services/billing_service.go
@@ -55,6 +55,10 @@ func NewBillingService(userRepo models.UserRepository, contractsRepo models.Cont
}
}
+func (svc *BillingService) Discount() Discount {
+ return svc.appliedDiscount
+}
+
func (svc *BillingService) SettleUserUsage(user *models.User) error {
usageInUSDMillicent, err := svc.getUserLatestUsageInUSD(user.ID)
if err != nil {
@@ -65,92 +69,70 @@ func (svc *BillingService) SettleUserUsage(user *models.User) error {
}
func (svc *BillingService) AfterUserGetCredit(ctx context.Context, user *models.User) error {
- if err := svc.CreateTransferRecordToChargeUserWithMinTFTAmount(user.ID, user.Username, user.Mnemonic); err != nil {
+ if err := svc.FundUserWithTFTs(ctx, user); err != nil {
return err
}
- if err := svc.SettleUserUsage(user); err != nil {
+ return svc.SettleUserUsage(user)
+}
+
+func (svc *BillingService) FundUserWithTFTs(ctx context.Context, user *models.User) error {
+ userGridAccountTFTBalance, err := svc.gridClient.GetFreeBalanceTFT(user.Mnemonic)
+ if err != nil {
return err
}
- return svc.FundUserToFulfillDiscount(ctx, user, nil, nil)
-}
-
-func (svc *BillingService) CreateTransferRecordToChargeUserWithMinTFTAmount(userID int, username, userMnemonic string) error {
- userTFTBalance, err := svc.gridClient.GetFreeBalanceTFT(userMnemonic)
+ totalPendingTFTAmount, err := svc.transferRecordsRepo.CalculateTotalPendingTFTAmountPerUser(user.ID)
if err != nil {
return err
}
- totalPendingTFTAmount, err := svc.transferRecordsRepo.CalculateTotalPendingTFTAmountPerUser(userID)
+ userBalanceInTFT, err := svc.gridClient.FromUSDMillicentToTFT(user.CreditedBalance + user.CreditCardBalance)
if err != nil {
return err
}
- if userTFTBalance+totalPendingTFTAmount >= zeroTFTBalanceValue {
+ if userGridAccountTFTBalance+totalPendingTFTAmount >= userBalanceInTFT {
return nil
}
return svc.transferRecordsRepo.CreateTransferRecord(&models.TransferRecord{
- UserID: userID,
- Username: username,
- TFTAmount: svc.minimumTFTAmountInWallet * TFTUnitFactor,
+ UserID: user.ID,
+ Username: user.Username,
+ TFTAmount: (userBalanceInTFT - (userGridAccountTFTBalance + totalPendingTFTAmount)) * TFTUnitFactor,
Operation: models.DepositOperation,
})
}
-func (svc *BillingService) FundUserToFulfillDiscount(ctx context.Context, user *models.User, addedRentedNodes []types.Node, addedSharedNodes []kubedeployer.Node) error {
- if user.CreditCardBalance+user.CreditedBalance-user.Debt <= 0 {
- // user has no USD balance, skip
- return nil
- }
-
- // calculate resources usage in USD applying discount
- // I took the cluster nodes since only the new node is in cluster.Nodes
- dailyUsageInUSDMillicent, err := svc.calculateResourcesUsageInUSDApplyingDiscount(ctx, user.ID, user.Mnemonic, addedRentedNodes, addedSharedNodes, svc.appliedDiscount)
- if err != nil {
- return err
- }
-
- dailyUsageInTFT, err := svc.gridClient.FromUSDMillicentToTFT(dailyUsageInUSDMillicent)
- if err != nil {
- return err
- }
-
- totalPendingTFTAmount, err := svc.transferRecordsRepo.CalculateTotalPendingTFTAmountPerUser(user.ID)
+func (svc *BillingService) CreateTransferRecordToChargeUserWithMinTFTAmount(userID int, username, userMnemonic string) error {
+ userTFTBalance, err := svc.gridClient.GetFreeBalanceTFT(userMnemonic)
if err != nil {
return err
}
- userTFTBalance, err := svc.gridClient.GetFreeBalanceTFT(user.Mnemonic)
+ totalPendingTFTAmount, err := svc.transferRecordsRepo.CalculateTotalPendingTFTAmountPerUser(userID)
if err != nil {
return err
}
- // fund user to fulfill discount
- // make sure no old payments will fund more than needed
- if totalPendingTFTAmount+userTFTBalance < dailyUsageInTFT &&
- dailyUsageInTFT > 0 {
- if err := svc.transferRecordsRepo.CreateTransferRecord(&models.TransferRecord{
- UserID: user.ID,
- Username: user.Username,
- TFTAmount: dailyUsageInTFT - userTFTBalance - totalPendingTFTAmount,
- Operation: models.DepositOperation,
- }); err != nil {
- return err
- }
+ if userTFTBalance+totalPendingTFTAmount >= zeroTFTBalanceValue {
+ return nil
}
- return nil
+ return svc.transferRecordsRepo.CreateTransferRecord(&models.TransferRecord{
+ UserID: userID,
+ Username: username,
+ TFTAmount: svc.minimumTFTAmountInWallet * TFTUnitFactor,
+ Operation: models.DepositOperation,
+ })
}
-func (svc *BillingService) calculateResourcesUsageInUSDApplyingDiscount(
+func (svc *BillingService) calculateResourcesUsageInUSD(
ctx context.Context,
userID int,
userMnemonic string,
addedRentedNodes []types.Node,
addedSharedNodes []kubedeployer.Node,
- configuredDiscount Discount,
) (uint64, error) {
calculator, err := svc.gridClient.NewCalculator(userMnemonic)
if err != nil {
@@ -251,8 +233,11 @@ func (svc *BillingService) calculateResourcesUsageInUSDApplyingDiscount(
}
totalResourcesCostMillicent += gridclient.FromUSDToUSDMillicent(float64(len(nameContracts)) * nameContractMonthlyCostInUSD)
+ return totalResourcesCostMillicent, nil
+}
- discount := getDiscountPackage(configuredDiscount).DurationInMonth
+func (svc *BillingService) ApplyDiscountOnUsage(totalResourcesCostMillicent uint64) (uint64, error) {
+ discount := getDiscountPackage(svc.appliedDiscount).DurationInMonth
if discount == 0 {
return totalResourcesCostMillicent, nil
}
diff --git a/backend/internal/core/services/workers_service.go b/backend/internal/core/services/workers_service.go
index 0dd07104..f09dbd18 100644
--- a/backend/internal/core/services/workers_service.go
+++ b/backend/internal/core/services/workers_service.go
@@ -51,9 +51,9 @@ type WorkerService struct {
gridClient gridclient.GridClient
ewfEngine *ewf.Engine
notificationDispatcher *notification.NotificationDispatcher
+ billingService BillingService
// configs
- systemMnemonic string
invoiceCompanyData config.InvoiceCompanyData
currency string
checkUserDebtIntervalInHours int
@@ -64,7 +64,6 @@ type WorkerService struct {
settleTransferRecordsIntervalInMinutes int
notifyAdminsForPendingRecordsInHours int
minimumTFTAmountInWallet int
- appliedDiscount Discount
usersBalanceCheckIntervalInHours int
}
@@ -73,12 +72,12 @@ func NewWorkersService(
invoicesRepo models.InvoiceRepository, clusterRepo models.ClusterRepository, transferRecordsRepo models.TransferRecordRepository,
mailService mailservice.MailService,
gridClient gridclient.GridClient, ewfEngine *ewf.Engine, notificationDispatcher *notification.NotificationDispatcher,
- graphql graphql.GraphQl, firesquidClient graphql.GraphQl,
- invoiceCompanyData config.InvoiceCompanyData, systemMnemonic, currency string,
+ graphql graphql.GraphQl, firesquidClient graphql.GraphQl, billingService BillingService,
+ invoiceCompanyData config.InvoiceCompanyData, currency string,
clusterHealthCheckIntervalInHours, reservedNodeHealthCheckIntervalInHours,
reservedNodeHealthCheckTimeoutInMinutes, reservedNodeHealthCheckWorkersNum,
settleTransferRecordsIntervalInMinutes, notifyAdminsForPendingRecordsInHours,
- minimumTFTAmountInWallet int, appliedDiscount Discount,
+ minimumTFTAmountInWallet,
usersBalanceCheckIntervalInHours,
checkUserDebtIntervalInHours int,
) WorkerService {
@@ -96,8 +95,8 @@ func NewWorkersService(
graphql: graphql,
firesquidClient: firesquidClient,
gridClient: gridClient,
+ billingService: billingService,
- systemMnemonic: systemMnemonic,
invoiceCompanyData: invoiceCompanyData,
currency: currency,
@@ -110,7 +109,6 @@ func NewWorkersService(
notifyAdminsForPendingRecordsInHours: notifyAdminsForPendingRecordsInHours,
minimumTFTAmountInWallet: minimumTFTAmountInWallet,
- appliedDiscount: appliedDiscount,
usersBalanceCheckIntervalInHours: usersBalanceCheckIntervalInHours,
}
}
@@ -168,7 +166,7 @@ func (svc WorkerService) GetUsersBalanceCheckInterval() time.Duration {
return time.Duration(svc.usersBalanceCheckIntervalInHours) * time.Hour
}
-func (svc WorkerService) CreateUserInvoice(BillingService BillingService, user models.User) error {
+func (svc WorkerService) CreateUserInvoice(user models.User) error {
now := time.Now()
timeMonthAgo := now.AddDate(0, -1, 0)
@@ -190,7 +188,7 @@ func (svc WorkerService) CreateUserInvoice(BillingService BillingService, user m
return err
}
- totalAmountBilledInUSDMillicent, err := BillingService.calculateTotalUsageOfReportsInUSDMillicent(billReports.Reports)
+ totalAmountBilledInUSDMillicent, err := svc.billingService.calculateTotalUsageOfReportsInUSDMillicent(billReports.Reports)
if err != nil {
return err
}
@@ -435,7 +433,7 @@ func (svc WorkerService) SettlePendingPayments(records []models.TransferRecord)
var transferFailure string
// getting balance every time to ensure we have the latest balance
- systemTFTBalance, err := svc.gridClient.GetFreeBalanceTFT(svc.systemMnemonic)
+ systemTFTBalance, err := svc.gridClient.GetSystemTFTBalance()
if err != nil {
log.Error().Err(err).Int("record_id", record.ID).Msg("Failed to get system TFT balance for pending record")
continue
@@ -517,7 +515,6 @@ func (svc *WorkerService) ResetUsersTFTsWithNoUSDBalance(users []models.User) er
}
func (svc WorkerService) NotifyAdminWithPendingRecords(records []models.TransferRecord) error {
-
admins, err := svc.userRepo.ListAdmins()
if err != nil {
return err
@@ -534,6 +531,61 @@ func (svc WorkerService) NotifyAdminWithPendingRecords(records []models.Transfer
return nil
}
+func (svc WorkerService) NotifyAdminWithInsufficientBalance() error {
+ currentBalance, err := svc.gridClient.GetSystemTFTBalance()
+ if err != nil {
+ return err
+ }
+
+ users, err := svc.userRepo.ListAllUsers()
+ if err != nil {
+ return err
+ }
+
+ var totalUserDailyUsage uint64
+
+ for _, user := range users {
+ dailyUsageInUSDMillicent, err := svc.billingService.calculateResourcesUsageInUSD(svc.ctx, user.ID, user.Mnemonic, nil, nil)
+ if err != nil {
+ return err
+ }
+
+ dailyUsageInTFT, err := svc.gridClient.FromUSDMillicentToTFT(dailyUsageInUSDMillicent)
+ if err != nil {
+ return err
+ }
+
+ totalUserDailyUsage += dailyUsageInTFT
+ }
+
+ requiredBalance, err := svc.billingService.ApplyDiscountOnUsage(totalUserDailyUsage)
+ if err != nil {
+ return err
+ }
+
+ if requiredBalance <= currentBalance {
+ return nil
+ }
+
+ admins, err := svc.userRepo.ListAdmins()
+ if err != nil {
+ return err
+ }
+
+ for _, admin := range admins {
+ err = svc.mailService.SendInsufficientBalanceNotificationEmail(
+ admin.Email, float64(currentBalance)/TFTUnitFactor,
+ float64(requiredBalance)/TFTUnitFactor, fmt.Sprint(svc.billingService.Discount()),
+ )
+ if err != nil {
+ logger.ForOperation("balance_monitor", "send_admin_mail").Error().Err(err).Msg("Failed to send admin notification email")
+ continue
+ }
+ }
+
+ return nil
+}
+
func (svc WorkerService) AsyncTrackClusterHealth(cluster models.Cluster) error {
wf, err := svc.ewfEngine.NewWorkflow(workflows.WorkflowTrackClusterHealth, ewf.WithDisplayName("Cluster Health Check"))
if err != nil {
diff --git a/backend/internal/core/workers/balance_monitor.go b/backend/internal/core/workers/balance_monitor.go
index 9dab546a..1983b10a 100644
--- a/backend/internal/core/workers/balance_monitor.go
+++ b/backend/internal/core/workers/balance_monitor.go
@@ -11,12 +11,10 @@ func (w Workers) MonitorSystemBalanceAndHandleSettlement() {
adminNotifyTicker := time.NewTicker(w.svc.GetNotifyAdminsForPendingRecordsInterval())
zeroUSDBalanceTicker := time.NewTicker(time.Minute)
zeroTFTBalanceTicker := time.NewTicker(time.Minute)
- fundUserTFTBalanceTicker := time.NewTicker(24 * time.Hour)
defer settleTransfersTicker.Stop()
defer adminNotifyTicker.Stop()
defer zeroUSDBalanceTicker.Stop()
defer zeroTFTBalanceTicker.Stop()
- defer fundUserTFTBalanceTicker.Stop()
for {
select {
@@ -87,19 +85,6 @@ func (w Workers) MonitorSystemBalanceAndHandleSettlement() {
logger.GetLogger().Error().Err(err).Msgf("Failed to create transfer record for user %d", user.ID)
}
}
-
- case <-fundUserTFTBalanceTicker.C:
- users, err := w.svc.ListAllUsers()
- if err != nil {
- logger.GetLogger().Error().Err(err).Msg("Failed to list users")
- continue
- }
-
- for _, user := range users {
- if err := w.billingService.FundUserToFulfillDiscount(w.ctx, &user, nil, nil); err != nil {
- logger.GetLogger().Error().Err(err).Msgf("Failed to fund user %d to claim discount", user.ID)
- }
- }
}
}
}
diff --git a/backend/internal/core/workers/create_invoices.go b/backend/internal/core/workers/create_invoices.go
index fb2988d0..327e9353 100644
--- a/backend/internal/core/workers/create_invoices.go
+++ b/backend/internal/core/workers/create_invoices.go
@@ -34,7 +34,7 @@ func (w Workers) MonthlyInvoicesHandler() {
}
for _, user := range users {
- if err = w.svc.CreateUserInvoice(w.billingService, user); err != nil {
+ if err = w.svc.CreateUserInvoice(user); err != nil {
baseLog.Error().Err(err).Int("user_id", user.ID).Msg("failed to create invoice for user")
}
}
diff --git a/backend/internal/core/workflows/user_activities.go b/backend/internal/core/workflows/user_activities.go
index 82a728f8..f6a0fa55 100644
--- a/backend/internal/core/workflows/user_activities.go
+++ b/backend/internal/core/workflows/user_activities.go
@@ -174,11 +174,15 @@ func SetupTFChainStep(gridClient gridclient.GridClient, userRepo models.UserRepo
return nil
}
- mnemonic, _, err := gridClient.SetupUserOnTFChain(termsAndConditions)
+ mnemonic, twinID, err := gridClient.SetupUserOnTFChain(termsAndConditions)
if err != nil {
return err
}
+ if err := gridClient.BondTwinAccount(twinID); err != nil {
+ return err
+ }
+
if err := userRepo.UpdateUserByID(&models.User{
ID: userConfig.UserID,
Mnemonic: mnemonic,
diff --git a/backend/internal/infrastructure/gridclient/grid_client.go b/backend/internal/infrastructure/gridclient/grid_client.go
index dd737dc2..d3f3b454 100644
--- a/backend/internal/infrastructure/gridclient/grid_client.go
+++ b/backend/internal/infrastructure/gridclient/grid_client.go
@@ -40,6 +40,7 @@ type GridClient interface {
GetTwinIDFromUserMnemonic(mnemonic string) (uint64, error)
GetTwin(mnemonic string) (uint32, error)
GetFreeBalanceTFT(mnemonic string) (uint64, error)
+ GetSystemTFTBalance() (uint64, error)
GetUserAddress(mnemonic string) (string, error)
AcceptTermsAndConditions(mnemonic, docLink, docHash string) error
CreateTwin(mnemonic string) (uint32, error)
@@ -49,6 +50,8 @@ type GridClient interface {
GetPricingPolicy(policyID uint32) (pricingPolicy substrate.PricingPolicy, err error)
GetTFTBillingRateAt(block uint64) (float64, error)
GetCurrentHeight() (uint32, error)
+ BondTwinAccount(twinID uint32) error
+ GetTwinBondedAccount(twinID uint32) (*substrate.AccountID, error)
// node methods
GetNodeClient(nodeID uint32) (*client.NodeClient, error)
@@ -314,6 +317,10 @@ func (s *gridClient) GetFreeBalanceTFT(mnemonic string) (uint64, error) {
return balance.Free.Uint64(), nil
}
+func (s *gridClient) GetSystemTFTBalance() (uint64, error) {
+ return s.GetFreeBalanceTFT(s.systemMnemonic)
+}
+
// GetUserAddress gets user address from user mnemonic
func (s *gridClient) GetUserAddress(mnemonic string) (string, error) {
identity, err := s.getIdentity(mnemonic)
@@ -451,6 +458,19 @@ func (s *gridClient) GetCurrentHeight() (uint32, error) {
return s.gridClient.SubstrateConn.GetCurrentHeight()
}
+func (s *gridClient) BondTwinAccount(twinID uint32) error {
+ stashIdentity, err := s.gridClient.SubstrateConn.NewIdentityFromSr25519Phrase(s.systemMnemonic)
+ if err != nil {
+ return fmt.Errorf("failed to create stash identity: %w", err)
+ }
+
+ return s.gridClient.SubstrateConn.BondTwinAccount(stashIdentity, twinID)
+}
+
+func (s *gridClient) GetTwinBondedAccount(twinID uint32) (*substrate.AccountID, error) {
+ return s.gridClient.SubstrateConn.GetTwinBondedAccount(twinID)
+}
+
func (s *gridClient) Close() {
s.gridClient.Close()
}
diff --git a/backend/internal/infrastructure/mailservice/mail_content_formatter/mail_fomatter.go b/backend/internal/infrastructure/mailservice/mail_content_formatter/mail_fomatter.go
index a66761d6..f1b7c8b4 100644
--- a/backend/internal/infrastructure/mailservice/mail_content_formatter/mail_fomatter.go
+++ b/backend/internal/infrastructure/mailservice/mail_content_formatter/mail_fomatter.go
@@ -9,5 +9,6 @@ type MailContentFormatter interface {
FormatInvoiceMailContent(invoiceTotal float64, currency string, invoiceID int) (string, string)
FormatSystemAnnouncementMailBody(body string) string
FormatNotifyAdminsMailContent(recordsNumber int, systemHost string) (string, string)
+ FormatInsufficientBalanceNotificationMailContent(currentBalance, requiredBalance float64, discount string, systemHost string) (string, string)
FormatNotificationMailContent(notification models.Notification) (string, string, error)
}
diff --git a/backend/internal/infrastructure/mailservice/mail_content_formatter/mail_html_formatter.go b/backend/internal/infrastructure/mailservice/mail_content_formatter/mail_html_formatter.go
index 2eedc65a..01e10187 100644
--- a/backend/internal/infrastructure/mailservice/mail_content_formatter/mail_html_formatter.go
+++ b/backend/internal/infrastructure/mailservice/mail_content_formatter/mail_html_formatter.go
@@ -27,6 +27,9 @@ var notifyPaymentRecordsMail []byte
//go:embed templates/system_announcement.html
var systemAnnouncementMail []byte
+//go:embed templates/insufficient_balance_notification.html
+var insufficientBalanceNotificationMail []byte
+
type MailHTMLFormatter struct {
}
@@ -98,6 +101,18 @@ func (f MailHTMLFormatter) FormatNotifyAdminsMailContent(recordsNumber int, syst
return subject, body
}
+func (f MailHTMLFormatter) FormatInsufficientBalanceNotificationMailContent(currentBalance, requiredBalance float64, discount string, systemHost string) (string, string) {
+ subject := "⚠️ System Balance Insufficient for Discount Requirements"
+ body := string(insufficientBalanceNotificationMail)
+
+ body = strings.ReplaceAll(body, "-balance-", fmt.Sprintf("%.2f", currentBalance))
+ body = strings.ReplaceAll(body, "-required-", fmt.Sprintf("%.2f", requiredBalance))
+ body = strings.ReplaceAll(body, "-discount-", discount)
+ body = strings.ReplaceAll(body, "-host-", systemHost)
+
+ return subject, body
+}
+
func (f MailHTMLFormatter) FormatNotificationMailContent(notification models.Notification) (string, string, error) {
tplName := string(notification.Type)
diff --git a/backend/internal/infrastructure/mailservice/mail_content_formatter/mail_text_formatter.go b/backend/internal/infrastructure/mailservice/mail_content_formatter/mail_text_formatter.go
index fcaaef7b..d910355b 100644
--- a/backend/internal/infrastructure/mailservice/mail_content_formatter/mail_text_formatter.go
+++ b/backend/internal/infrastructure/mailservice/mail_content_formatter/mail_text_formatter.go
@@ -76,6 +76,16 @@ func (f MailTextFormatter) FormatNotifyAdminsMailContent(recordsNumber int, syst
return subject, body
}
+func (f MailTextFormatter) FormatInsufficientBalanceNotificationMailContent(currentBalance, requiredBalance float64, discount string, systemHost string) (string, string) {
+ subject := "Insufficient Balance"
+ body := fmt.Sprintf(
+ "We hope you're well.\n\nYour account balance %f is insufficient for discount %s. Should b e %f. Please add more funds to your account.\n\nVisit %s to add funds.",
+ currentBalance, discount, requiredBalance, systemHost,
+ )
+
+ return subject, body
+}
+
func (f MailTextFormatter) FormatNotificationMailContent(notification models.Notification) (string, string, error) {
subject := notification.Payload["subject"]
if subject == "" {
diff --git a/backend/internal/infrastructure/mailservice/mail_content_formatter/templates/insufficient_balance_notification.html b/backend/internal/infrastructure/mailservice/mail_content_formatter/templates/insufficient_balance_notification.html
new file mode 100644
index 00000000..91289cdc
--- /dev/null
+++ b/backend/internal/infrastructure/mailservice/mail_content_formatter/templates/insufficient_balance_notification.html
@@ -0,0 +1,283 @@
+
+
+
+
+
+
+
+
+
+
+
+ |
+
+ |
+
+
+
+
+
+
+
+ ⚠️ System Balance Alert
+
+ |
+
+
+ |
+
+
+
+
+
+ |
+
+ Hello Admin,
+
+
+ The system account balance is insufficient to fulfill the configured discount requirements.
+
+
+
+
+
+
+
+ | Current System Balance: |
+ $-balance- |
+
+
+ | Required for Discount: |
+ $-required- |
+
+
+ | Configured Discount: |
+ -discount- |
+
+
+ |
+
+
+
+
+ Please top up the system account to ensure users can benefit from the configured discount package.
+
+
+ You can view and manage the system balance from the admin dashboard:
+
+ |
+
+
+ |
+
+ |
+
+
+ |
+
+ Best regards,
+ Mycelium Cloud System
+
+ |
+
+
+ |
+
+
+
+
+
+ |
+
+ This is an automated notification from the Mycelium Cloud
+ monitoring system.
+
+ |
+
+
+ |
+
+
+
+
diff --git a/backend/internal/infrastructure/mailservice/mail_service.go b/backend/internal/infrastructure/mailservice/mail_service.go
index 645a9ac9..c6077d20 100644
--- a/backend/internal/infrastructure/mailservice/mail_service.go
+++ b/backend/internal/infrastructure/mailservice/mail_service.go
@@ -80,6 +80,16 @@ func (m MailService) SendNotifyAdminsEmail(to string, recordsNumber int) error {
})
}
+func (m MailService) SendInsufficientBalanceNotificationEmail(to string, currentBalance, requiredBalance float64, discount string) error {
+ subject, body := m.mailContentFormatter.FormatInsufficientBalanceNotificationMailContent(currentBalance, requiredBalance, discount, m.config.Server.Host)
+ return m.mailSender.Send(mailsender.MailRequest{
+ From: m.config.MailSender.Email,
+ To: to,
+ Subject: subject,
+ Body: body,
+ })
+}
+
func (m MailService) SendEmailNotification(to string, notification models.Notification) error {
subject, body, err := m.mailContentFormatter.FormatNotificationMailContent(notification)
if err != nil {
diff --git a/backend/internal/infrastructure/notification/email_notifier.go b/backend/internal/infrastructure/notification/email_notifier.go
index 0f82bec5..9c8ae9d9 100644
--- a/backend/internal/infrastructure/notification/email_notifier.go
+++ b/backend/internal/infrastructure/notification/email_notifier.go
@@ -5,7 +5,7 @@ import (
"fmt"
"kubecloud/internal/core/models"
"kubecloud/internal/infrastructure/logger"
- mailservice "kubecloud/internal/infrastructure/mailservice"
+ "kubecloud/internal/infrastructure/mailservice"
"github.com/xmonader/ewf"
)