go/svc/invoice: import from code.hackerspace.pl/q3k/inboice
diff --git a/go/svc/invoice/main.go b/go/svc/invoice/main.go
new file mode 100644
index 0000000..8a5ddbe
--- /dev/null
+++ b/go/svc/invoice/main.go
@@ -0,0 +1,211 @@
+package main
+
+import (
+	"context"
+	"flag"
+
+	"github.com/golang/glog"
+	"google.golang.org/grpc/codes"
+	"google.golang.org/grpc/status"
+
+	"code.hackerspace.pl/hscloud/go/mirko"
+	pb "code.hackerspace.pl/hscloud/proto/invoice"
+)
+
+var (
+	flagDatabasePath string
+	flagInit         bool
+	flagDisablePKI   bool
+)
+
+type service struct {
+	m *model
+}
+
+func (s *service) CreateInvoice(ctx context.Context, req *pb.CreateInvoiceRequest) (*pb.CreateInvoiceResponse, error) {
+	if req.Invoice == nil {
+		return nil, status.Error(codes.InvalidArgument, "invoice must be given")
+	}
+	if len(req.Invoice.Item) < 1 {
+		return nil, status.Error(codes.InvalidArgument, "invoice must contain at least one item")
+	}
+	for i, item := range req.Invoice.Item {
+		if item.Title == "" {
+			return nil, status.Errorf(codes.InvalidArgument, "invoice item %d must have title set", i)
+		}
+		if item.Count == 0 || item.Count > 1000000 {
+			return nil, status.Errorf(codes.InvalidArgument, "invoice item %d must have correct count", i)
+		}
+		if item.UnitPrice == 0 {
+			return nil, status.Errorf(codes.InvalidArgument, "invoice item %d must have correct unit price", i)
+		}
+		if item.Vat > 100000 {
+			return nil, status.Errorf(codes.InvalidArgument, "invoice item %d must have correct vat set", i)
+		}
+	}
+	if len(req.Invoice.CustomerBilling) < 1 {
+		return nil, status.Error(codes.InvalidArgument, "invoice must contain at least one line of the customer's billing address")
+	}
+	if len(req.Invoice.InvoicerBilling) < 1 {
+		return nil, status.Error(codes.InvalidArgument, "invoice must contain at least one line of the invoicer's billing address")
+	}
+	for i, c := range req.Invoice.InvoicerContact {
+		if c.Medium == "" {
+			return nil, status.Errorf(codes.InvalidArgument, "contact point %d must have medium set", i)
+		}
+		if c.Contact == "" {
+			return nil, status.Errorf(codes.InvalidArgument, "contact point %d must have contact set", i)
+		}
+	}
+	if req.Invoice.InvoicerVatId == "" {
+		return nil, status.Error(codes.InvalidArgument, "invoice must contain invoicer's vat id")
+	}
+
+	uid, err := s.m.createInvoice(ctx, req.Invoice)
+	if err != nil {
+		if _, ok := status.FromError(err); ok {
+			return nil, err
+		}
+		glog.Errorf("createInvoice(_, _): %v", err)
+		return nil, status.Error(codes.Unavailable, "could not create invoice")
+	}
+	return &pb.CreateInvoiceResponse{
+		Uid: uid,
+	}, nil
+}
+
+func (s *service) GetInvoice(ctx context.Context, req *pb.GetInvoiceRequest) (*pb.GetInvoiceResponse, error) {
+	invoice, err := s.m.getInvoice(ctx, req.Uid)
+	if err != nil {
+		if _, ok := status.FromError(err); ok {
+			return nil, err
+		}
+		glog.Errorf("getInvoice(_, %q): %v", req.Uid, err)
+		return nil, status.Error(codes.Unavailable, "internal server error")
+	}
+	sealedUid, err := s.m.getSealedUid(ctx, req.Uid)
+	if err != nil {
+		if _, ok := status.FromError(err); ok {
+			return nil, err
+		}
+		glog.Errorf("getSealedUid(_, %q): %v", req.Uid, err)
+		return nil, status.Error(codes.Unavailable, "internal server error")
+	}
+
+	res := &pb.GetInvoiceResponse{
+		Invoice: invoice,
+	}
+	if sealedUid == "" {
+		res.State = pb.GetInvoiceResponse_STATE_PROFORMA
+	} else {
+		res.State = pb.GetInvoiceResponse_STATE_SEALED
+		res.FinalUid = sealedUid
+	}
+
+	return res, nil
+}
+
+func newService(m *model) *service {
+	return &service{
+		m: m,
+	}
+}
+
+func (s *service) RenderInvoice(req *pb.RenderInvoiceRequest, srv pb.Invoicer_RenderInvoiceServer) error {
+	sealed, err := s.m.getSealedUid(srv.Context(), req.Uid)
+	if err != nil {
+		if _, ok := status.FromError(err); ok {
+			return err
+		}
+		glog.Errorf("getSealedUid(_, %q): %v", req.Uid, err)
+		return status.Error(codes.Unavailable, "internal server error")
+	}
+
+	var rendered []byte
+	if sealed != "" {
+		// Invoice is sealed, return stored PDF.
+		rendered, err = s.m.getRendered(srv.Context(), req.Uid)
+		if err != nil {
+			if _, ok := status.FromError(err); ok {
+				return err
+			}
+			glog.Errorf("getRendered(_, %q): %v", req.Uid, err)
+			return status.Error(codes.Unavailable, "internal server error")
+		}
+	} else {
+		// Invoice is proforma, render.
+		invoice, err := s.m.getInvoice(srv.Context(), req.Uid)
+		if err != nil {
+			if _, ok := status.FromError(err); ok {
+				return err
+			}
+			glog.Errorf("getInvoice(_, %q): %v", req.Uid, err)
+			return status.Error(codes.Unavailable, "internal server error")
+		}
+
+		glog.Infof("%+v", invoice)
+		rendered, err = renderInvoicePDF(invoice, "xxxx", true)
+		if err != nil {
+			glog.Errorf("renderProformaPDF(_): %v", err)
+			return status.Error(codes.Unavailable, "internal server error")
+		}
+	}
+
+	chunkSize := 16 * 1024
+	chunk := &pb.RenderInvoiceResponse{}
+	for i := 0; i < len(rendered); i += chunkSize {
+		if i+chunkSize > len(rendered) {
+			chunk.Data = rendered[i:len(rendered)]
+		} else {
+			chunk.Data = rendered[i : i+chunkSize]
+		}
+		if err := srv.Send(chunk); err != nil {
+			glog.Errorf("srv.Send: %v", err)
+			return status.Error(codes.Unavailable, "stream broken")
+		}
+	}
+	return nil
+}
+
+func (s *service) SealInvoice(ctx context.Context, req *pb.SealInvoiceRequest) (*pb.SealInvoiceResponse, error) {
+	if err := s.m.sealInvoice(ctx, req.Uid); err != nil {
+		if _, ok := status.FromError(err); ok {
+			return nil, err
+		}
+		glog.Errorf("sealInvoice(_, %q): %v", req.Uid, err)
+		return nil, status.Error(codes.Unavailable, "internal server error")
+	}
+	return &pb.SealInvoiceResponse{}, nil
+}
+
+func init() {
+	flag.Set("logtostderr", "true")
+}
+
+func main() {
+	flag.StringVar(&flagDatabasePath, "db_path", "./foo.db", "path to sqlite database")
+	flag.BoolVar(&flagInit, "init_db", false, "init database and exit")
+	flag.Parse()
+
+	m, err := newModel(flagDatabasePath)
+	if err != nil {
+		glog.Exitf("newModel: %v", err)
+	}
+	if flagInit {
+		glog.Exit(m.init())
+	}
+
+	mi := mirko.New()
+	if err := mi.Listen(); err != nil {
+		glog.Exitf("Listen failed: %v", err)
+	}
+
+	s := newService(m)
+	pb.RegisterInvoicerServer(mi.GRPC(), s)
+
+	if err := mi.Serve(); err != nil {
+		glog.Exitf("Serve failed: %v", err)
+	}
+
+	<-mi.Done()
+}