forked from hswaw/hscloud
295 lines
6.1 KiB
Go
295 lines
6.1 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"fmt"
|
|
"strconv"
|
|
"time"
|
|
|
|
"github.com/golang/glog"
|
|
"github.com/golang/protobuf/proto"
|
|
_ "github.com/mattn/go-sqlite3"
|
|
"google.golang.org/grpc/codes"
|
|
"google.golang.org/grpc/status"
|
|
|
|
pb "code.hackerspace.pl/hscloud/bgpwtf/invoice/proto"
|
|
)
|
|
|
|
type model struct {
|
|
db *sql.DB
|
|
}
|
|
|
|
func newModel(dsn string) (*model, error) {
|
|
db, err := sql.Open("sqlite3", dsn)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &model{
|
|
db: db,
|
|
}, nil
|
|
}
|
|
|
|
func (m *model) init() error {
|
|
_, err := m.db.Exec(`
|
|
create table invoice (
|
|
id integer primary key not null,
|
|
created_time integer not null,
|
|
proto blob not null
|
|
);
|
|
create table invoice_seal (
|
|
id integer primary key not null,
|
|
invoice_id integer not null,
|
|
final_uid text not null unique,
|
|
sealed_time integer not null,
|
|
foreign key (invoice_id) references invoice(id)
|
|
);
|
|
create table invoice_blob (
|
|
id integer primary key not null,
|
|
invoice_id integer not null,
|
|
pdf blob not null,
|
|
foreign key (invoice_id) references invoice(id)
|
|
);
|
|
`)
|
|
return err
|
|
}
|
|
|
|
func (m *model) sealInvoice(ctx context.Context, uid, language string, useProformaTime bool) error {
|
|
id, err := strconv.Atoi(uid)
|
|
if err != nil {
|
|
return status.Error(codes.InvalidArgument, "invalid uid")
|
|
}
|
|
|
|
invoice, err := m.getInvoice(ctx, uid)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
tx, err := m.db.BeginTx(ctx, nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
q := `
|
|
insert into invoice_seal (
|
|
invoice_id, final_uid, sealed_time
|
|
) values (
|
|
?,
|
|
( select printf("%04d", ifnull( (select final_uid as v from invoice_seal order by final_uid desc limit 1), 21000) + 1 )),
|
|
?
|
|
)
|
|
`
|
|
|
|
sealTime := time.Now()
|
|
if useProformaTime {
|
|
sealTime = time.Unix(0, invoice.Date)
|
|
}
|
|
res, err := tx.Exec(q, id, sealTime.UnixNano())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
lastInvoiceSealId, err := res.LastInsertId()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
q = `
|
|
select final_uid from invoice_seal where id = ?
|
|
`
|
|
|
|
var finalUid string
|
|
if err := tx.QueryRow(q, lastInvoiceSealId).Scan(&finalUid); err != nil {
|
|
return err
|
|
}
|
|
|
|
invoice.State = pb.Invoice_STATE_SEALED
|
|
// TODO(q3k): this should be configurable.
|
|
invoice.FinalUid = fmt.Sprintf("FV/%s", finalUid)
|
|
invoice.Date = sealTime.UnixNano()
|
|
calculateInvoiceData(invoice)
|
|
|
|
pdfBlob, err := renderInvoicePDF(invoice, language)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
q = `
|
|
insert into invoice_blob (
|
|
invoice_id, pdf
|
|
) values (
|
|
?, ?
|
|
)
|
|
`
|
|
|
|
if _, err := tx.Exec(q, id, pdfBlob); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := tx.Commit(); err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (m *model) createInvoice(ctx context.Context, id *pb.InvoiceData) (string, error) {
|
|
data, err := proto.Marshal(id)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
sql := `
|
|
insert into invoice (
|
|
proto, created_time
|
|
) values (
|
|
?, ?
|
|
)
|
|
`
|
|
|
|
t := time.Now()
|
|
if id.Date != 0 {
|
|
t = time.Unix(0, id.Date)
|
|
}
|
|
|
|
res, err := m.db.Exec(sql, data, t.UnixNano())
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
uid, err := res.LastInsertId()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
glog.Infof("%+v", uid)
|
|
return fmt.Sprintf("%d", uid), nil
|
|
}
|
|
|
|
func (m *model) getRendered(ctx context.Context, uid string) ([]byte, error) {
|
|
id, err := strconv.Atoi(uid)
|
|
if err != nil {
|
|
return nil, status.Error(codes.InvalidArgument, "invalid uid")
|
|
}
|
|
|
|
q := `
|
|
select invoice_blob.pdf from invoice_blob where invoice_blob.invoice_id = ?
|
|
`
|
|
res := m.db.QueryRow(q, id)
|
|
|
|
data := []byte{}
|
|
if err := res.Scan(&data); err != nil {
|
|
if err == sql.ErrNoRows {
|
|
return nil, status.Error(codes.InvalidArgument, "no such invoice")
|
|
}
|
|
return nil, err
|
|
}
|
|
return data, nil
|
|
}
|
|
|
|
func (m *model) getSealedUid(ctx context.Context, uid string) (string, error) {
|
|
id, err := strconv.Atoi(uid)
|
|
if err != nil {
|
|
return "", status.Error(codes.InvalidArgument, "invalid uid")
|
|
}
|
|
|
|
q := `
|
|
select invoice_seal.final_uid from invoice_seal where invoice_seal.invoice_id = ?
|
|
`
|
|
res := m.db.QueryRow(q, id)
|
|
finalUid := ""
|
|
if err := res.Scan(&finalUid); err != nil {
|
|
if err == sql.ErrNoRows {
|
|
return "", nil
|
|
}
|
|
return "", err
|
|
}
|
|
return finalUid, nil
|
|
}
|
|
|
|
type sqlInvoiceSealRow struct {
|
|
proto []byte
|
|
createdTime int64
|
|
sealedTime sql.NullInt64
|
|
finalUid sql.NullString
|
|
uid int64
|
|
}
|
|
|
|
func (s *sqlInvoiceSealRow) Proto() (*pb.Invoice, error) {
|
|
data := &pb.InvoiceData{}
|
|
if err := proto.Unmarshal(s.proto, data); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
p := &pb.Invoice{
|
|
Uid: fmt.Sprintf("%d", s.uid),
|
|
Data: data,
|
|
}
|
|
if s.finalUid.Valid {
|
|
p.State = pb.Invoice_STATE_SEALED
|
|
p.FinalUid = fmt.Sprintf("FV/%s", s.finalUid.String)
|
|
p.Date = s.sealedTime.Int64
|
|
} else {
|
|
p.State = pb.Invoice_STATE_PROFORMA
|
|
p.FinalUid = fmt.Sprintf("PROFORMA/%d", s.uid)
|
|
p.Date = s.createdTime
|
|
}
|
|
calculateInvoiceData(p)
|
|
return p, nil
|
|
}
|
|
|
|
func (m *model) getInvoice(ctx context.Context, uid string) (*pb.Invoice, error) {
|
|
id, err := strconv.Atoi(uid)
|
|
if err != nil {
|
|
return nil, status.Error(codes.InvalidArgument, "invalid uid")
|
|
}
|
|
|
|
q := `
|
|
select
|
|
invoice.id, invoice.proto, invoice.created_time, invoice_seal.sealed_time, invoice_seal.final_uid
|
|
from invoice
|
|
left join invoice_seal
|
|
on invoice_seal.invoice_id = invoice.id
|
|
where invoice.id = ?
|
|
`
|
|
res := m.db.QueryRow(q, id)
|
|
row := sqlInvoiceSealRow{}
|
|
if err := res.Scan(&row.uid, &row.proto, &row.createdTime, &row.sealedTime, &row.finalUid); err != nil {
|
|
if err == sql.ErrNoRows {
|
|
return nil, status.Error(codes.NotFound, "no such invoice")
|
|
}
|
|
return nil, err
|
|
}
|
|
|
|
return row.Proto()
|
|
}
|
|
|
|
func (m *model) getInvoices(ctx context.Context) ([]*pb.Invoice, error) {
|
|
q := `
|
|
select
|
|
invoice.id, invoice.proto, invoice.created_time, invoice_seal.sealed_time, invoice_seal.final_uid
|
|
from invoice
|
|
left join invoice_seal
|
|
on invoice_seal.invoice_id = invoice.id
|
|
`
|
|
rows, err := m.db.QueryContext(ctx, q)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
|
|
res := []*pb.Invoice{}
|
|
|
|
for rows.Next() {
|
|
row := sqlInvoiceSealRow{}
|
|
if err := rows.Scan(&row.uid, &row.proto, &row.createdTime, &row.sealedTime, &row.finalUid); err != nil {
|
|
return nil, err
|
|
}
|
|
p, err := row.Proto()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
res = append(res, p)
|
|
}
|
|
|
|
return res, nil
|
|
}
|