go/svc/invoice: add statusz
diff --git a/go/svc/invoice/BUILD.bazel b/go/svc/invoice/BUILD.bazel
index d654bc9..0f8bd8b 100644
--- a/go/svc/invoice/BUILD.bazel
+++ b/go/svc/invoice/BUILD.bazel
@@ -6,11 +6,13 @@
"main.go",
"model.go",
"render.go",
+ "statusz.go",
],
importpath = "code.hackerspace.pl/hscloud/go/svc/invoice",
visibility = ["//visibility:private"],
deps = [
"//go/mirko:go_default_library",
+ "//go/statusz:go_default_library",
"//go/svc/invoice/templates:go_default_library",
"//proto/invoice:go_default_library",
"@com_github_golang_glog//:go_default_library",
diff --git a/go/svc/invoice/main.go b/go/svc/invoice/main.go
index 8a5ddbe..33bc125 100644
--- a/go/svc/invoice/main.go
+++ b/go/svc/invoice/main.go
@@ -111,45 +111,43 @@
}
}
-func (s *service) RenderInvoice(req *pb.RenderInvoiceRequest, srv pb.Invoicer_RenderInvoiceServer) error {
- sealed, err := s.m.getSealedUid(srv.Context(), req.Uid)
+func (s *service) invoicePDF(ctx context.Context, uid string) ([]byte, error) {
+ sealed, err := s.m.getSealedUid(ctx, uid)
if err != nil {
- if _, ok := status.FromError(err); ok {
- return err
- }
- glog.Errorf("getSealedUid(_, %q): %v", req.Uid, err)
- return status.Error(codes.Unavailable, "internal server error")
+ return nil, err
}
var rendered []byte
if sealed != "" {
// Invoice is sealed, return stored PDF.
- rendered, err = s.m.getRendered(srv.Context(), req.Uid)
+ rendered, err = s.m.getRendered(ctx, uid)
if err != nil {
- if _, ok := status.FromError(err); ok {
- return err
- }
- glog.Errorf("getRendered(_, %q): %v", req.Uid, err)
- return status.Error(codes.Unavailable, "internal server error")
+ return nil, err
}
} else {
// Invoice is proforma, render.
- invoice, err := s.m.getInvoice(srv.Context(), req.Uid)
+ invoice, err := s.m.getInvoice(ctx, uid)
if err != nil {
- if _, ok := status.FromError(err); ok {
- return err
- }
- glog.Errorf("getInvoice(_, %q): %v", req.Uid, err)
- return status.Error(codes.Unavailable, "internal server error")
+ return nil, err
}
- glog.Infof("%+v", invoice)
rendered, err = renderInvoicePDF(invoice, "xxxx", true)
if err != nil {
- glog.Errorf("renderProformaPDF(_): %v", err)
- return status.Error(codes.Unavailable, "internal server error")
+ return nil, err
}
}
+ return rendered, nil
+}
+
+func (s *service) RenderInvoice(req *pb.RenderInvoiceRequest, srv pb.Invoicer_RenderInvoiceServer) error {
+ rendered, err := s.invoicePDF(srv.Context(), req.Uid)
+ if err != nil {
+ if _, ok := status.FromError(err); ok {
+ return err
+ }
+ glog.Errorf("invoicePDF(_, %q): %v", req.Uid, err)
+ return status.Error(codes.Unavailable, "internal server error")
+ }
chunkSize := 16 * 1024
chunk := &pb.RenderInvoiceResponse{}
@@ -201,6 +199,7 @@
}
s := newService(m)
+ s.setupStatusz(mi)
pb.RegisterInvoicerServer(mi.GRPC(), s)
if err := mi.Serve(); err != nil {
diff --git a/go/svc/invoice/model.go b/go/svc/invoice/model.go
index 0ed8245..cb15a16 100644
--- a/go/svc/invoice/model.go
+++ b/go/svc/invoice/model.go
@@ -211,3 +211,62 @@
}
return p, nil
}
+
+type invoice struct {
+ ID int64
+ Number string
+ Sealed bool
+ Proto *pb.Invoice
+ TotalNet int
+ Total int
+}
+
+func (m *model) getInvoices(ctx context.Context) ([]invoice, error) {
+ q := `
+ select invoice_seal.final_uid, invoice.id, invoice.proto from invoice
+ left join invoice_seal
+ on invoice_seal.invoice_id = invoice.id
+ `
+ rows, err := m.db.QueryContext(ctx, q)
+ if err != nil {
+ return []invoice{}, err
+ }
+ defer rows.Close()
+
+ res := []invoice{}
+ for rows.Next() {
+ i := invoice{
+ Proto: &pb.Invoice{},
+ }
+ buf := []byte{}
+
+ number := sql.NullString{}
+ if err := rows.Scan(&number, &i.ID, &buf); err != nil {
+ return []invoice{}, err
+ }
+
+ if err := proto.Unmarshal(buf, i.Proto); err != nil {
+ return []invoice{}, err
+ }
+
+ if number.Valid {
+ i.Sealed = true
+ i.Number = number.String
+ } else {
+ i.Number = "proforma"
+ }
+
+ i.Total = 0
+ i.TotalNet = 0
+ for _, it := range i.Proto.Item {
+ rowTotalNet := int(it.UnitPrice * it.Count)
+ rowTotal := int(float64(rowTotalNet) * (float64(1) + float64(it.Vat)/100000))
+ i.TotalNet += rowTotalNet
+ i.Total += rowTotal
+ }
+
+ res = append(res, i)
+ }
+
+ return res, nil
+}
diff --git a/go/svc/invoice/proto/BUILD.bazel b/go/svc/invoice/proto/BUILD.bazel
index 63c82cc..6c78fa3 100644
--- a/go/svc/invoice/proto/BUILD.bazel
+++ b/go/svc/invoice/proto/BUILD.bazel
@@ -2,15 +2,7 @@
go_library(
name = "go_default_library",
- srcs = [
- "generate.go",
- "inboice.pb.go",
- ],
+ srcs = ["generate.go"],
importpath = "code.hackerspace.pl/hscloud/go/svc/invoice/proto",
visibility = ["//visibility:public"],
- deps = [
- "@com_github_golang_protobuf//proto:go_default_library",
- "@org_golang_google_grpc//:go_default_library",
- "@org_golang_x_net//context:go_default_library",
- ],
)
diff --git a/go/svc/invoice/statusz.go b/go/svc/invoice/statusz.go
new file mode 100644
index 0000000..dbc59f1
--- /dev/null
+++ b/go/svc/invoice/statusz.go
@@ -0,0 +1,93 @@
+package main
+
+import (
+ "context"
+ "fmt"
+ "net/http"
+
+ "code.hackerspace.pl/hscloud/go/mirko"
+ "code.hackerspace.pl/hscloud/go/statusz"
+ "github.com/golang/glog"
+)
+
+const invoicesFragment = `
+ <style type="text/css">
+ .table td,th {
+ background-color: #eee;
+ padding: 0.2em 0.4em 0.2em 0.4em;
+ }
+ .table th {
+ background-color: #c0c0c0;
+ }
+ .table {
+ background-color: #fff;
+ border-spacing: 0.2em;
+ margin-left: auto;
+ margin-right: auto;
+ }
+ </style>
+ <div>
+ {{ .Msg }}
+ <table class="table">
+ <tr>
+ <th>Internal ID</th>
+ <th>Number</th>
+ <th>Customer</th>
+ <th>Amount (net)</th>
+ <th>Actions</th>
+ </tr>
+ {{ range .Invoices }}
+ {{ if .Sealed }}
+ <tr>
+ {{ else }}
+ <tr style="opacity: 0.5">
+ {{ end }}
+ <td>{{ .ID }}</td>
+ <td>{{ .Number }}</td>
+ <td>{{ index .Proto.CustomerBilling 0 }}</td>
+ <td>{{ .TotalNetPretty }}</td>
+ <td>
+ <a href="/debug/view?id={{ .ID }}">View</a>
+ </td>
+ </tr>
+ {{ end }}
+ </table>
+ </div>
+`
+
+type templateInvoice struct {
+ invoice
+ TotalNetPretty string
+}
+
+func (s *service) setupStatusz(m *mirko.Mirko) {
+ statusz.AddStatusPart("Invoices", invoicesFragment, func(ctx context.Context) interface{} {
+ var res struct {
+ Invoices []templateInvoice
+ Msg string
+ }
+ invoices, err := s.m.getInvoices(ctx)
+ res.Invoices = make([]templateInvoice, len(invoices))
+ for i, inv := range invoices {
+ res.Invoices[i] = templateInvoice{
+ invoice: inv,
+ TotalNetPretty: fmt.Sprintf("%.2f %s", float64(inv.TotalNet)/100, inv.Proto.Unit),
+ }
+ }
+
+ if err != nil {
+ glog.Errorf("Could not get invoices for statusz: %v", err)
+ res.Msg = fmt.Sprintf("Could not get invoices: %v", err)
+ }
+ return res
+ })
+
+ m.HTTPMux().HandleFunc("/debug/view", func(w http.ResponseWriter, r *http.Request) {
+ rendered, err := s.invoicePDF(r.Context(), r.URL.Query().Get("id"))
+ if err != nil {
+ fmt.Fprintf(w, "error: %v", err)
+ }
+ w.Header().Set("Content-type", "application/pdf")
+ w.Write(rendered)
+ })
+}