blob: 84e5622c7f4ab121555e59d5fa5ebe943b85ff21 [file] [log] [blame]
Serge Bazanskifa818da2021-05-06 00:12:53 +02001package 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
17import (
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
37func init() {
38 flag.Set("logtostderr", "true")
39}
40
41var (
42 flagConfiguration string
43 flagService string
44)
45
46func 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}