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() } }