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