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_test.go b/bgpwtf/invoice/calc_test.go
new file mode 100644
index 0000000..e8607c9
--- /dev/null
+++ b/bgpwtf/invoice/calc_test.go
@@ -0,0 +1,129 @@
+package main
+
+import (
+	"testing"
+	"time"
+
+	pb "code.hackerspace.pl/hscloud/bgpwtf/invoice/proto"
+)
+
+// Fake test data for test in this file.
+var (
+	itemInternet1 = &pb.Item{
+		Title:     "Dostęp do Internetu - Umowa FOOBAR/10 - Opłata Abonentowa 2020/08",
+		Count:     1,
+		UnitPrice: 4200,
+		Vat:       23000,
+	}
+	itemInternet2 = &pb.Item{
+		Title:     "Dostęp do Internetu - Umowa FOOBAR/10 - Opłata Abonentowa 2020/09",
+		Count:     1,
+		UnitPrice: 4200,
+		Vat:       23000,
+	}
+	itemHardware = &pb.Item{
+		Title:     "Thinkpad x230, i7, 16GB RAM, Refurbished",
+		Count:     1,
+		UnitPrice: 10000,
+		Vat:       23000,
+		GtuCode:   []pb.GTUCode{pb.GTUCode_GTU_05},
+	}
+	billing1 = []string{
+		"Wykop Sp. z o. o.",
+		"Zakręt 8",
+		"60-351 Poznań",
+	}
+	billing2 = []string{
+		"TEH Adam Karolczak",
+		"Zgoda 18/2",
+		"95-200 Pabianice",
+	}
+	vatID1 = "PL8086133742"
+	vatID2 = "DE133742429"
+	iban   = "PL 59 1090 2402 9746 7956 2256 2375"
+	swift  = "WLPPZLPAXXX"
+)
+
+func TestCalculate(t *testing.T) {
+	now := time.Now()
+	for _, te := range []struct {
+		description string
+		data        *pb.InvoiceData
+		want        *pb.Invoice
+	}{
+		{
+			description: "Invoice without JPK_V7 codes",
+			data: &pb.InvoiceData{
+				Item:            []*pb.Item{itemInternet1, itemInternet2},
+				InvoicerBilling: billing1,
+				CustomerBilling: billing2,
+				InvoicerVatId:   vatID1,
+				CustomerVatId:   vatID2,
+				Date:            now.UnixNano(),
+				DaysDue:         21,
+				Iban:            iban,
+				Swift:           swift,
+				Unit:            "PLN",
+			},
+			want: &pb.Invoice{
+				TotalNet: 8400,
+				Total:    10332,
+				Unit:     "PLN",
+			},
+		},
+		{
+			description: "Invoice with JPK_V7 codes",
+			data: &pb.InvoiceData{
+				// Repeated item with GTU code GTU_5, to ensure result doesn't
+				// have repeated codes.
+				Item:            []*pb.Item{itemInternet1, itemHardware, itemHardware},
+				InvoicerBilling: billing1,
+				CustomerBilling: billing2,
+				InvoicerVatId:   vatID1,
+				CustomerVatId:   vatID2,
+				Date:            now.UnixNano(),
+				DaysDue:         21,
+				Iban:            iban,
+				Swift:           swift,
+				Unit:            "PLN",
+			},
+			want: &pb.Invoice{
+				TotalNet: 24200,
+				Total:    29766,
+				Unit:     "PLN",
+				GtuCode:  []pb.GTUCode{pb.GTUCode_GTU_05},
+			},
+		},
+	} {
+		t.Run(te.description, func(t *testing.T) {
+			invoice := &pb.Invoice{
+				Data: te.data,
+				Date: te.data.Date,
+			}
+			calculateInvoiceData(invoice)
+			if want, got := te.want.TotalNet, invoice.TotalNet; want != got {
+				t.Errorf("got TotalNet %d, wanted %d", got, want)
+			}
+			if want, got := te.want.Total, invoice.Total; want != got {
+				t.Errorf("got Total %d, wanted %d", got, want)
+			}
+			if want, got := te.want.Unit, invoice.Unit; want != got {
+				t.Errorf("got Unit %q, wanted %q", got, want)
+			}
+			due := time.Duration(int64(time.Hour*24) * te.data.DaysDue)
+			if want, got := now.Add(due).UnixNano(), invoice.DueDate; want != got {
+				t.Errorf("got DueDate %d, wanted %d", got, want)
+			}
+			if want, got := len(te.want.GtuCode), len(invoice.GtuCode); want != got {
+				t.Errorf("got %d GTU codes, wanted %d", got, want)
+			} else {
+				for i, want := range te.want.GtuCode {
+					got := invoice.GtuCode[i]
+					if want != got {
+						t.Errorf("GTU code %d: wanted %s, got %s", i, want.String(), got.String())
+					}
+				}
+			}
+		})
+	}
+}