go/svc/invoice: refactor
We unify calculation logic, move the existing Invoice proto message into
InvoiceData, and create other messages/fields around it to hold
denormalized data.
diff --git a/go/svc/invoice/BUILD.bazel b/go/svc/invoice/BUILD.bazel
index 0f8bd8b..05fcd23 100644
--- a/go/svc/invoice/BUILD.bazel
+++ b/go/svc/invoice/BUILD.bazel
@@ -3,6 +3,7 @@
go_library(
name = "go_default_library",
srcs = [
+ "calc.go",
"main.go",
"model.go",
"render.go",
diff --git a/go/svc/invoice/calc.go b/go/svc/invoice/calc.go
new file mode 100644
index 0000000..5df763f
--- /dev/null
+++ b/go/svc/invoice/calc.go
@@ -0,0 +1,29 @@
+package main
+
+import (
+ "time"
+
+ pb "code.hackerspace.pl/hscloud/proto/invoice"
+)
+
+func calculateInvoiceData(p *pb.Invoice) {
+ p.Unit = p.Data.Unit
+ if p.Unit == "" {
+ p.Unit = "€"
+ }
+
+ p.TotalNet = 0
+ p.Total = 0
+ for _, i := range p.Data.Item {
+ rowTotalNet := uint64(i.UnitPrice * i.Count)
+ rowTotal := uint64(float64(rowTotalNet) * (float64(1) + float64(i.Vat)/100000))
+
+ p.TotalNet += rowTotalNet
+ p.Total += rowTotal
+ i.TotalNet = rowTotalNet
+ i.Total = rowTotal
+ }
+
+ due := int64(time.Hour*24) * p.Data.DaysDue
+ p.DueDate = time.Unix(0, p.Date).Add(time.Duration(due)).UnixNano()
+}
diff --git a/go/svc/invoice/main.go b/go/svc/invoice/main.go
index 33bc125..11733c4 100644
--- a/go/svc/invoice/main.go
+++ b/go/svc/invoice/main.go
@@ -23,33 +23,33 @@
}
func (s *service) CreateInvoice(ctx context.Context, req *pb.CreateInvoiceRequest) (*pb.CreateInvoiceResponse, error) {
- if req.Invoice == nil {
- return nil, status.Error(codes.InvalidArgument, "invoice must be given")
+ if req.InvoiceData == nil {
+ return nil, status.Error(codes.InvalidArgument, "invoice data must be given")
}
- if len(req.Invoice.Item) < 1 {
- return nil, status.Error(codes.InvalidArgument, "invoice must contain at least one item")
+ if len(req.InvoiceData.Item) < 1 {
+ return nil, status.Error(codes.InvalidArgument, "invoice data must contain at least one item")
}
- for i, item := range req.Invoice.Item {
+ for i, item := range req.InvoiceData.Item {
if item.Title == "" {
- return nil, status.Errorf(codes.InvalidArgument, "invoice item %d must have title set", i)
+ return nil, status.Errorf(codes.InvalidArgument, "invoice data item %d must have title set", i)
}
if item.Count == 0 || item.Count > 1000000 {
- return nil, status.Errorf(codes.InvalidArgument, "invoice item %d must have correct count", i)
+ return nil, status.Errorf(codes.InvalidArgument, "invoice data item %d must have correct count", i)
}
if item.UnitPrice == 0 {
- return nil, status.Errorf(codes.InvalidArgument, "invoice item %d must have correct unit price", i)
+ return nil, status.Errorf(codes.InvalidArgument, "invoice data item %d must have correct unit price", i)
}
if item.Vat > 100000 {
- return nil, status.Errorf(codes.InvalidArgument, "invoice item %d must have correct vat set", i)
+ return nil, status.Errorf(codes.InvalidArgument, "invoice data item %d must have correct vat set", i)
}
}
- if len(req.Invoice.CustomerBilling) < 1 {
- return nil, status.Error(codes.InvalidArgument, "invoice must contain at least one line of the customer's billing address")
+ if len(req.InvoiceData.CustomerBilling) < 1 {
+ return nil, status.Error(codes.InvalidArgument, "invoice data must contain at least one line of the customer's billing address")
}
- if len(req.Invoice.InvoicerBilling) < 1 {
- return nil, status.Error(codes.InvalidArgument, "invoice must contain at least one line of the invoicer's billing address")
+ if len(req.InvoiceData.InvoicerBilling) < 1 {
+ return nil, status.Error(codes.InvalidArgument, "invoice data must contain at least one line of the invoicer's billing address")
}
- for i, c := range req.Invoice.InvoicerContact {
+ for i, c := range req.InvoiceData.InvoicerContact {
if c.Medium == "" {
return nil, status.Errorf(codes.InvalidArgument, "contact point %d must have medium set", i)
}
@@ -57,11 +57,11 @@
return nil, status.Errorf(codes.InvalidArgument, "contact point %d must have contact set", i)
}
}
- if req.Invoice.InvoicerVatId == "" {
- return nil, status.Error(codes.InvalidArgument, "invoice must contain invoicer's vat id")
+ if req.InvoiceData.InvoicerVatId == "" {
+ return nil, status.Error(codes.InvalidArgument, "invoice data must contain invoicer's vat id")
}
- uid, err := s.m.createInvoice(ctx, req.Invoice)
+ uid, err := s.m.createInvoice(ctx, req.InvoiceData)
if err != nil {
if _, ok := status.FromError(err); ok {
return nil, err
@@ -83,25 +83,9 @@
glog.Errorf("getInvoice(_, %q): %v", req.Uid, err)
return nil, status.Error(codes.Unavailable, "internal server error")
}
- sealedUid, err := s.m.getSealedUid(ctx, req.Uid)
- if err != nil {
- if _, ok := status.FromError(err); ok {
- return nil, err
- }
- glog.Errorf("getSealedUid(_, %q): %v", req.Uid, err)
- return nil, status.Error(codes.Unavailable, "internal server error")
- }
-
res := &pb.GetInvoiceResponse{
Invoice: invoice,
}
- if sealedUid == "" {
- res.State = pb.GetInvoiceResponse_STATE_PROFORMA
- } else {
- res.State = pb.GetInvoiceResponse_STATE_SEALED
- res.FinalUid = sealedUid
- }
-
return res, nil
}
@@ -131,7 +115,7 @@
return nil, err
}
- rendered, err = renderInvoicePDF(invoice, "xxxx", true)
+ rendered, err = renderInvoicePDF(invoice)
if err != nil {
return nil, err
}
diff --git a/go/svc/invoice/model.go b/go/svc/invoice/model.go
index cb15a16..701807a 100644
--- a/go/svc/invoice/model.go
+++ b/go/svc/invoice/model.go
@@ -5,6 +5,7 @@
"database/sql"
"fmt"
"strconv"
+ "time"
"github.com/golang/glog"
"github.com/golang/protobuf/proto"
@@ -33,12 +34,14 @@
_, err := m.db.Exec(`
create table invoice (
id integer primary key not null,
+ created_time integer not null,
proto blob not null
);
create table invoice_seal (
id integer primary key not null,
invoice_id integer not null,
final_uid text not null unique,
+ sealed_time integer not null,
foreign key (invoice_id) references invoice(id)
);
create table invoice_blob (
@@ -69,14 +72,16 @@
q := `
insert into invoice_seal (
- invoice_id, final_uid
+ invoice_id, final_uid, sealed_time
) values (
?,
- ( select printf("%04d", ifnull( (select final_uid as v from invoice_seal order by final_uid desc limit 1), 19000) + 1 ))
+ ( select printf("%04d", ifnull( (select final_uid as v from invoice_seal order by final_uid desc limit 1), 19000) + 1 )),
+ ?
)
`
- res, err := tx.Exec(q, id)
+ sealTime := time.Now()
+ res, err := tx.Exec(q, id, sealTime.UnixNano())
if err != nil {
return err
}
@@ -95,20 +100,24 @@
return err
}
+ invoice.State = pb.Invoice_STATE_SEALED
+ invoice.FinalUid = fmt.Sprintf("FV/%s", finalUid)
+ invoice.Date = sealTime.UnixNano()
+ calculateInvoiceData(invoice)
+
+ pdfBlob, err := renderInvoicePDF(invoice)
+ if err != nil {
+ return err
+ }
+
q = `
insert into invoice_blob (
invoice_id, pdf
) values (
- ?,
- ?
+ ?, ?
)
`
- pdfBlob, err := renderInvoicePDF(invoice, finalUid, false)
- if err != nil {
- return err
- }
-
if _, err := tx.Exec(q, id, pdfBlob); err != nil {
return err
}
@@ -120,30 +129,30 @@
return nil
}
-func (m *model) createInvoice(ctx context.Context, i *pb.Invoice) (string, error) {
- data, err := proto.Marshal(i)
+func (m *model) createInvoice(ctx context.Context, id *pb.InvoiceData) (string, error) {
+ data, err := proto.Marshal(id)
if err != nil {
return "", err
}
sql := `
insert into invoice (
- proto
+ proto, created_time
) values (
- ?
+ ?, ?
)
`
- res, err := m.db.Exec(sql, data)
+ res, err := m.db.Exec(sql, data, time.Now().UnixNano())
if err != nil {
return "", err
}
- id, err := res.LastInsertId()
+ uid, err := res.LastInsertId()
if err != nil {
return "", err
}
- glog.Infof("%+v", id)
- return fmt.Sprintf("%d", id), nil
+ glog.Infof("%+v", uid)
+ return fmt.Sprintf("%d", uid), nil
}
func (m *model) getRendered(ctx context.Context, uid string) ([]byte, error) {
@@ -187,6 +196,37 @@
return finalUid, nil
}
+type sqlInvoiceSealRow struct {
+ proto []byte
+ createdTime int64
+ sealedTime sql.NullInt64
+ finalUid sql.NullString
+ uid int64
+}
+
+func (s *sqlInvoiceSealRow) Proto() (*pb.Invoice, error) {
+ data := &pb.InvoiceData{}
+ if err := proto.Unmarshal(s.proto, data); err != nil {
+ return nil, err
+ }
+
+ p := &pb.Invoice{
+ Uid: fmt.Sprintf("%d", s.uid),
+ Data: data,
+ }
+ if s.finalUid.Valid {
+ p.State = pb.Invoice_STATE_SEALED
+ p.FinalUid = fmt.Sprintf("FV/%s", s.finalUid.String)
+ p.Date = s.sealedTime.Int64
+ } else {
+ p.State = pb.Invoice_STATE_PROFORMA
+ p.FinalUid = fmt.Sprintf("PROFORMA/%d", s.uid)
+ p.Date = s.createdTime
+ }
+ calculateInvoiceData(p)
+ return p, nil
+}
+
func (m *model) getInvoice(ctx context.Context, uid string) (*pb.Invoice, error) {
id, err := strconv.Atoi(uid)
if err != nil {
@@ -194,78 +234,51 @@
}
q := `
- select invoice.proto from invoice where invoice.id = ?
+ select
+ invoice.id, invoice.proto, invoice.created_time, invoice_seal.sealed_time, invoice_seal.final_uid
+ from invoice
+ left join invoice_seal
+ on invoice_seal.invoice_id = invoice.id
+ where invoice.id = ?
`
res := m.db.QueryRow(q, id)
- data := []byte{}
- if err := res.Scan(&data); err != nil {
+ row := sqlInvoiceSealRow{}
+ if err := res.Scan(&row.uid, &row.proto, &row.createdTime, &row.sealedTime, &row.finalUid); err != nil {
if err == sql.ErrNoRows {
return nil, status.Error(codes.NotFound, "no such invoice")
}
return nil, err
}
- p := &pb.Invoice{}
- if err := proto.Unmarshal(data, p); err != nil {
- return nil, err
- }
- return p, nil
+ return row.Proto()
}
-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) {
+func (m *model) getInvoices(ctx context.Context) ([]*pb.Invoice, error) {
q := `
- select invoice_seal.final_uid, invoice.id, invoice.proto from invoice
+ select
+ invoice.id, invoice.proto, invoice.created_time, invoice_seal.sealed_time, invoice_seal.final_uid
+ 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
+ return nil, err
}
defer rows.Close()
- res := []invoice{}
+ res := []*pb.Invoice{}
+
for rows.Next() {
- i := invoice{
- Proto: &pb.Invoice{},
+ row := sqlInvoiceSealRow{}
+ if err := rows.Scan(&row.uid, &row.proto, &row.createdTime, &row.sealedTime, &row.finalUid); err != nil {
+ return nil, err
}
- buf := []byte{}
-
- number := sql.NullString{}
- if err := rows.Scan(&number, &i.ID, &buf); err != nil {
- return []invoice{}, err
+ p, err := row.Proto()
+ if err != nil {
+ return nil, 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)
+ res = append(res, p)
}
return res, nil
diff --git a/go/svc/invoice/render.go b/go/svc/invoice/render.go
index a433fa8..68e3472 100644
--- a/go/svc/invoice/render.go
+++ b/go/svc/invoice/render.go
@@ -24,9 +24,7 @@
invTmpl = template.Must(template.New("invoice.html").Parse(string(a)))
}
-func renderInvoicePDF(i *pb.Invoice, number string, proforma bool) ([]byte, error) {
- now := time.Now()
-
+func renderInvoicePDF(i *pb.Invoice) ([]byte, error) {
type item struct {
Title string
UnitPrice string
@@ -56,55 +54,45 @@
Total string
DeliveryCharge string
}{
- InvoiceNumber: number,
- Date: now,
- DueDate: now.AddDate(0, 0, int(i.DaysDue)),
- IBAN: i.Iban,
- SWIFT: i.Swift,
- InvoicerVAT: i.InvoicerVatId,
- InvoicerCompanyNumber: i.InvoicerCompanyNumber,
- InvoiceeVAT: i.CustomerVatId,
- Proforma: proforma,
- ReverseVAT: i.ReverseVat,
- USCustomer: i.UsCustomer,
+ InvoiceNumber: i.FinalUid,
+ Date: time.Unix(0, i.Date),
+ DueDate: time.Unix(0, i.DueDate),
+ IBAN: i.Data.Iban,
+ SWIFT: i.Data.Swift,
+ InvoicerVAT: i.Data.InvoicerVatId,
+ InvoicerCompanyNumber: i.Data.InvoicerCompanyNumber,
+ InvoiceeVAT: i.Data.CustomerVatId,
+ Proforma: i.State == pb.Invoice_STATE_PROFORMA,
+ ReverseVAT: i.Data.ReverseVat,
+ USCustomer: i.Data.UsCustomer,
- InvoicerBilling: make([]string, len(i.InvoicerBilling)),
- InvoiceeBilling: make([]string, len(i.CustomerBilling)),
+ InvoicerBilling: make([]string, len(i.Data.InvoicerBilling)),
+ InvoiceeBilling: make([]string, len(i.Data.CustomerBilling)),
}
- unit := i.Unit
- if unit == "" {
- unit = "€"
- }
-
- for d, s := range i.InvoicerBilling {
+ for d, s := range i.Data.InvoicerBilling {
data.InvoicerBilling[d] = s
}
- for d, s := range i.CustomerBilling {
+ for d, s := range i.Data.CustomerBilling {
data.InvoiceeBilling[d] = s
}
- totalNet := 0
- total := 0
- for _, i := range i.Item {
- rowTotalNet := int(i.UnitPrice * i.Count)
- rowTotal := int(float64(rowTotalNet) * (float64(1) + float64(i.Vat)/100000))
+ unit := i.Unit
- totalNet += rowTotalNet
- total += rowTotal
+ for _, it := range i.Data.Item {
data.Items = append(data.Items, item{
- Title: i.Title,
- Qty: fmt.Sprintf("%d", i.Count),
- UnitPrice: fmt.Sprintf(unit+"%.2f", float64(i.UnitPrice)/100),
- VATRate: fmt.Sprintf("%.2f%%", float64(i.Vat)/1000),
- TotalNet: fmt.Sprintf(unit+"%.2f", float64(rowTotalNet)/100),
- Total: fmt.Sprintf(unit+"%.2f", float64(rowTotal)/100),
+ Title: it.Title,
+ Qty: fmt.Sprintf("%d", it.Count),
+ UnitPrice: fmt.Sprintf(unit+"%.2f", float64(it.UnitPrice)/100),
+ VATRate: fmt.Sprintf("%.2f%%", float64(it.Vat)/1000),
+ TotalNet: fmt.Sprintf(unit+"%.2f", float64(it.TotalNet)/100),
+ Total: fmt.Sprintf(unit+"%.2f", float64(it.Total)/100),
})
}
- data.TotalNet = fmt.Sprintf(unit+"%.2f", float64(totalNet)/100)
- data.VATTotal = fmt.Sprintf(unit+"%.2f", float64(total-totalNet)/100)
- data.Total = fmt.Sprintf(unit+"%.2f", float64(total)/100)
+ data.TotalNet = fmt.Sprintf(unit+"%.2f", float64(i.TotalNet)/100)
+ data.VATTotal = fmt.Sprintf(unit+"%.2f", float64(i.Total-i.TotalNet)/100)
+ data.Total = fmt.Sprintf(unit+"%.2f", float64(i.Total)/100)
data.DeliveryCharge = fmt.Sprintf(unit+"%.2f", float64(0))
var b bytes.Buffer
diff --git a/go/svc/invoice/statusz.go b/go/svc/invoice/statusz.go
index dbc59f1..f076299 100644
--- a/go/svc/invoice/statusz.go
+++ b/go/svc/invoice/statusz.go
@@ -7,6 +7,7 @@
"code.hackerspace.pl/hscloud/go/mirko"
"code.hackerspace.pl/hscloud/go/statusz"
+ pb "code.hackerspace.pl/hscloud/proto/invoice"
"github.com/golang/glog"
)
@@ -37,17 +38,17 @@
<th>Actions</th>
</tr>
{{ range .Invoices }}
- {{ if .Sealed }}
+ {{ if eq .State 2 }}
<tr>
{{ else }}
<tr style="opacity: 0.5">
{{ end }}
- <td>{{ .ID }}</td>
- <td>{{ .Number }}</td>
- <td>{{ index .Proto.CustomerBilling 0 }}</td>
+ <td>{{ .Uid }}</td>
+ <td>{{ .FinalUid }}</td>
+ <td>{{ index .Data.CustomerBilling 0 }}</td>
<td>{{ .TotalNetPretty }}</td>
<td>
- <a href="/debug/view?id={{ .ID }}">View</a>
+ <a href="/debug/view?id={{ .Uid }}">View</a>
</td>
</tr>
{{ end }}
@@ -56,7 +57,7 @@
`
type templateInvoice struct {
- invoice
+ *pb.Invoice
TotalNetPretty string
}
@@ -70,8 +71,8 @@
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),
+ Invoice: inv,
+ TotalNetPretty: fmt.Sprintf("%.2f %s", float64(inv.TotalNet)/100, inv.Unit),
}
}