Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions server/app/admin_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -559,7 +559,7 @@ func (a *App) CreateNewAnnouncementHandler(req *http.Request) (interface{}, Resp
for _, user := range users {
subject, body := internal.AdminAnnouncementMailContent(adminAnnouncement.Subject, adminAnnouncement.Body, a.config.Server.Host, user.Name())

err = internal.SendMail(a.config.MailSender.Email, a.config.MailSender.SendGridKey, user.Email, subject, body)
err = a.mailer.SendMail(a.config.MailSender.Email, user.Email, subject, body)
if err != nil {
log.Error().Err(err).Send()
return nil, InternalServerError(errors.New(internalServerErrorMsg))
Expand Down Expand Up @@ -621,7 +621,7 @@ func (a *App) SendEmailHandler(req *http.Request) (interface{}, Response) {

subject, body := internal.AdminMailContent(fmt.Sprintf("Hey! 📢 %s", emailUser.Subject), emailUser.Body, a.config.Server.Host, user.Name())

err = internal.SendMail(a.config.MailSender.Email, a.config.MailSender.SendGridKey, user.Email, subject, body)
err = a.mailer.SendMail(a.config.MailSender.Email, user.Email, subject, body)
if err != nil {
log.Error().Err(err).Send()
return nil, InternalServerError(errors.New(internalServerErrorMsg))
Expand Down Expand Up @@ -699,7 +699,7 @@ func (a *App) notifyAdmins() {
subject, body := internal.NotifyAdminsMailContent(len(pending), a.config.Server.Host)

for _, admin := range admins {
err = internal.SendMail(a.config.MailSender.Email, a.config.MailSender.SendGridKey, admin.Email, subject, body)
err = a.mailer.SendMail(a.config.MailSender.Email, admin.Email, subject, body)
if err != nil {
log.Error().Err(err).Send()
}
Expand All @@ -716,7 +716,7 @@ func (a *App) notifyAdmins() {
subject, body := internal.NotifyAdminsMailLowBalanceContent(balance, a.config.Server.Host)

for _, admin := range admins {
err = internal.SendMail(a.config.MailSender.Email, a.config.MailSender.SendGridKey, admin.Email, subject, body)
err = a.mailer.SendMail(a.config.MailSender.Email, admin.Email, subject, body)
if err != nil {
log.Error().Err(err).Send()
}
Expand Down
3 changes: 3 additions & 0 deletions server/app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ type App struct {
db models.DB
redis streams.RedisClient
deployer c4sDeployer.Deployer
mailer internal.Mailer
}

// NewApp creates new server app all configurations
Expand Down Expand Up @@ -73,6 +74,7 @@ func NewApp(ctx context.Context, configFile string) (app *App, err error) {
db: db,
redis: redis,
deployer: newDeployer,
mailer: internal.NewMailer(config.MailSender.SendGridKey),
}, nil
}

Expand Down Expand Up @@ -156,6 +158,7 @@ func (a *App) registerHandlers() {

invoiceRouter.HandleFunc("", WrapFunc(a.ListInvoicesHandler)).Methods("GET", "OPTIONS")
invoiceRouter.HandleFunc("/{id}", WrapFunc(a.GetInvoiceHandler)).Methods("GET", "OPTIONS")
invoiceRouter.HandleFunc("/download/{id}", WrapFunc(a.DownloadInvoiceHandler)).Methods("GET", "OPTIONS")
invoiceRouter.HandleFunc("/pay/{id}", WrapFunc(a.PayInvoiceHandler)).Methods("PUT", "OPTIONS")

notificationRouter.HandleFunc("", WrapFunc(a.ListNotificationsHandler)).Methods("GET", "OPTIONS")
Expand Down
116 changes: 102 additions & 14 deletions server/app/invoice_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,72 @@ func (a *App) GetInvoiceHandler(req *http.Request) (interface{}, Response) {
}, Ok()
}

// DownloadInvoiceHandler downloads user's invoice by ID
// Example endpoint: Downloads user's invoice by ID
// @Summary Downloads user's invoice by ID
// @Description Downloads user's invoice by ID
// @Tags Invoice
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param id path string true "Invoice ID"
// @Success 200 {object} Response
// @Failure 400 {object} Response
// @Failure 401 {object} Response
// @Failure 404 {object} Response
// @Failure 500 {object} Response
// @Router /invoice/download/{id} [get]
func (a *App) DownloadInvoiceHandler(req *http.Request) (interface{}, Response) {
userID := req.Context().Value(middlewares.UserIDKey("UserID")).(string)

id, err := strconv.Atoi(mux.Vars(req)["id"])
if err != nil {
log.Error().Err(err).Send()
return nil, BadRequest(errors.New("failed to read invoice id"))
}

invoice, err := a.db.GetInvoice(id)
if err == gorm.ErrRecordNotFound {
return nil, NotFound(errors.New("invoice is not found"))
}
if err != nil {
log.Error().Err(err).Send()
return nil, InternalServerError(errors.New(internalServerErrorMsg))
}

// Creating pdf for invoice if it doesn't have it
if len(invoice.FileData) == 0 {
user, err := a.db.GetUserByID(userID)
if err == gorm.ErrRecordNotFound {
return nil, NotFound(errors.New("user is not found"))
}
if err != nil {
log.Error().Err(err).Send()
return nil, InternalServerError(errors.New(internalServerErrorMsg))
}

pdfContent, err := internal.CreateInvoicePDF(invoice, user, a.config.InvoiceLogoPath)
if err != nil {
log.Error().Err(err).Send()
return nil, InternalServerError(errors.New(internalServerErrorMsg))
}

invoice.FileData = pdfContent
if err := a.db.UpdateInvoicePDF(id, invoice.FileData); err != nil {
log.Error().Err(err).Send()
return nil, InternalServerError(errors.New(internalServerErrorMsg))
}
}

if userID != invoice.UserID {
return nil, NotFound(errors.New("invoice is not found"))
}

return invoice.FileData, Ok().
WithHeader("Content-Disposition", fmt.Sprintf("attachment; filename=%s", fmt.Sprintf("invoice-%s-%d.pdf", invoice.UserID, invoice.ID))).
WithHeader("Content-Type", "application/pdf")
}

// PayInvoiceHandler pay user's invoice
// Example endpoint: Pay user's invoice
// @Summary Pay user's invoice
Expand Down Expand Up @@ -213,7 +279,7 @@ func (a *App) monthlyInvoices() {
// Create invoices for all system users
for _, user := range users {
// 1. Create new monthly invoice
if err = a.createInvoice(user.ID.String(), now); err != nil {
if err = a.createInvoice(user, now); err != nil {
log.Error().Err(err).Send()
}

Expand Down Expand Up @@ -275,15 +341,15 @@ func (a *App) monthlyInvoices() {
}
}

func (a *App) createInvoice(userID string, now time.Time) error {
func (a *App) createInvoice(user models.User, now time.Time) error {
monthStart := time.Date(now.Year(), now.Month(), 0, 0, 0, 0, 0, time.Local)

vms, err := a.db.GetAllSuccessfulVms(userID)
vms, err := a.db.GetAllSuccessfulVms(user.ID.String())
if err != nil && err != gorm.ErrRecordNotFound {
return err
}

k8s, err := a.db.GetAllSuccessfulK8s(userID)
k8s, err := a.db.GetAllSuccessfulK8s(user.ID.String())
if err != nil && err != gorm.ErrRecordNotFound {
return err
}
Expand All @@ -308,6 +374,8 @@ func (a *App) createInvoice(userID string, now time.Time) error {
DeploymentResources: vm.Resources,
DeploymentType: "vm",
DeploymentID: vm.ID,
DeploymentName: vm.Name,
DeploymentCreatedAt: vm.CreatedAt,
HasPublicIP: vm.Public,
PeriodInHours: time.Since(usageStart).Hours(),
Cost: cost,
Expand All @@ -333,6 +401,8 @@ func (a *App) createInvoice(userID string, now time.Time) error {
DeploymentResources: cluster.Master.Resources,
DeploymentType: "k8s",
DeploymentID: cluster.ID,
DeploymentName: cluster.Master.Name,
DeploymentCreatedAt: cluster.CreatedAt,
HasPublicIP: cluster.Master.Public,
PeriodInHours: time.Since(usageStart).Hours(),
Cost: cost,
Expand All @@ -342,11 +412,22 @@ func (a *App) createInvoice(userID string, now time.Time) error {
}

if len(items) > 0 {
if err = a.db.CreateInvoice(&models.Invoice{
UserID: userID,
in := models.Invoice{
UserID: user.ID.String(),
Total: total,
Deployments: items,
}); err != nil {
}

// Creating pdf for invoice
pdfContent, err := internal.CreateInvoicePDF(in, user, a.config.InvoiceLogoPath)
if err != nil {
return err
}

in.FileData = pdfContent

// Creating invoice in db
if err = a.db.CreateInvoice(&in); err != nil {
return err
}
}
Expand Down Expand Up @@ -415,14 +496,14 @@ func (a *App) sendInvoiceReminderToUser(userID, userEmail, userName string, now
}

for _, invoice := range invoices {
oneMonthsAgo := now.AddDate(0, -1, 0)
// oneMonthsAgo := now.AddDate(0, -1, 0)
oneWeekAgo := now.AddDate(0, 0, -7)

// check if the invoice created 1 months ago (not after it) and
// last remainder sent for this invoice was 7 days ago and
// last remainder sent for this invoice was before 7 days ago and
// invoice is not paid
if invoice.CreatedAt.Before(oneMonthsAgo) &&
invoice.LastReminderAt.Before(oneWeekAgo) &&
// invoice.CreatedAt.Before(oneMonthsAgo) &&
if invoice.LastReminderAt.Before(oneWeekAgo) &&
!invoice.Paid {
// overdue date starts after one month since invoice creation
overDueStart := invoice.CreatedAt.AddDate(0, 1, 0)
Expand All @@ -434,20 +515,27 @@ func (a *App) sendInvoiceReminderToUser(userID, userEmail, userName string, now

mailBody := "We hope this message finds you well.\n"
mailBody += fmt.Sprintf("Our records show that there is an outstanding invoice for %v %s associated with your account (%d). ", invoice.Total, currencyName, invoice.ID)
mailBody += fmt.Sprintf("As of today, the payment for this invoice is %d days overdue.", overDueDays)
if overDueDays > 0 {
mailBody += fmt.Sprintf("As of today, the payment for this invoice is %d days overdue.", overDueDays)
}
mailBody += "To avoid any interruptions to your services and the potential deletion of your deployments, "
mailBody += fmt.Sprintf("we kindly ask that you make the payment within the next %d days. If the invoice remains unpaid after this period, ", gracePeriod)
mailBody += "please be advised that the associated deployments will be deleted from our system.\n\n"

mailBody += "You can easily pay your invoice by charging balance, activating voucher or using cards.\n\n"
mailBody += "If you have already made the payment or need any assistance, "
mailBody += "please don't hesitate to reach out to us.\n\n"
mailBody += "We appreciate your prompt attention to this matter and thank you fosr being a valued customer."
mailBody += "We appreciate your prompt attention to this matter and thank you for being a valued customer."

subject := "Unpaid Invoice Notification – Action Required"
subject, body := internal.AdminMailContent(subject, mailBody, a.config.Server.Host, userName)

if err = internal.SendMail(a.config.MailSender.Email, a.config.MailSender.SendGridKey, userEmail, subject, body); err != nil {
if err = a.mailer.SendMail(
a.config.MailSender.Email, userEmail, subject, body, internal.Attachment{
FileName: fmt.Sprintf("invoice-%s-%d.pdf", invoice.UserID, invoice.ID),
Data: invoice.FileData,
},
); err != nil {
log.Error().Err(err).Send()
}

Expand Down
2 changes: 2 additions & 0 deletions server/app/payments_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -349,6 +349,8 @@ func (a *App) DeleteCardHandler(req *http.Request) (interface{}, Response) {
return nil, BadRequest(errors.New("you have active deployment and cannot delete the card"))
}

// TODO: deleting vms before the end of the month then deleting all cards case

// Update the default payment method for future payments (if deleted card is the default)
if card.PaymentMethodID == user.StripeDefaultPaymentID {
var newPaymentMethod string
Expand Down
5 changes: 4 additions & 1 deletion server/app/setup.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,9 @@ func SetUp(t testing.TB) *App {
"medium_vm": 20,
"large_vm": 30
},
"stripe_secret": "sk_test"
"stripe_secret": "sk_test",
"voucher_balance": 10,
"invoice_logo": "server/internal/img/logo.png"
}
`, dbPath)

Expand Down Expand Up @@ -105,6 +107,7 @@ func SetUp(t testing.TB) *App {
db: db,
redis: streams.RedisClient{},
deployer: newDeployer,
mailer: internal.NewMailer(configuration.MailSender.SendGridKey),
}

return app
Expand Down
15 changes: 7 additions & 8 deletions server/app/user_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,7 @@ type EmailInput struct {

// ApplyForVoucherInput struct for user to apply for voucher
type ApplyForVoucherInput struct {
Balance uint64 `json:"balance" binding:"required" validate:"min=0"`
Reason string `json:"reason" binding:"required" validate:"nonzero"`
Reason string `json:"reason" binding:"required" validate:"nonzero"`
}

// AddVoucherInput struct for voucher applied by user
Expand Down Expand Up @@ -145,7 +144,7 @@ func (a *App) SignUpHandler(req *http.Request) (interface{}, Response) {
// send verification code if user is not verified or not exist
code := internal.GenerateRandomCode()
subject, body := internal.SignUpMailContent(code, a.config.MailSender.Timeout, fmt.Sprintf("%s %s", signUp.FirstName, signUp.LastName), a.config.Server.Host)
err = internal.SendMail(a.config.MailSender.Email, a.config.MailSender.SendGridKey, signUp.Email, subject, body)
err = a.mailer.SendMail(a.config.MailSender.Email, signUp.Email, subject, body)
if err != nil {
log.Error().Err(err).Send()
return nil, InternalServerError(errors.New(internalServerErrorMsg))
Expand Down Expand Up @@ -245,7 +244,7 @@ func (a *App) VerifySignUpCodeHandler(req *http.Request) (interface{}, Response)
middlewares.UserCreations.WithLabelValues(user.ID.String(), user.Email).Inc()

subject, body := internal.WelcomeMailContent(user.Name(), a.config.Server.Host)
err = internal.SendMail(a.config.MailSender.Email, a.config.MailSender.SendGridKey, user.Email, subject, body)
err = a.mailer.SendMail(a.config.MailSender.Email, user.Email, subject, body)
if err != nil {
log.Error().Err(err).Send()
return nil, InternalServerError(errors.New(internalServerErrorMsg))
Expand Down Expand Up @@ -405,7 +404,7 @@ func (a *App) ForgotPasswordHandler(req *http.Request) (interface{}, Response) {
// send verification code
code := internal.GenerateRandomCode()
subject, body := internal.ResetPasswordMailContent(code, a.config.MailSender.Timeout, user.Name(), a.config.Server.Host)
err = internal.SendMail(a.config.MailSender.Email, a.config.MailSender.SendGridKey, email.Email, subject, body)
err = a.mailer.SendMail(a.config.MailSender.Email, email.Email, subject, body)

if err != nil {
log.Error().Err(err).Send()
Expand Down Expand Up @@ -711,7 +710,7 @@ func (a *App) ApplyForVoucherHandler(req *http.Request) (interface{}, Response)
voucher := models.Voucher{
Voucher: v,
UserID: userID,
Balance: input.Balance,
Balance: a.config.VoucherBalance,
Reason: input.Reason,
}

Expand Down Expand Up @@ -840,7 +839,6 @@ func (a *App) ActivateVoucherHandler(req *http.Request) (interface{}, Response)
// @Failure 500 {object} Response
// @Router /user/charge_balance [put]
func (a *App) ChargeBalance(req *http.Request) (interface{}, Response) {

userID := req.Context().Value(middlewares.UserIDKey("UserID")).(string)

var input ChargeBalance
Expand Down Expand Up @@ -919,6 +917,7 @@ func (a *App) ChargeBalance(req *http.Request) (interface{}, Response) {
// @Failure 500 {object} Response
// @Router /user [delete]
func (a *App) DeleteUserHandler(req *http.Request) (interface{}, Response) {
// TODO: delete customer from stripe
userID := req.Context().Value(middlewares.UserIDKey("UserID")).(string)
user, err := a.db.GetUserByID(userID)
if err == gorm.ErrRecordNotFound {
Expand All @@ -930,7 +929,7 @@ func (a *App) DeleteUserHandler(req *http.Request) (interface{}, Response) {
}

// 1. Create last invoice to pay if there were active deployments
if err := a.createInvoice(userID, time.Now()); err != nil {
if err := a.createInvoice(user, time.Now()); err != nil {
log.Error().Err(err).Send()
return nil, InternalServerError(errors.New(internalServerErrorMsg))
}
Expand Down
2 changes: 0 additions & 2 deletions server/app/user_handler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -870,8 +870,6 @@ func TestApplyForVoucherHandler(t *testing.T) {
assert.NoError(t, err)

voucherBody := []byte(`{
"vms":10,
"public_ips":1,
"reason":"strongReason"
}`)

Expand Down
4 changes: 2 additions & 2 deletions server/app/voucher_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,7 @@ func (a *App) UpdateVoucherHandler(req *http.Request) (interface{}, Response) {
subject, body = internal.RejectedVoucherMailContent(user.Name(), a.config.Server.Host)
}

err = internal.SendMail(a.config.MailSender.Email, a.config.MailSender.SendGridKey, user.Email, subject, body)
err = a.mailer.SendMail(a.config.MailSender.Email, user.Email, subject, body)
if err != nil {
log.Error().Err(err).Send()
return nil, InternalServerError(errors.New(internalServerErrorMsg))
Expand Down Expand Up @@ -228,7 +228,7 @@ func (a *App) ApproveAllVouchersHandler(req *http.Request) (interface{}, Respons
}

subject, body := internal.ApprovedVoucherMailContent(v.Voucher, user.Name(), a.config.Server.Host)
err = internal.SendMail(a.config.MailSender.Email, a.config.MailSender.SendGridKey, user.Email, subject, body)
err = a.mailer.SendMail(a.config.MailSender.Email, user.Email, subject, body)
if err != nil {
log.Error().Err(err).Send()
return nil, InternalServerError(errors.New(internalServerErrorMsg))
Expand Down
6 changes: 5 additions & 1 deletion server/app/wrapper.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,11 @@ func WrapFunc(a Handler) http.HandlerFunc {
status = result.Status()
}

if err := json.NewEncoder(w).Encode(object); err != nil {
if bytes, ok := object.([]byte); ok {
if _, err := w.Write(bytes); err != nil {
log.Error().Err(err).Msg("failed to write return object")
}
} else if err := json.NewEncoder(w).Encode(object); err != nil {
log.Error().Err(err).Msg("failed to encode return object")
}
middlewares.Requests.WithLabelValues(r.Method, r.RequestURI, fmt.Sprint(status)).Inc()
Expand Down
Loading
Loading