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 }