bgpwtf/invoice: add recurrent billing tool
Change-Id: Ic3cc03d7b04304ae8c7aa76d8bb889ae8c144838
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()
+ }
+
+}