Serge Bazanski | fa818da | 2021-05-06 00:12:53 +0200 | [diff] [blame] | 1 | package main |
| 2 | |
| 3 | // recurrent is a tool to bill recurrent monthly invoices. It should be run at |
| 4 | // the beginning of each month against a database of customers stored as a |
| 5 | // prototext. |
| 6 | // |
| 7 | // This is a fairly janky tool, and should be replaced by a proper billing |
| 8 | // service. |
| 9 | // |
| 10 | // $ bazel run //bgpwtf/invoice/recurrent -- \ |
| 11 | // -invoice_configuration=$(pwd)bgpwtf/invoice/customers.pb.text \ |
| 12 | // -invoice_service 10.78.253.10:4200 -hspki_disable |
| 13 | // |
| 14 | // q3k has the sqlite database for the invoice service and the customer |
| 15 | // prototext. |
| 16 | |
| 17 | import ( |
| 18 | "bufio" |
| 19 | "context" |
| 20 | "flag" |
| 21 | "fmt" |
| 22 | "io" |
| 23 | "io/ioutil" |
| 24 | "os" |
| 25 | "strings" |
| 26 | "time" |
| 27 | |
| 28 | "github.com/golang/glog" |
| 29 | "github.com/golang/protobuf/proto" |
| 30 | "google.golang.org/grpc" |
| 31 | |
| 32 | "code.hackerspace.pl/hscloud/go/pki" |
| 33 | |
| 34 | pb "code.hackerspace.pl/hscloud/bgpwtf/invoice/proto" |
| 35 | ) |
| 36 | |
| 37 | func init() { |
| 38 | flag.Set("logtostderr", "true") |
| 39 | } |
| 40 | |
| 41 | var ( |
| 42 | flagConfiguration string |
| 43 | flagService string |
| 44 | ) |
| 45 | |
| 46 | func main() { |
| 47 | flag.StringVar(&flagService, "invoice_service", "127.0.0.1:4200", "Address of invoice service") |
| 48 | flag.StringVar(&flagConfiguration, "invoice_configuration", "customers.pb.text", "Prototext of customer data") |
| 49 | flag.Parse() |
| 50 | |
| 51 | if flagConfiguration == "" { |
| 52 | glog.Exit("-invoice_configuration must be set") |
| 53 | } |
| 54 | cfgBytes, err := ioutil.ReadFile(flagConfiguration) |
| 55 | if err != nil { |
| 56 | glog.Exitf("could not read configuration: %v", err) |
| 57 | } |
| 58 | |
| 59 | var cfg pb.Configuration |
| 60 | if err := proto.UnmarshalText(string(cfgBytes), &cfg); err != nil { |
| 61 | glog.Exitf("UnmarshalText: %v", err) |
| 62 | } |
| 63 | |
| 64 | conn, err := grpc.Dial(flagService, pki.WithClientHSPKI()) |
| 65 | if err != nil { |
| 66 | glog.Exitf("Dial(%q): %v", flagService, err) |
| 67 | return |
| 68 | } |
| 69 | svc := pb.NewInvoicerClient(conn) |
| 70 | ctx := context.Background() |
| 71 | |
| 72 | var created []string |
| 73 | now := time.Now() |
| 74 | for _, sub := range cfg.Subscription { |
| 75 | glog.Infof("Emitting for %q...", sub.Template.CustomerBilling[0]) |
| 76 | |
| 77 | data := sub.Template |
| 78 | if data.Date == 0 { |
| 79 | data.Date = now.UnixNano() |
| 80 | } |
| 81 | |
| 82 | date := time.Unix(0, data.Date) |
| 83 | year := int(date.Year()) |
| 84 | month := int(date.Month()) |
| 85 | switch sub.Cycle { |
| 86 | case pb.Subscription_CYCLE_CURRENT: |
| 87 | case pb.Subscription_CYCLE_PREV: |
| 88 | month -= 1 |
| 89 | if month < 1 { |
| 90 | month = 12 |
| 91 | year -= 1 |
| 92 | } |
| 93 | default: |
| 94 | glog.Exitf("Invalid cycle: %v", sub.Cycle) |
| 95 | } |
| 96 | |
| 97 | for _, item := range data.Item { |
| 98 | item.Title = strings.ReplaceAll(item.Title, "%M", fmt.Sprintf("%02d", month)) |
| 99 | item.Title = strings.ReplaceAll(item.Title, "%Y", fmt.Sprintf("%04d", year)) |
| 100 | } |
| 101 | res, err := svc.CreateInvoice(ctx, &pb.CreateInvoiceRequest{ |
| 102 | InvoiceData: data, |
| 103 | }) |
| 104 | if err != nil { |
| 105 | glog.Exitf("CreateInvoice: %v", err) |
| 106 | } |
| 107 | glog.Infof("Created invoice %q", res.Uid) |
| 108 | created = append(created, res.Uid) |
| 109 | } |
| 110 | |
| 111 | reader := bufio.NewReader(os.Stdin) |
| 112 | fmt.Print("Invoices generated. Seal? [Yn]") |
| 113 | text, err := reader.ReadString('\n') |
| 114 | if err != nil { |
| 115 | glog.Exitf("Response: %v", err) |
| 116 | } |
| 117 | switch strings.TrimSpace(strings.ToLower(text)) { |
| 118 | case "", "y": |
| 119 | default: |
| 120 | glog.Exitf("Aborting.") |
| 121 | } |
| 122 | for _, uid := range created { |
| 123 | glog.Infof("Sealing %q...", uid) |
| 124 | _, err := svc.SealInvoice(ctx, &pb.SealInvoiceRequest{ |
| 125 | Uid: uid, |
| 126 | DateSource: pb.SealInvoiceRequest_DATE_SOURCE_PROFORMA, |
| 127 | Language: "pl", |
| 128 | }) |
| 129 | if err != nil { |
| 130 | glog.Errorf("Sealing %q failed: %v", uid, err) |
| 131 | continue |
| 132 | } |
| 133 | res, err := svc.GetInvoice(ctx, &pb.GetInvoiceRequest{ |
| 134 | Uid: uid, |
| 135 | }) |
| 136 | if err != nil { |
| 137 | glog.Errorf("Retrieving sealed invoice %q failed: %v", uid, err) |
| 138 | continue |
| 139 | } |
| 140 | fuid := res.Invoice.FinalUid |
| 141 | glog.Infof("%q: Final UID: %s", uid, fuid) |
| 142 | stream, err := svc.RenderInvoice(ctx, &pb.RenderInvoiceRequest{ |
| 143 | Uid: uid, |
| 144 | }) |
| 145 | if err != nil { |
| 146 | glog.Errorf("Rendering sealed invoice failed: %v", err) |
| 147 | continue |
| 148 | } |
| 149 | |
| 150 | path := fmt.Sprintf("/tmp/%s.pdf", strings.ReplaceAll(fuid, "/", "")) |
| 151 | glog.Infof("Downloading %s...", path) |
| 152 | f, err := os.Create(path) |
| 153 | if err != nil { |
| 154 | glog.Errorf("Create: %v", err) |
| 155 | continue |
| 156 | } |
| 157 | |
| 158 | for { |
| 159 | block, err := stream.Recv() |
| 160 | if err == io.EOF { |
| 161 | break |
| 162 | } |
| 163 | if err != nil { |
| 164 | glog.Errorf("Recv: %v", err) |
| 165 | break |
| 166 | } |
| 167 | if _, err := f.Write(block.Data); err != nil { |
| 168 | glog.Errorf("Write: %v", err) |
| 169 | break |
| 170 | } |
| 171 | } |
| 172 | f.Close() |
| 173 | } |
| 174 | |
| 175 | } |