diff --git a/bgpwtf/invoice/BUILD.bazel b/bgpwtf/invoice/BUILD.bazel index 900f0b3c..950474e6 100644 --- a/bgpwtf/invoice/BUILD.bazel +++ b/bgpwtf/invoice/BUILD.bazel @@ -1,4 +1,4 @@ -load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library") +load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library", "go_test") go_library( name = "go_default_library", @@ -31,3 +31,9 @@ go_binary( embed = [":go_default_library"], visibility = ["//visibility:public"], ) + +go_test( + name = "go_default_test", + srcs = ["calc_test.go"], + embed = [":go_default_library"], +) diff --git a/bgpwtf/invoice/calc.go b/bgpwtf/invoice/calc.go index 9c411dac..72933d36 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 @@ func calculateInvoiceData(p *pb.Invoice) { 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 } diff --git a/bgpwtf/invoice/calc_test.go b/bgpwtf/invoice/calc_test.go new file mode 100644 index 00000000..e8607c9b --- /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()) + } + } + } + }) + } +} diff --git a/bgpwtf/invoice/proto/BUILD.bazel b/bgpwtf/invoice/proto/BUILD.bazel index 51f85fe7..2eeae644 100644 --- a/bgpwtf/invoice/proto/BUILD.bazel +++ b/bgpwtf/invoice/proto/BUILD.bazel @@ -1,3 +1,4 @@ +load("@rules_proto//proto:defs.bzl", "proto_library") load("@io_bazel_rules_go//go:def.bzl", "go_library") load("@io_bazel_rules_go//proto:def.bzl", "go_proto_library") diff --git a/bgpwtf/invoice/proto/invoice.proto b/bgpwtf/invoice/proto/invoice.proto index ee2b9d8a..e6caae58 100644 --- a/bgpwtf/invoice/proto/invoice.proto +++ b/bgpwtf/invoice/proto/invoice.proto @@ -264,12 +264,12 @@ message Invoice { // If sealed, otherwise 'proforma'. string final_uid = 3; int64 date = 4; - int64 due_date = 5; // Denormalized fields follow. + int64 due_date = 5; uint64 total_net = 6; uint64 total = 7; string unit = 8; - repeated GTUCode gtu_codes = 10; + repeated GTUCode gtu_code = 10; // Next tag: 11; }