Skip to content

Commit f89cead

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

14 files changed

Lines changed: 669 additions & 18 deletions

File tree

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: 82 additions & 7 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
}

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: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -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/docs/docs.go

Lines changed: 63 additions & 0 deletions
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,6 +3198,12 @@ 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
},

server/docs/swagger.yaml

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -335,8 +335,12 @@ definitions:
335335
properties:
336336
cost:
337337
type: number
338+
deployment_created_at:
339+
type: string
338340
deployment_id:
339341
type: integer
342+
deployment_name:
343+
type: string
340344
has_public_ip:
341345
type: boolean
342346
id:
@@ -365,6 +369,10 @@ definitions:
365369
items:
366370
$ref: '#/definitions/models.DeploymentItem'
367371
type: array
372+
file_data:
373+
items:
374+
type: integer
375+
type: array
368376
id:
369377
type: integer
370378
last_remainder_at:
@@ -915,6 +923,40 @@ paths:
915923
summary: List all invoices
916924
tags:
917925
- Admin
926+
/invoice/download/{id}:
927+
get:
928+
consumes:
929+
- application/json
930+
description: Downloads user's invoice by ID
931+
parameters:
932+
- description: Invoice ID
933+
in: path
934+
name: id
935+
required: true
936+
type: string
937+
produces:
938+
- application/json
939+
responses:
940+
"200":
941+
description: OK
942+
schema: {}
943+
"400":
944+
description: Bad Request
945+
schema: {}
946+
"401":
947+
description: Unauthorized
948+
schema: {}
949+
"404":
950+
description: Not Found
951+
schema: {}
952+
"500":
953+
description: Internal Server Error
954+
schema: {}
955+
security:
956+
- BearerAuth: []
957+
summary: Downloads user's invoice by ID
958+
tags:
959+
- Invoice
918960
/invoice/pay/{id}:
919961
put:
920962
consumes:

server/go.mod

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ require (
1515
github.com/prometheus/client_golang v1.20.5
1616
github.com/rs/zerolog v1.33.0
1717
github.com/sendgrid/sendgrid-go v3.16.0+incompatible
18+
github.com/signintech/gopdf v0.29.0
1819
github.com/spf13/cobra v1.8.1
1920
github.com/stretchr/testify v1.10.0
2021
github.com/stripe/stripe-go/v81 v81.1.1
@@ -71,6 +72,7 @@ require (
7172
github.com/mimoo/StrobeGo v0.0.0-20220103164710-9a04d6ca976b // indirect
7273
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
7374
github.com/onsi/gomega v1.34.2 // indirect
75+
github.com/phpdave11/gofpdi v1.0.14-0.20211212211723-1f10f9844311 // indirect
7476
github.com/pierrec/xxHash v0.1.5 // indirect
7577
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
7678
github.com/prometheus/client_model v0.6.1 // indirect

server/go.sum

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,8 +137,11 @@ github.com/onsi/ginkgo v1.16.4 h1:29JGrr5oVBm5ulCWet69zQkzWipVXIol6ygQUe/EzNc=
137137
github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0=
138138
github.com/onsi/gomega v1.34.2 h1:pNCwDkzrsv7MS9kpaQvVb1aVLahQXyJ/Tv5oAZMI3i8=
139139
github.com/onsi/gomega v1.34.2/go.mod h1:v1xfxRgk0KIsG+QOdm7p8UosrOzPYRo60fd3B/1Dukc=
140+
github.com/phpdave11/gofpdi v1.0.14-0.20211212211723-1f10f9844311 h1:zyWXQ6vu27ETMpYsEMAsisQ+GqJ4e1TPvSNfdOPF0no=
141+
github.com/phpdave11/gofpdi v1.0.14-0.20211212211723-1f10f9844311/go.mod h1:vBmVV0Do6hSBHC8uKUQ71JGW+ZGQq74llk/7bXwjDoI=
140142
github.com/pierrec/xxHash v0.1.5 h1:n/jBpwTHiER4xYvK3/CdPVnLDPchj8eTJFFLUb4QHBo=
141143
github.com/pierrec/xxHash v0.1.5/go.mod h1:w2waW5Zoa/Wc4Yqe0wgrIYAGKqRMf7czn2HNKXmuL+I=
144+
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
142145
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
143146
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
144147
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
@@ -166,6 +169,8 @@ github.com/sendgrid/sendgrid-go v3.16.0+incompatible h1:i8eE6IMkiCy7vusSdacHHSBU
166169
github.com/sendgrid/sendgrid-go v3.16.0+incompatible/go.mod h1:QRQt+LX/NmgVEvmdRw0VT/QgUn499+iza2FnDca9fg8=
167170
github.com/shirou/gopsutil v3.21.11+incompatible h1:+1+c1VGhc88SSonWP6foOcLhvnKlUeu/erjjvaPEYiI=
168171
github.com/shirou/gopsutil v3.21.11+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA=
172+
github.com/signintech/gopdf v0.29.0 h1:ZwnHKvdgBtl1C2DUmbC9a29RCtQTehb11v/Z9w8xb3s=
173+
github.com/signintech/gopdf v0.29.0/go.mod h1:d23eO35GpEliSrF22eJ4bsM3wVeQJTjXTHq5x5qGKjA=
169174
github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM=
170175
github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y=
171176
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
958 KB
Binary file not shown.
701 KB
Binary file not shown.

0 commit comments

Comments
 (0)