bgpwtf/invoice: add recurrent billing tool

Change-Id: Ic3cc03d7b04304ae8c7aa76d8bb889ae8c144838
diff --git a/bgpwtf/invoice/proto/BUILD.bazel b/bgpwtf/invoice/proto/BUILD.bazel
index 2eeae64..a1a7033 100644
--- a/bgpwtf/invoice/proto/BUILD.bazel
+++ b/bgpwtf/invoice/proto/BUILD.bazel
@@ -4,7 +4,10 @@
 
 proto_library(
     name = "proto_proto",
-    srcs = ["invoice.proto"],
+    srcs = [
+        "invoice.proto",
+        "recurrent.proto",
+    ],
     visibility = ["//visibility:public"],
 )
 
diff --git a/bgpwtf/invoice/proto/recurrent.proto b/bgpwtf/invoice/proto/recurrent.proto
new file mode 100644
index 0000000..1f4ac66
--- /dev/null
+++ b/bgpwtf/invoice/proto/recurrent.proto
@@ -0,0 +1,50 @@
+syntax = "proto3";
+package invoice;
+option go_package = "code.hackerspace.pl/hscloud/bgpwtf/invoice/proto";
+
+import "bgpwtf/invoice/proto/invoice.proto";
+
+// Subscription is the subscription to a service for which we want to generate
+// monthly (at least for now) invoices.
+message Subscription {
+    // Template is the data that will be used to emit the invoice. It will be
+    // used verbatim in a CreateInvoice request, apart from the following
+    // changes:
+    // - if 'date' is not set, the current date will be substituted instead
+    // - for every item in the invoice, any %Y and %M value in its title will
+    //   be replaced by the year and month of the billing cycle. The billing
+    //   cycle is defined in relation to the date in the Cycle enum below..
+    InvoiceData template = 1;
+
+    // Cycle defines the billing cycle policy for this subscription.
+    enum Cycle {
+        CYCLE_INVALID = 0;
+        // The subscription is billed for the month that it is invoiced for.
+        // Eg., if the invoice has a date of April 1st, April 15th or April
+        // 30th, the %M in title will be replaced with 04.
+        //
+        // This is used for subscriptions that are invoiced a month in advance,
+        // with invoices being sent out in the beginning of the month.
+        //
+        // In the future, the meaning of this enum value might change to 'bill
+        // at beginning of month/cycle', but currently we only bill once per
+        // month.
+        CYCLE_CURRENT = 1;
+        // The subscription is billed for the month from when it was invoiced.
+        // Eg., if the invoice has a date of April 1st, April 15th or April
+        // 30th, the %M in the title will be replaced with 03.
+        // This is used for subscriptions that are invoiced right after a month
+        // ends.
+        // In the future, the meaning of this enum value might change to 'bill
+        // at end of month/cycle', but currently we only bill once per month.
+        CYCLE_PREV = 2;
+    }
+    Cycle cycle = 2;
+}
+
+// Configuration is a prototext defining subscriptions. Currently it's read
+// from a file by //bgpwtf/invoice/recurrent. In The future this might be
+// broken up into a database schema.
+message Configuration {
+    repeated Subscription subscription = 1;
+}
diff --git a/bgpwtf/invoice/recurrent/BUILD.bazel b/bgpwtf/invoice/recurrent/BUILD.bazel
new file mode 100644
index 0000000..b9fc578
--- /dev/null
+++ b/bgpwtf/invoice/recurrent/BUILD.bazel
@@ -0,0 +1,21 @@
+load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library")
+
+go_library(
+    name = "go_default_library",
+    srcs = ["main.go"],
+    importpath = "code.hackerspace.pl/hscloud/bgpwtf/invoice/recurrent",
+    visibility = ["//visibility:private"],
+    deps = [
+        "//bgpwtf/invoice/proto:go_default_library",
+        "//go/pki:go_default_library",
+        "@com_github_golang_glog//:go_default_library",
+        "@com_github_golang_protobuf//proto:go_default_library",
+        "@org_golang_google_grpc//:go_default_library",
+    ],
+)
+
+go_binary(
+    name = "recurrent",
+    embed = [":go_default_library"],
+    visibility = ["//visibility:public"],
+)
diff --git a/bgpwtf/invoice/recurrent/main.go b/bgpwtf/invoice/recurrent/main.go
new file mode 100644
index 0000000..84e5622
--- /dev/null
+++ b/bgpwtf/invoice/recurrent/main.go
@@ -0,0 +1,175 @@
+package main
+
+// recurrent is a tool to bill recurrent monthly invoices. It should be run at
+// the beginning of each month against a database of customers stored as a
+// prototext.
+//
+// This is a fairly janky tool, and should be replaced by a proper billing
+// service.
+//
+//    $ bazel run //bgpwtf/invoice/recurrent -- \
+//        -invoice_configuration=$(pwd)bgpwtf/invoice/customers.pb.text \
+//        -invoice_service 10.78.253.10:4200 -hspki_disable
+//
+// q3k has the sqlite database for the invoice service and the customer
+// prototext.
+
+import (
+	"bufio"
+	"context"
+	"flag"
+	"fmt"
+	"io"
+	"io/ioutil"
+	"os"
+	"strings"
+	"time"
+
+	"github.com/golang/glog"
+	"github.com/golang/protobuf/proto"
+	"google.golang.org/grpc"
+
+	"code.hackerspace.pl/hscloud/go/pki"
+
+	pb "code.hackerspace.pl/hscloud/bgpwtf/invoice/proto"
+)
+
+func init() {
+	flag.Set("logtostderr", "true")
+}
+
+var (
+	flagConfiguration string
+	flagService       string
+)
+
+func main() {
+	flag.StringVar(&flagService, "invoice_service", "127.0.0.1:4200", "Address of invoice service")
+	flag.StringVar(&flagConfiguration, "invoice_configuration", "customers.pb.text", "Prototext of customer data")
+	flag.Parse()
+
+	if flagConfiguration == "" {
+		glog.Exit("-invoice_configuration must be set")
+	}
+	cfgBytes, err := ioutil.ReadFile(flagConfiguration)
+	if err != nil {
+		glog.Exitf("could not read configuration: %v", err)
+	}
+
+	var cfg pb.Configuration
+	if err := proto.UnmarshalText(string(cfgBytes), &cfg); err != nil {
+		glog.Exitf("UnmarshalText: %v", err)
+	}
+
+	conn, err := grpc.Dial(flagService, pki.WithClientHSPKI())
+	if err != nil {
+		glog.Exitf("Dial(%q): %v", flagService, err)
+		return
+	}
+	svc := pb.NewInvoicerClient(conn)
+	ctx := context.Background()
+
+	var created []string
+	now := time.Now()
+	for _, sub := range cfg.Subscription {
+		glog.Infof("Emitting for %q...", sub.Template.CustomerBilling[0])
+
+		data := sub.Template
+		if data.Date == 0 {
+			data.Date = now.UnixNano()
+		}
+
+		date := time.Unix(0, data.Date)
+		year := int(date.Year())
+		month := int(date.Month())
+		switch sub.Cycle {
+		case pb.Subscription_CYCLE_CURRENT:
+		case pb.Subscription_CYCLE_PREV:
+			month -= 1
+			if month < 1 {
+				month = 12
+				year -= 1
+			}
+		default:
+			glog.Exitf("Invalid cycle: %v", sub.Cycle)
+		}
+
+		for _, item := range data.Item {
+			item.Title = strings.ReplaceAll(item.Title, "%M", fmt.Sprintf("%02d", month))
+			item.Title = strings.ReplaceAll(item.Title, "%Y", fmt.Sprintf("%04d", year))
+		}
+		res, err := svc.CreateInvoice(ctx, &pb.CreateInvoiceRequest{
+			InvoiceData: data,
+		})
+		if err != nil {
+			glog.Exitf("CreateInvoice: %v", err)
+		}
+		glog.Infof("Created invoice %q", res.Uid)
+		created = append(created, res.Uid)
+	}
+
+	reader := bufio.NewReader(os.Stdin)
+	fmt.Print("Invoices generated. Seal? [Yn]")
+	text, err := reader.ReadString('\n')
+	if err != nil {
+		glog.Exitf("Response: %v", err)
+	}
+	switch strings.TrimSpace(strings.ToLower(text)) {
+	case "", "y":
+	default:
+		glog.Exitf("Aborting.")
+	}
+	for _, uid := range created {
+		glog.Infof("Sealing %q...", uid)
+		_, err := svc.SealInvoice(ctx, &pb.SealInvoiceRequest{
+			Uid:        uid,
+			DateSource: pb.SealInvoiceRequest_DATE_SOURCE_PROFORMA,
+			Language:   "pl",
+		})
+		if err != nil {
+			glog.Errorf("Sealing %q failed: %v", uid, err)
+			continue
+		}
+		res, err := svc.GetInvoice(ctx, &pb.GetInvoiceRequest{
+			Uid: uid,
+		})
+		if err != nil {
+			glog.Errorf("Retrieving sealed invoice %q failed: %v", uid, err)
+			continue
+		}
+		fuid := res.Invoice.FinalUid
+		glog.Infof("%q: Final UID: %s", uid, fuid)
+		stream, err := svc.RenderInvoice(ctx, &pb.RenderInvoiceRequest{
+			Uid: uid,
+		})
+		if err != nil {
+			glog.Errorf("Rendering sealed invoice failed: %v", err)
+			continue
+		}
+
+		path := fmt.Sprintf("/tmp/%s.pdf", strings.ReplaceAll(fuid, "/", ""))
+		glog.Infof("Downloading %s...", path)
+		f, err := os.Create(path)
+		if err != nil {
+			glog.Errorf("Create: %v", err)
+			continue
+		}
+
+		for {
+			block, err := stream.Recv()
+			if err == io.EOF {
+				break
+			}
+			if err != nil {
+				glog.Errorf("Recv: %v", err)
+				break
+			}
+			if _, err := f.Write(block.Data); err != nil {
+				glog.Errorf("Write: %v", err)
+				break
+			}
+		}
+		f.Close()
+	}
+
+}