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/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