Skip to content

Commit

Permalink
add a pdf generator for invoices
Browse files Browse the repository at this point in the history
  • Loading branch information
rawdaGastan committed Jan 13, 2025
1 parent 26ced02 commit 70768d9
Show file tree
Hide file tree
Showing 18 changed files with 708 additions and 41 deletions.
8 changes: 4 additions & 4 deletions server/app/admin_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -523,7 +523,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 = internal.SendMail(a.config.MailSender.Email, a.config.MailSender.SendGridKey, user.Email, subject, body, "")
if err != nil {
log.Error().Err(err).Send()
return nil, InternalServerError(errors.New(internalServerErrorMsg))
Expand Down Expand Up @@ -585,7 +585,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 = internal.SendMail(a.config.MailSender.Email, a.config.MailSender.SendGridKey, user.Email, subject, body, "")
if err != nil {
log.Error().Err(err).Send()
return nil, InternalServerError(errors.New(internalServerErrorMsg))
Expand Down Expand Up @@ -663,7 +663,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 = internal.SendMail(a.config.MailSender.Email, a.config.MailSender.SendGridKey, admin.Email, subject, body, "")
if err != nil {
log.Error().Err(err).Send()
}
Expand All @@ -680,7 +680,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 = internal.SendMail(a.config.MailSender.Email, a.config.MailSender.SendGridKey, admin.Email, subject, body, "")
if err != nil {
log.Error().Err(err).Send()
}
Expand Down
1 change: 1 addition & 0 deletions server/app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,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
108 changes: 94 additions & 14 deletions server/app/invoice_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import (
"errors"
"fmt"
"net/http"
"os"
"path/filepath"
"strconv"
"strings"
"time"
Expand Down Expand Up @@ -119,6 +121,64 @@ 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))
}

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

// Get downloads dir
homeDir, err := os.UserHomeDir()
if err != nil {
log.Error().Err(err).Send()
return nil, InternalServerError(errors.New(internalServerErrorMsg))
}

downloadsDir := filepath.Join(homeDir, "Downloads")
pdfPath := filepath.Join(downloadsDir, fmt.Sprintf("invoice-%s-%d.pdf", invoice.UserID, invoice.ID))

err = os.WriteFile(pdfPath, invoice.FileData, 0644)
if err != nil {
log.Error().Err(err).Send()
return nil, InternalServerError(errors.New(internalServerErrorMsg))
}

return ResponseMsg{
Message: fmt.Sprintf("Invoice is downloaded successfully at %s", pdfPath),
}, Ok()
}

// PayInvoiceHandler pay user's invoice
// Example endpoint: Pay user's invoice
// @Summary Pay user's invoice
Expand Down Expand Up @@ -213,7 +273,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 +335,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 +368,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 +395,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 +406,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)
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 +490,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 +509,25 @@ 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 = internal.SendMail(
a.config.MailSender.Email, a.config.MailSender.SendGridKey, userEmail, subject, body,
fmt.Sprintf("invoice-%s-%d.pdf", invoice.UserID, invoice.ID), 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
10 changes: 5 additions & 5 deletions server/app/user_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,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 = internal.SendMail(a.config.MailSender.Email, a.config.MailSender.SendGridKey, signUp.Email, subject, body, "")
if err != nil {
log.Error().Err(err).Send()
return nil, InternalServerError(errors.New(internalServerErrorMsg))
Expand Down Expand Up @@ -245,7 +245,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 = internal.SendMail(a.config.MailSender.Email, a.config.MailSender.SendGridKey, user.Email, subject, body, "")
if err != nil {
log.Error().Err(err).Send()
return nil, InternalServerError(errors.New(internalServerErrorMsg))
Expand Down Expand Up @@ -405,7 +405,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 = internal.SendMail(a.config.MailSender.Email, a.config.MailSender.SendGridKey, email.Email, subject, body, "")

if err != nil {
log.Error().Err(err).Send()
Expand Down Expand Up @@ -840,7 +840,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 +918,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 +930,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
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 = internal.SendMail(a.config.MailSender.Email, a.config.MailSender.SendGridKey, 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 = internal.SendMail(a.config.MailSender.Email, a.config.MailSender.SendGridKey, user.Email, subject, body, "")
if err != nil {
log.Error().Err(err).Send()
return nil, InternalServerError(errors.New(internalServerErrorMsg))
Expand Down
Loading

0 comments on commit 70768d9

Please sign in to comment.