forked from hswaw/hscloud
bgpwtf/invoice: add recurrent billing tool
Change-Id: Ic3cc03d7b04304ae8c7aa76d8bb889ae8c144838master
parent
605aadbfa4
commit
fa818da7c6
|
@ -4,7 +4,10 @@ load("@io_bazel_rules_go//proto:def.bzl", "go_proto_library")
|
|||
|
||||
proto_library(
|
||||
name = "proto_proto",
|
||||
srcs = ["invoice.proto"],
|
||||
srcs = [
|
||||
"invoice.proto",
|
||||
"recurrent.proto",
|
||||
],
|
||||
visibility = ["//visibility:public"],
|
||||
)
|
||||
|
||||
|
|
|
@ -0,0 +1,50 @@
|
|||
syntax = "proto3";
|
||||
package invoice;
|
||||
option go_package = "code.hackerspace.pl/hscloud/bgpwtf/invoice/proto";
|
||||
|
||||
import "bgpwtf/invoice/proto/invoice.proto";
|
||||
|
||||
// Subscription is the subscription to a service for which we want to generate
|
||||
// monthly (at least for now) invoices.
|
||||
message Subscription {
|
||||
// Template is the data that will be used to emit the invoice. It will be
|
||||
// used verbatim in a CreateInvoice request, apart from the following
|
||||
// changes:
|
||||
// - if 'date' is not set, the current date will be substituted instead
|
||||
// - for every item in the invoice, any %Y and %M value in its title will
|
||||
// be replaced by the year and month of the billing cycle. The billing
|
||||
// cycle is defined in relation to the date in the Cycle enum below..
|
||||
InvoiceData template = 1;
|
||||
|
||||
// Cycle defines the billing cycle policy for this subscription.
|
||||
enum Cycle {
|
||||
CYCLE_INVALID = 0;
|
||||
// The subscription is billed for the month that it is invoiced for.
|
||||
// Eg., if the invoice has a date of April 1st, April 15th or April
|
||||
// 30th, the %M in title will be replaced with 04.
|
||||
//
|
||||
// This is used for subscriptions that are invoiced a month in advance,
|
||||
// with invoices being sent out in the beginning of the month.
|
||||
//
|
||||
// In the future, the meaning of this enum value might change to 'bill
|
||||
// at beginning of month/cycle', but currently we only bill once per
|
||||
// month.
|
||||
CYCLE_CURRENT = 1;
|
||||
// The subscription is billed for the month from when it was invoiced.
|
||||
// Eg., if the invoice has a date of April 1st, April 15th or April
|
||||
// 30th, the %M in the title will be replaced with 03.
|
||||
// This is used for subscriptions that are invoiced right after a month
|
||||
// ends.
|
||||
// In the future, the meaning of this enum value might change to 'bill
|
||||
// at end of month/cycle', but currently we only bill once per month.
|
||||
CYCLE_PREV = 2;
|
||||
}
|
||||
Cycle cycle = 2;
|
||||
}
|
||||
|
||||
// Configuration is a prototext defining subscriptions. Currently it's read
|
||||
// from a file by //bgpwtf/invoice/recurrent. In The future this might be
|
||||
// broken up into a database schema.
|
||||
message Configuration {
|
||||
repeated Subscription subscription = 1;
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library")
|
||||
|
||||
go_library(
|
||||
name = "go_default_library",
|
||||
srcs = ["main.go"],
|
||||
importpath = "code.hackerspace.pl/hscloud/bgpwtf/invoice/recurrent",
|
||||
visibility = ["//visibility:private"],
|
||||
deps = [
|
||||
"//bgpwtf/invoice/proto:go_default_library",
|
||||
"//go/pki:go_default_library",
|
||||
"@com_github_golang_glog//:go_default_library",
|
||||
"@com_github_golang_protobuf//proto:go_default_library",
|
||||
"@org_golang_google_grpc//:go_default_library",
|
||||
],
|
||||
)
|
||||
|
||||
go_binary(
|
||||
name = "recurrent",
|
||||
embed = [":go_default_library"],
|
||||
visibility = ["//visibility:public"],
|
||||
)
|
|
@ -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()
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in New Issue