Skip to content

Commit cb24c71

Browse files
committed
add a pdf generator for invoices
1 parent 26ced02 commit cb24c71

18 files changed

Lines changed: 704 additions & 37 deletions

server/app/admin_handler.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -523,7 +523,7 @@ func (a *App) CreateNewAnnouncementHandler(req *http.Request) (interface{}, Resp
523523
for _, user := range users {
524524
subject, body := internal.AdminAnnouncementMailContent(adminAnnouncement.Subject, adminAnnouncement.Body, a.config.Server.Host, user.Name())
525525

526-
err = internal.SendMail(a.config.MailSender.Email, a.config.MailSender.SendGridKey, user.Email, subject, body)
526+
err = internal.SendMail(a.config.MailSender.Email, a.config.MailSender.SendGridKey, user.Email, subject, body, "")
527527
if err != nil {
528528
log.Error().Err(err).Send()
529529
return nil, InternalServerError(errors.New(internalServerErrorMsg))
@@ -585,7 +585,7 @@ func (a *App) SendEmailHandler(req *http.Request) (interface{}, Response) {
585585

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

588-
err = internal.SendMail(a.config.MailSender.Email, a.config.MailSender.SendGridKey, user.Email, subject, body)
588+
err = internal.SendMail(a.config.MailSender.Email, a.config.MailSender.SendGridKey, user.Email, subject, body, "")
589589
if err != nil {
590590
log.Error().Err(err).Send()
591591
return nil, InternalServerError(errors.New(internalServerErrorMsg))
@@ -663,7 +663,7 @@ func (a *App) notifyAdmins() {
663663
subject, body := internal.NotifyAdminsMailContent(len(pending), a.config.Server.Host)
664664

665665
for _, admin := range admins {
666-
err = internal.SendMail(a.config.MailSender.Email, a.config.MailSender.SendGridKey, admin.Email, subject, body)
666+
err = internal.SendMail(a.config.MailSender.Email, a.config.MailSender.SendGridKey, admin.Email, subject, body, "")
667667
if err != nil {
668668
log.Error().Err(err).Send()
669669
}
@@ -680,7 +680,7 @@ func (a *App) notifyAdmins() {
680680
subject, body := internal.NotifyAdminsMailLowBalanceContent(balance, a.config.Server.Host)
681681

682682
for _, admin := range admins {
683-
err = internal.SendMail(a.config.MailSender.Email, a.config.MailSender.SendGridKey, admin.Email, subject, body)
683+
err = internal.SendMail(a.config.MailSender.Email, a.config.MailSender.SendGridKey, admin.Email, subject, body, "")
684684
if err != nil {
685685
log.Error().Err(err).Send()
686686
}

server/app/app.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,7 @@ func (a *App) registerHandlers() {
156156

157157
invoiceRouter.HandleFunc("", WrapFunc(a.ListInvoicesHandler)).Methods("GET", "OPTIONS")
158158
invoiceRouter.HandleFunc("/{id}", WrapFunc(a.GetInvoiceHandler)).Methods("GET", "OPTIONS")
159+
invoiceRouter.HandleFunc("/download/{id}", WrapFunc(a.DownloadInvoiceHandler)).Methods("GET", "OPTIONS")
159160
invoiceRouter.HandleFunc("/pay/{id}", WrapFunc(a.PayInvoiceHandler)).Methods("PUT", "OPTIONS")
160161

161162
notificationRouter.HandleFunc("", WrapFunc(a.ListNotificationsHandler)).Methods("GET", "OPTIONS")

server/app/invoice_handler.go

Lines changed: 90 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import (
55
"errors"
66
"fmt"
77
"net/http"
8+
"os"
9+
"path/filepath"
810
"strconv"
911
"strings"
1012
"time"
@@ -119,6 +121,64 @@ func (a *App) GetInvoiceHandler(req *http.Request) (interface{}, Response) {
119121
}, Ok()
120122
}
121123

124+
// DownloadInvoiceHandler downloads user's invoice by ID
125+
// Example endpoint: Downloads user's invoice by ID
126+
// @Summary Downloads user's invoice by ID
127+
// @Description Downloads user's invoice by ID
128+
// @Tags Invoice
129+
// @Accept json
130+
// @Produce json
131+
// @Security BearerAuth
132+
// @Param id path string true "Invoice ID"
133+
// @Success 200 {object} Response
134+
// @Failure 400 {object} Response
135+
// @Failure 401 {object} Response
136+
// @Failure 404 {object} Response
137+
// @Failure 500 {object} Response
138+
// @Router /invoice/download/{id} [get]
139+
func (a *App) DownloadInvoiceHandler(req *http.Request) (interface{}, Response) {
140+
userID := req.Context().Value(middlewares.UserIDKey("UserID")).(string)
141+
142+
id, err := strconv.Atoi(mux.Vars(req)["id"])
143+
if err != nil {
144+
log.Error().Err(err).Send()
145+
return nil, BadRequest(errors.New("failed to read invoice id"))
146+
}
147+
148+
invoice, err := a.db.GetInvoice(id)
149+
if err == gorm.ErrRecordNotFound {
150+
return nil, NotFound(errors.New("invoice is not found"))
151+
}
152+
if err != nil {
153+
log.Error().Err(err).Send()
154+
return nil, InternalServerError(errors.New(internalServerErrorMsg))
155+
}
156+
157+
if userID != invoice.UserID {
158+
return nil, NotFound(errors.New("invoice is not found"))
159+
}
160+
161+
// Get downloads dir
162+
homeDir, err := os.UserHomeDir()
163+
if err != nil {
164+
log.Error().Err(err).Send()
165+
return nil, InternalServerError(errors.New(internalServerErrorMsg))
166+
}
167+
168+
downloadsDir := filepath.Join(homeDir, "Downloads")
169+
pdfPath := filepath.Join(downloadsDir, fmt.Sprintf("invoice-%s-%d.pdf", invoice.UserID, invoice.ID))
170+
171+
err = os.WriteFile(pdfPath, invoice.FileData, 0644)
172+
if err != nil {
173+
log.Error().Err(err).Send()
174+
return nil, InternalServerError(errors.New(internalServerErrorMsg))
175+
}
176+
177+
return ResponseMsg{
178+
Message: fmt.Sprintf("Invoice is downloaded successfully at %s", pdfPath),
179+
}, Ok()
180+
}
181+
122182
// PayInvoiceHandler pay user's invoice
123183
// Example endpoint: Pay user's invoice
124184
// @Summary Pay user's invoice
@@ -213,7 +273,7 @@ func (a *App) monthlyInvoices() {
213273
// Create invoices for all system users
214274
for _, user := range users {
215275
// 1. Create new monthly invoice
216-
if err = a.createInvoice(user.ID.String(), now); err != nil {
276+
if err = a.createInvoice(user, now); err != nil {
217277
log.Error().Err(err).Send()
218278
}
219279

@@ -275,15 +335,15 @@ func (a *App) monthlyInvoices() {
275335
}
276336
}
277337

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

281-
vms, err := a.db.GetAllSuccessfulVms(userID)
341+
vms, err := a.db.GetAllSuccessfulVms(user.ID.String())
282342
if err != nil && err != gorm.ErrRecordNotFound {
283343
return err
284344
}
285345

286-
k8s, err := a.db.GetAllSuccessfulK8s(userID)
346+
k8s, err := a.db.GetAllSuccessfulK8s(user.ID.String())
287347
if err != nil && err != gorm.ErrRecordNotFound {
288348
return err
289349
}
@@ -308,6 +368,8 @@ func (a *App) createInvoice(userID string, now time.Time) error {
308368
DeploymentResources: vm.Resources,
309369
DeploymentType: "vm",
310370
DeploymentID: vm.ID,
371+
DeploymentName: vm.Name,
372+
DeploymentCreatedAt: vm.CreatedAt,
311373
HasPublicIP: vm.Public,
312374
PeriodInHours: time.Since(usageStart).Hours(),
313375
Cost: cost,
@@ -333,6 +395,8 @@ func (a *App) createInvoice(userID string, now time.Time) error {
333395
DeploymentResources: cluster.Master.Resources,
334396
DeploymentType: "k8s",
335397
DeploymentID: cluster.ID,
398+
DeploymentName: cluster.Master.Name,
399+
DeploymentCreatedAt: cluster.CreatedAt,
336400
HasPublicIP: cluster.Master.Public,
337401
PeriodInHours: time.Since(usageStart).Hours(),
338402
Cost: cost,
@@ -342,11 +406,22 @@ func (a *App) createInvoice(userID string, now time.Time) error {
342406
}
343407

344408
if len(items) > 0 {
345-
if err = a.db.CreateInvoice(&models.Invoice{
346-
UserID: userID,
409+
in := models.Invoice{
410+
UserID: user.ID.String(),
347411
Total: total,
348412
Deployments: items,
349-
}); err != nil {
413+
}
414+
415+
// Creating pdf for invoice
416+
pdfContent, err := internal.CreateInvoicePDF(in, user)
417+
if err != nil {
418+
return err
419+
}
420+
421+
in.FileData = pdfContent
422+
423+
// Creating invoice in db
424+
if err = a.db.CreateInvoice(&in); err != nil {
350425
return err
351426
}
352427
}
@@ -434,20 +509,25 @@ func (a *App) sendInvoiceReminderToUser(userID, userEmail, userName string, now
434509

435510
mailBody := "We hope this message finds you well.\n"
436511
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)
437-
mailBody += fmt.Sprintf("As of today, the payment for this invoice is %d days overdue.", overDueDays)
512+
if overDueDays > 0 {
513+
mailBody += fmt.Sprintf("As of today, the payment for this invoice is %d days overdue.", overDueDays)
514+
}
438515
mailBody += "To avoid any interruptions to your services and the potential deletion of your deployments, "
439516
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)
440517
mailBody += "please be advised that the associated deployments will be deleted from our system.\n\n"
441518

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

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

450-
if err = internal.SendMail(a.config.MailSender.Email, a.config.MailSender.SendGridKey, userEmail, subject, body); err != nil {
527+
if err = internal.SendMail(
528+
a.config.MailSender.Email, a.config.MailSender.SendGridKey, userEmail, subject, body,
529+
fmt.Sprintf("invoice-%s-%d.pdf", invoice.UserID, invoice.ID), invoice.FileData,
530+
); err != nil {
451531
log.Error().Err(err).Send()
452532
}
453533

server/app/payments_handler.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -349,6 +349,8 @@ func (a *App) DeleteCardHandler(req *http.Request) (interface{}, Response) {
349349
return nil, BadRequest(errors.New("you have active deployment and cannot delete the card"))
350350
}
351351

352+
// TODO: deleting vms before the end of the month then deleting all cards case
353+
352354
// Update the default payment method for future payments (if deleted card is the default)
353355
if card.PaymentMethodID == user.StripeDefaultPaymentID {
354356
var newPaymentMethod string

server/app/user_handler.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,7 @@ func (a *App) SignUpHandler(req *http.Request) (interface{}, Response) {
145145
// send verification code if user is not verified or not exist
146146
code := internal.GenerateRandomCode()
147147
subject, body := internal.SignUpMailContent(code, a.config.MailSender.Timeout, fmt.Sprintf("%s %s", signUp.FirstName, signUp.LastName), a.config.Server.Host)
148-
err = internal.SendMail(a.config.MailSender.Email, a.config.MailSender.SendGridKey, signUp.Email, subject, body)
148+
err = internal.SendMail(a.config.MailSender.Email, a.config.MailSender.SendGridKey, signUp.Email, subject, body, "")
149149
if err != nil {
150150
log.Error().Err(err).Send()
151151
return nil, InternalServerError(errors.New(internalServerErrorMsg))
@@ -245,7 +245,7 @@ func (a *App) VerifySignUpCodeHandler(req *http.Request) (interface{}, Response)
245245
middlewares.UserCreations.WithLabelValues(user.ID.String(), user.Email).Inc()
246246

247247
subject, body := internal.WelcomeMailContent(user.Name(), a.config.Server.Host)
248-
err = internal.SendMail(a.config.MailSender.Email, a.config.MailSender.SendGridKey, user.Email, subject, body)
248+
err = internal.SendMail(a.config.MailSender.Email, a.config.MailSender.SendGridKey, user.Email, subject, body, "")
249249
if err != nil {
250250
log.Error().Err(err).Send()
251251
return nil, InternalServerError(errors.New(internalServerErrorMsg))
@@ -405,7 +405,7 @@ func (a *App) ForgotPasswordHandler(req *http.Request) (interface{}, Response) {
405405
// send verification code
406406
code := internal.GenerateRandomCode()
407407
subject, body := internal.ResetPasswordMailContent(code, a.config.MailSender.Timeout, user.Name(), a.config.Server.Host)
408-
err = internal.SendMail(a.config.MailSender.Email, a.config.MailSender.SendGridKey, email.Email, subject, body)
408+
err = internal.SendMail(a.config.MailSender.Email, a.config.MailSender.SendGridKey, email.Email, subject, body, "")
409409

410410
if err != nil {
411411
log.Error().Err(err).Send()
@@ -840,7 +840,6 @@ func (a *App) ActivateVoucherHandler(req *http.Request) (interface{}, Response)
840840
// @Failure 500 {object} Response
841841
// @Router /user/charge_balance [put]
842842
func (a *App) ChargeBalance(req *http.Request) (interface{}, Response) {
843-
844843
userID := req.Context().Value(middlewares.UserIDKey("UserID")).(string)
845844

846845
var input ChargeBalance
@@ -919,6 +918,7 @@ func (a *App) ChargeBalance(req *http.Request) (interface{}, Response) {
919918
// @Failure 500 {object} Response
920919
// @Router /user [delete]
921920
func (a *App) DeleteUserHandler(req *http.Request) (interface{}, Response) {
921+
// TODO: delete customer from stripe
922922
userID := req.Context().Value(middlewares.UserIDKey("UserID")).(string)
923923
user, err := a.db.GetUserByID(userID)
924924
if err == gorm.ErrRecordNotFound {
@@ -930,7 +930,7 @@ func (a *App) DeleteUserHandler(req *http.Request) (interface{}, Response) {
930930
}
931931

932932
// 1. Create last invoice to pay if there were active deployments
933-
if err := a.createInvoice(userID, time.Now()); err != nil {
933+
if err := a.createInvoice(user, time.Now()); err != nil {
934934
log.Error().Err(err).Send()
935935
return nil, InternalServerError(errors.New(internalServerErrorMsg))
936936
}

server/app/voucher_handler.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -176,7 +176,7 @@ func (a *App) UpdateVoucherHandler(req *http.Request) (interface{}, Response) {
176176
subject, body = internal.RejectedVoucherMailContent(user.Name(), a.config.Server.Host)
177177
}
178178

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

230230
subject, body := internal.ApprovedVoucherMailContent(v.Voucher, user.Name(), a.config.Server.Host)
231-
err = internal.SendMail(a.config.MailSender.Email, a.config.MailSender.SendGridKey, user.Email, subject, body)
231+
err = internal.SendMail(a.config.MailSender.Email, a.config.MailSender.SendGridKey, user.Email, subject, body, "")
232232
if err != nil {
233233
log.Error().Err(err).Send()
234234
return nil, InternalServerError(errors.New(internalServerErrorMsg))

server/docs/docs.go

Lines changed: 64 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -387,6 +387,57 @@ const docTemplate = `{
387387
}
388388
}
389389
},
390+
"/invoice/download/{id}": {
391+
"get": {
392+
"security": [
393+
{
394+
"BearerAuth": []
395+
}
396+
],
397+
"description": "Downloads user's invoice by ID",
398+
"consumes": [
399+
"application/json"
400+
],
401+
"produces": [
402+
"application/json"
403+
],
404+
"tags": [
405+
"Invoice"
406+
],
407+
"summary": "Downloads user's invoice by ID",
408+
"parameters": [
409+
{
410+
"type": "string",
411+
"description": "Invoice ID",
412+
"name": "id",
413+
"in": "path",
414+
"required": true
415+
}
416+
],
417+
"responses": {
418+
"200": {
419+
"description": "OK",
420+
"schema": {}
421+
},
422+
"400": {
423+
"description": "Bad Request",
424+
"schema": {}
425+
},
426+
"401": {
427+
"description": "Unauthorized",
428+
"schema": {}
429+
},
430+
"404": {
431+
"description": "Not Found",
432+
"schema": {}
433+
},
434+
"500": {
435+
"description": "Internal Server Error",
436+
"schema": {}
437+
}
438+
}
439+
}
440+
},
390441
"/invoice/pay/{id}": {
391442
"put": {
392443
"security": [
@@ -3092,9 +3143,15 @@ const docTemplate = `{
30923143
"cost": {
30933144
"type": "number"
30943145
},
3146+
"deployment_created_at": {
3147+
"type": "string"
3148+
},
30953149
"deployment_id": {
30963150
"type": "integer"
30973151
},
3152+
"deployment_name": {
3153+
"type": "string"
3154+
},
30983155
"has_public_ip": {
30993156
"type": "boolean"
31003157
},
@@ -3141,10 +3198,16 @@ const docTemplate = `{
31413198
"$ref": "#/definitions/models.DeploymentItem"
31423199
}
31433200
},
3201+
"file_data": {
3202+
"type": "array",
3203+
"items": {
3204+
"type": "integer"
3205+
}
3206+
},
31443207
"id": {
31453208
"type": "integer"
31463209
},
3147-
"last_remainder_at": {
3210+
"last_reminder_at": {
31483211
"type": "string"
31493212
},
31503213
"paid": {

0 commit comments

Comments
 (0)