blob: 51fe85069a4af1ccd2090cb4e8e209c89447f867 [file] [log] [blame]
Sergiusz Bazanskifb18c992019-05-01 12:27:03 +02001package main
2
3import (
4 "context"
5 "database/sql"
6 "fmt"
7 "strconv"
Sergiusz Bazanski3976e3c2019-05-01 15:27:49 +02008 "time"
Sergiusz Bazanskifb18c992019-05-01 12:27:03 +02009
10 "github.com/golang/glog"
11 "github.com/golang/protobuf/proto"
12 _ "github.com/mattn/go-sqlite3"
13 "google.golang.org/grpc/codes"
14 "google.golang.org/grpc/status"
15
16 pb "code.hackerspace.pl/hscloud/proto/invoice"
17)
18
19type model struct {
20 db *sql.DB
21}
22
23func newModel(dsn string) (*model, error) {
24 db, err := sql.Open("sqlite3", dsn)
25 if err != nil {
26 return nil, err
27 }
28 return &model{
29 db: db,
30 }, nil
31}
32
33func (m *model) init() error {
34 _, err := m.db.Exec(`
35 create table invoice (
36 id integer primary key not null,
Sergiusz Bazanski3976e3c2019-05-01 15:27:49 +020037 created_time integer not null,
Sergiusz Bazanskifb18c992019-05-01 12:27:03 +020038 proto blob not null
39 );
40 create table invoice_seal (
41 id integer primary key not null,
42 invoice_id integer not null,
43 final_uid text not null unique,
Sergiusz Bazanski3976e3c2019-05-01 15:27:49 +020044 sealed_time integer not null,
Sergiusz Bazanskifb18c992019-05-01 12:27:03 +020045 foreign key (invoice_id) references invoice(id)
46 );
47 create table invoice_blob (
48 id integer primary key not null,
49 invoice_id integer not null,
50 pdf blob not null,
51 foreign key (invoice_id) references invoice(id)
52 );
53 `)
54 return err
55}
56
Sergiusz Bazanskia818ef22019-06-07 10:37:22 +020057func (m *model) sealInvoice(ctx context.Context, uid, language string, useProformaTime bool) error {
Sergiusz Bazanskifb18c992019-05-01 12:27:03 +020058 id, err := strconv.Atoi(uid)
59 if err != nil {
60 return status.Error(codes.InvalidArgument, "invalid uid")
61 }
62
63 invoice, err := m.getInvoice(ctx, uid)
64 if err != nil {
65 return err
66 }
67
68 tx, err := m.db.BeginTx(ctx, nil)
69 if err != nil {
70 return err
71 }
72
73 q := `
74 insert into invoice_seal (
Sergiusz Bazanski3976e3c2019-05-01 15:27:49 +020075 invoice_id, final_uid, sealed_time
Sergiusz Bazanskifb18c992019-05-01 12:27:03 +020076 ) values (
77 ?,
Sergiusz Bazanski3976e3c2019-05-01 15:27:49 +020078 ( select printf("%04d", ifnull( (select final_uid as v from invoice_seal order by final_uid desc limit 1), 19000) + 1 )),
79 ?
Sergiusz Bazanskifb18c992019-05-01 12:27:03 +020080 )
81
82 `
Sergiusz Bazanski3976e3c2019-05-01 15:27:49 +020083 sealTime := time.Now()
Sergiusz Bazanskia818ef22019-06-07 10:37:22 +020084 if useProformaTime {
85 sealTime = time.Unix(0, invoice.Date)
86 }
Sergiusz Bazanski3976e3c2019-05-01 15:27:49 +020087 res, err := tx.Exec(q, id, sealTime.UnixNano())
Sergiusz Bazanskifb18c992019-05-01 12:27:03 +020088 if err != nil {
89 return err
90 }
91
92 lastInvoiceSealId, err := res.LastInsertId()
93 if err != nil {
94 return err
95 }
96
97 q = `
98 select final_uid from invoice_seal where id = ?
99 `
100
101 var finalUid string
102 if err := tx.QueryRow(q, lastInvoiceSealId).Scan(&finalUid); err != nil {
103 return err
104 }
105
Sergiusz Bazanski3976e3c2019-05-01 15:27:49 +0200106 invoice.State = pb.Invoice_STATE_SEALED
Sergiusz Bazanskia818ef22019-06-07 10:37:22 +0200107 // TODO(q3k): this should be configurable.
108 if language == "pl" {
109 invoice.FinalUid = fmt.Sprintf("FV/%s", finalUid)
110 } else {
111 invoice.FinalUid = fmt.Sprintf("%s", finalUid)
112 }
Sergiusz Bazanski3976e3c2019-05-01 15:27:49 +0200113 invoice.Date = sealTime.UnixNano()
114 calculateInvoiceData(invoice)
115
Sergiusz Bazanskia818ef22019-06-07 10:37:22 +0200116 pdfBlob, err := renderInvoicePDF(invoice, language)
Sergiusz Bazanski3976e3c2019-05-01 15:27:49 +0200117 if err != nil {
118 return err
119 }
120
Sergiusz Bazanskifb18c992019-05-01 12:27:03 +0200121 q = `
122 insert into invoice_blob (
123 invoice_id, pdf
124 ) values (
Sergiusz Bazanski3976e3c2019-05-01 15:27:49 +0200125 ?, ?
Sergiusz Bazanskifb18c992019-05-01 12:27:03 +0200126 )
127 `
128
Sergiusz Bazanskifb18c992019-05-01 12:27:03 +0200129 if _, err := tx.Exec(q, id, pdfBlob); err != nil {
130 return err
131 }
132
133 if err := tx.Commit(); err != nil {
134 return err
135 }
136
137 return nil
138}
139
Sergiusz Bazanski3976e3c2019-05-01 15:27:49 +0200140func (m *model) createInvoice(ctx context.Context, id *pb.InvoiceData) (string, error) {
141 data, err := proto.Marshal(id)
Sergiusz Bazanskifb18c992019-05-01 12:27:03 +0200142 if err != nil {
143 return "", err
144 }
145
146 sql := `
147 insert into invoice (
Sergiusz Bazanski3976e3c2019-05-01 15:27:49 +0200148 proto, created_time
Sergiusz Bazanskifb18c992019-05-01 12:27:03 +0200149 ) values (
Sergiusz Bazanski3976e3c2019-05-01 15:27:49 +0200150 ?, ?
Sergiusz Bazanskifb18c992019-05-01 12:27:03 +0200151 )
152 `
Sergiusz Bazanskia818ef22019-06-07 10:37:22 +0200153
154 t := time.Now()
155 if id.Date != 0 {
156 t = time.Unix(0, id.Date)
157 }
158
159 res, err := m.db.Exec(sql, data, t.UnixNano())
Sergiusz Bazanskifb18c992019-05-01 12:27:03 +0200160 if err != nil {
161 return "", err
162 }
Sergiusz Bazanski3976e3c2019-05-01 15:27:49 +0200163 uid, err := res.LastInsertId()
Sergiusz Bazanskifb18c992019-05-01 12:27:03 +0200164 if err != nil {
165 return "", err
166 }
167
Sergiusz Bazanski3976e3c2019-05-01 15:27:49 +0200168 glog.Infof("%+v", uid)
169 return fmt.Sprintf("%d", uid), nil
Sergiusz Bazanskifb18c992019-05-01 12:27:03 +0200170}
171
172func (m *model) getRendered(ctx context.Context, uid string) ([]byte, error) {
173 id, err := strconv.Atoi(uid)
174 if err != nil {
175 return nil, status.Error(codes.InvalidArgument, "invalid uid")
176 }
177
178 q := `
179 select invoice_blob.pdf from invoice_blob where invoice_blob.invoice_id = ?
180 `
181 res := m.db.QueryRow(q, id)
182
183 data := []byte{}
184 if err := res.Scan(&data); err != nil {
185 if err == sql.ErrNoRows {
186 return nil, status.Error(codes.InvalidArgument, "no such invoice")
187 }
188 return nil, err
189 }
190 return data, nil
191}
192
193func (m *model) getSealedUid(ctx context.Context, uid string) (string, error) {
194 id, err := strconv.Atoi(uid)
195 if err != nil {
196 return "", status.Error(codes.InvalidArgument, "invalid uid")
197 }
198
199 q := `
200 select invoice_seal.final_uid from invoice_seal where invoice_seal.invoice_id = ?
201 `
202 res := m.db.QueryRow(q, id)
203 finalUid := ""
204 if err := res.Scan(&finalUid); err != nil {
205 if err == sql.ErrNoRows {
206 return "", nil
207 }
208 return "", err
209 }
210 return finalUid, nil
211}
212
Sergiusz Bazanski3976e3c2019-05-01 15:27:49 +0200213type sqlInvoiceSealRow struct {
214 proto []byte
215 createdTime int64
216 sealedTime sql.NullInt64
217 finalUid sql.NullString
218 uid int64
219}
220
221func (s *sqlInvoiceSealRow) Proto() (*pb.Invoice, error) {
222 data := &pb.InvoiceData{}
223 if err := proto.Unmarshal(s.proto, data); err != nil {
224 return nil, err
225 }
226
227 p := &pb.Invoice{
228 Uid: fmt.Sprintf("%d", s.uid),
229 Data: data,
230 }
231 if s.finalUid.Valid {
232 p.State = pb.Invoice_STATE_SEALED
Sergiusz Bazanskia818ef22019-06-07 10:37:22 +0200233 p.FinalUid = fmt.Sprintf("%s", s.finalUid.String)
Sergiusz Bazanski3976e3c2019-05-01 15:27:49 +0200234 p.Date = s.sealedTime.Int64
235 } else {
236 p.State = pb.Invoice_STATE_PROFORMA
237 p.FinalUid = fmt.Sprintf("PROFORMA/%d", s.uid)
238 p.Date = s.createdTime
239 }
240 calculateInvoiceData(p)
241 return p, nil
242}
243
Sergiusz Bazanskifb18c992019-05-01 12:27:03 +0200244func (m *model) getInvoice(ctx context.Context, uid string) (*pb.Invoice, error) {
245 id, err := strconv.Atoi(uid)
246 if err != nil {
247 return nil, status.Error(codes.InvalidArgument, "invalid uid")
248 }
249
250 q := `
Sergiusz Bazanski3976e3c2019-05-01 15:27:49 +0200251 select
252 invoice.id, invoice.proto, invoice.created_time, invoice_seal.sealed_time, invoice_seal.final_uid
253 from invoice
254 left join invoice_seal
255 on invoice_seal.invoice_id = invoice.id
256 where invoice.id = ?
Sergiusz Bazanskifb18c992019-05-01 12:27:03 +0200257 `
258 res := m.db.QueryRow(q, id)
Sergiusz Bazanski3976e3c2019-05-01 15:27:49 +0200259 row := sqlInvoiceSealRow{}
260 if err := res.Scan(&row.uid, &row.proto, &row.createdTime, &row.sealedTime, &row.finalUid); err != nil {
Sergiusz Bazanskifb18c992019-05-01 12:27:03 +0200261 if err == sql.ErrNoRows {
262 return nil, status.Error(codes.NotFound, "no such invoice")
263 }
264 return nil, err
265 }
266
Sergiusz Bazanski3976e3c2019-05-01 15:27:49 +0200267 return row.Proto()
Sergiusz Bazanskifb18c992019-05-01 12:27:03 +0200268}
Sergiusz Bazanski57ef6b02019-05-01 14:08:29 +0200269
Sergiusz Bazanski3976e3c2019-05-01 15:27:49 +0200270func (m *model) getInvoices(ctx context.Context) ([]*pb.Invoice, error) {
Sergiusz Bazanski57ef6b02019-05-01 14:08:29 +0200271 q := `
Sergiusz Bazanski3976e3c2019-05-01 15:27:49 +0200272 select
273 invoice.id, invoice.proto, invoice.created_time, invoice_seal.sealed_time, invoice_seal.final_uid
274 from invoice
Sergiusz Bazanski57ef6b02019-05-01 14:08:29 +0200275 left join invoice_seal
276 on invoice_seal.invoice_id = invoice.id
277 `
278 rows, err := m.db.QueryContext(ctx, q)
279 if err != nil {
Sergiusz Bazanski3976e3c2019-05-01 15:27:49 +0200280 return nil, err
Sergiusz Bazanski57ef6b02019-05-01 14:08:29 +0200281 }
282 defer rows.Close()
283
Sergiusz Bazanski3976e3c2019-05-01 15:27:49 +0200284 res := []*pb.Invoice{}
285
Sergiusz Bazanski57ef6b02019-05-01 14:08:29 +0200286 for rows.Next() {
Sergiusz Bazanski3976e3c2019-05-01 15:27:49 +0200287 row := sqlInvoiceSealRow{}
288 if err := rows.Scan(&row.uid, &row.proto, &row.createdTime, &row.sealedTime, &row.finalUid); err != nil {
289 return nil, err
Sergiusz Bazanski57ef6b02019-05-01 14:08:29 +0200290 }
Sergiusz Bazanski3976e3c2019-05-01 15:27:49 +0200291 p, err := row.Proto()
292 if err != nil {
293 return nil, err
Sergiusz Bazanski57ef6b02019-05-01 14:08:29 +0200294 }
Sergiusz Bazanski3976e3c2019-05-01 15:27:49 +0200295 res = append(res, p)
Sergiusz Bazanski57ef6b02019-05-01 14:08:29 +0200296 }
297
298 return res, nil
299}