invoice: calculate GTU codes for invoice, implement some tests

Also drive-by fix two proto issues:
 - rename gtu_codes to gtu_code (following convention)
 - move denormalized Item.due_date field past denormalized comment.

Change-Id: Ibfe0a21aadc0a5d4e2f784b182e530b9603aae62
diff --git a/bgpwtf/invoice/calc.go b/bgpwtf/invoice/calc.go
index 9c411da..72933d3 100644
--- a/bgpwtf/invoice/calc.go
+++ b/bgpwtf/invoice/calc.go
@@ -1,17 +1,23 @@
 package main
 
 import (
+	"sort"
 	"time"
 
 	pb "code.hackerspace.pl/hscloud/bgpwtf/invoice/proto"
 )
 
+// calculateInvoiceData applies all business logic to populate an Invoice's
+// denormalized fields from its InvoiceData.
 func calculateInvoiceData(p *pb.Invoice) {
+	// Populate default unit.
+	// TODO(q3k): this really should be done on invoice submit instead.
 	p.Unit = p.Data.Unit
 	if p.Unit == "" {
 		p.Unit = "€"
 	}
 
+	// Calculate totals.
 	p.TotalNet = 0
 	p.Total = 0
 	for _, i := range p.Data.Item {
@@ -24,6 +30,21 @@
 		i.Total = rowTotal
 	}
 
+	// Calculate due date.
 	due := int64(time.Hour*24) * p.Data.DaysDue
 	p.DueDate = time.Unix(0, p.Date).Add(time.Duration(due)).UnixNano()
+
+	// Denormalize Items' GTUCodes into the Invoice's summary GTU codes.
+	codeSet := make(map[pb.GTUCode]bool)
+	for _, item := range p.Data.Item {
+		for _, code := range item.GtuCode {
+			codeSet[code] = true
+		}
+	}
+	var codes []pb.GTUCode
+	for c, _ := range codeSet {
+		codes = append(codes, c)
+	}
+	sort.Slice(codes, func(i, j int) bool { return codes[i] < codes[j] })
+	p.GtuCode = codes
 }