From 794558a007f00fb711dc35c27177b616d1ba8d5b Mon Sep 17 00:00:00 2001 From: schreifuchs Date: Tue, 26 Aug 2025 22:40:49 +0200 Subject: [PATCH] feat(api): fix POST /invoice --- .vscode/launch.json | 17 +++++++ README.md | 64 ++++++++++++++++++++++++++ cmd/invoiceapi/main.go | 21 +++------ config.example.json | 66 +++++---------------------- internal/api/api.go | 6 ++- internal/api/httpinvoce/controller.go | 4 +- internal/api/httpinvoce/model.go | 12 ++--- internal/api/httpinvoce/routes.go | 2 + internal/api/routes.go | 11 +++-- internal/config/config.go | 43 +++++++++++++++++ internal/jtype/duration.go | 17 +++++++ pkg/invoice/report/resource.go | 2 +- 12 files changed, 181 insertions(+), 84 deletions(-) create mode 100644 .vscode/launch.json create mode 100644 internal/config/config.go create mode 100644 internal/jtype/duration.go diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..3b282d8 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,17 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + + { + "name": "Launch Package", + "type": "go", + "request": "launch", + "mode": "auto", + "program": "${workspaceFolder}/cmd/invoiceapi", + "cwd": "${workspaceFolder}" + } + ] +} \ No newline at end of file diff --git a/README.md b/README.md index a5d8a83..32fdec8 100644 --- a/README.md +++ b/README.md @@ -1 +1,65 @@ docker run --rm -p 3030:3000 gotenberg/gotenberg:8 + +## Configuration + +This application is configured using a `config.json` file in the root of the project. You can use the `config.json.example` file as a starting point. + +```json +{ + "email": { + "smtp": { + "host": "smtp.example.com", + "port": 587, + "username": "user", + "password": "password" + }, + "from": "from@example.com" + }, + "pdf": { + "hostname": "http://localhost:3030" + }, + "gitea": { + "url": "https://gitea.example.com", + "token": "your-gitea-token" + } +} +``` + +## API Endpoints + +### POST /invoice + +Creates a new invoice. + +```bash +curl -X POST -H "Content-Type: application/json" -d '{ + "debtor": { + "name": "John Doe", + "Address": { + "street": "Musterstrasse", + "number": "1", + "zipCode": "1234", + "place": "Musterstadt", + "country": "CH" + }, + "contact": "john.doe@example.com" + }, + "creditor": { + "name": "Jane Doe", + "Address": { + "street": "Beispielweg", + "number": "2", + "zipCode": "5678", + "place": "Beispielhausen", + "country": "CH" + }, + "contact": "jane.doe@example.com", + "iban": "CH1234567890123456789" + }, + "durationThreshold": "1h", + "hourlyRate": 100, + "repositories": [ + "lou-taylor/accounting" + ] +}' http://localhost:8080/invoice +``` diff --git a/cmd/invoiceapi/main.go b/cmd/invoiceapi/main.go index cb8db49..ce81c23 100644 --- a/cmd/invoiceapi/main.go +++ b/cmd/invoiceapi/main.go @@ -5,22 +5,15 @@ import ( "os" "git.schreifuchs.ch/lou-taylor/accounting/internal/api" + "git.schreifuchs.ch/lou-taylor/accounting/internal/config" ) func main() { - // var cfg invoice.Config - // file, err := os.Open("config.json") - // if err != nil { - // panic(err) - // } - // defer file.Close() - // decoder := json.NewDecoder(file) - // err = decoder.Decode(&cfg) - // if err != nil { - // panic(err) - // } - // - // invoice.Generate(cfg) + cfg, err := config.Load("config.json") + if err != nil { + panic(err) + } + log := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug})) - api.Start(log, ":8080") + api.Start(log, ":8080", cfg) } diff --git a/config.example.json b/config.example.json index 42d411d..c8cb3fd 100644 --- a/config.example.json +++ b/config.example.json @@ -1,60 +1,18 @@ { - "gitea_url": "https://git.schreifuchs.ch", - "gitea_token": "", - "repos": [ - { - "owner": "lou-taylor", - "repo": "lou-taylor-web" - }, - { - "owner": "lou-taylor", - "repo": "lou-taylor-api" - }, - { - "owner": "lou-taylor", - "repo": "accounting" - } - ], - "min_duration": "15m", - "hourly": 16, - "from_entity": { - "name": "schreifuchs.ch", - "iban": "", - "address": { - "street": "", - "number": "", - "zip_code": "", - "place": "", - "country": "" - }, - "contact": "" - }, - "to_entity": { - "name": "", - "address": { - "street": "", - "number": "", - "zip_code": "", - "place": "", - "country": "" - }, - "contact": "Loana Groux" - }, - "pdf_generator_url": "http://localhost:3030", - "mailer": { + "email": { "smtp": { - "host": "mail.your-server.de", - "port": "465", - "user": "", - "password": "" + "host": "smtp.example.com", + "port": "587", + "username": "user", + "password": "password" }, - "from": "" + "from": "from@example.com" }, - "mail": { - "to": "", - "subject": "", - "body": "" + "pdf": { + "hostname": "http://localhost:3030" }, - "mail_bcc": [""] + "gitea": { + "url": "https://gitea.example.com", + "token": "your-gitea-token" + } } - diff --git a/internal/api/api.go b/internal/api/api.go index 293650d..1d6e0a5 100644 --- a/internal/api/api.go +++ b/internal/api/api.go @@ -5,11 +5,13 @@ import ( "log/slog" "net/http" "time" + + "git.schreifuchs.ch/lou-taylor/accounting/internal/config" ) -func Start(log *slog.Logger, address string) error { +func Start(log *slog.Logger, address string, cfg *config.Config) error { mux := http.NewServeMux() - RegisterRoutes(log, mux) + RegisterRoutes(log, mux, cfg) s := &http.Server{ Addr: address, diff --git a/internal/api/httpinvoce/controller.go b/internal/api/httpinvoce/controller.go index ce72b98..6d84c31 100644 --- a/internal/api/httpinvoce/controller.go +++ b/internal/api/httpinvoce/controller.go @@ -29,7 +29,7 @@ func (s Service) createInvoice(w http.ResponseWriter, r *http.Request) { return } - invoice, err := s.invoice.Generate(req.Creditor, req.Debtor, req.DurationThreshold, req.HourlyRate, repos) + invoice, err := s.invoice.Generate(req.Creditor, req.Debtor, time.Duration(req.DurationThreshold), req.HourlyRate, repos) if err != nil { log.Println("error while processing invoice:", err) fmt.Fprint(w, "internal server error") @@ -63,7 +63,7 @@ func (s Service) sendInvoice(w http.ResponseWriter, r *http.Request) { s.sendErr(w, 500, err.Error()) return } - invoice, err := s.invoice.Generate(req.Invoice.Creditor, req.Invoice.Debtor, req.Invoice.DurationThreshold, req.Invoice.HourlyRate, repos) + invoice, err := s.invoice.Generate(req.Invoice.Creditor, req.Invoice.Debtor, time.Duration(req.Invoice.DurationThreshold), req.Invoice.HourlyRate, repos) if err != nil { s.sendErr(w, http.StatusInternalServerError, "error while processing invoice:", err) return diff --git a/internal/api/httpinvoce/model.go b/internal/api/httpinvoce/model.go index fa12767..faf2859 100644 --- a/internal/api/httpinvoce/model.go +++ b/internal/api/httpinvoce/model.go @@ -5,19 +5,19 @@ import ( "log/slog" "net/http" "strings" - "time" "git.schreifuchs.ch/lou-taylor/accounting/internal/email" + "git.schreifuchs.ch/lou-taylor/accounting/internal/jtype" "git.schreifuchs.ch/lou-taylor/accounting/pkg/invoice" "git.schreifuchs.ch/lou-taylor/accounting/pkg/invoice/model" ) type invoiceReq struct { - Debtor model.Entity `json:"debtor"` - Creditor model.Entity `json:"creditor"` - DurationThreshold time.Duration `json:"durationThreshold"` - HourlyRate float64 `json:"hourlyRate"` - Repos []string `json:"repositories"` + Debtor model.Entity `json:"debtor"` + Creditor model.Entity `json:"creditor"` + DurationThreshold jtype.Duration `json:"durationThreshold"` + HourlyRate float64 `json:"hourlyRate"` + Repos []string `json:"repositories"` } func (i invoiceReq) GetRepos() (repos []invoice.Repo, err error) { diff --git a/internal/api/httpinvoce/routes.go b/internal/api/httpinvoce/routes.go index 9bfd3bf..6837587 100644 --- a/internal/api/httpinvoce/routes.go +++ b/internal/api/httpinvoce/routes.go @@ -4,6 +4,8 @@ import ( "net/http" ) +// RegisterRoutes registers the HTTP routes for the invoice service. func (s Service) RegisterRoutes(mux *http.ServeMux) { mux.HandleFunc("POST /invoice", s.createInvoice) + mux.HandleFunc("POST /invoice/send", s.sendInvoice) } diff --git a/internal/api/routes.go b/internal/api/routes.go index 70dcaf9..d0c9da6 100644 --- a/internal/api/routes.go +++ b/internal/api/routes.go @@ -6,26 +6,27 @@ import ( "code.gitea.io/sdk/gitea" "git.schreifuchs.ch/lou-taylor/accounting/internal/api/httpinvoce" + "git.schreifuchs.ch/lou-taylor/accounting/internal/config" "git.schreifuchs.ch/lou-taylor/accounting/internal/email" "git.schreifuchs.ch/lou-taylor/accounting/pdf" "git.schreifuchs.ch/lou-taylor/accounting/pkg/invoice" ) -func RegisterRoutes(log *slog.Logger, mux *http.ServeMux) error { - gotenberg, err := pdf.New("http://localhost:3030") +func RegisterRoutes(log *slog.Logger, mux *http.ServeMux, cfg *config.Config) error { + gotenberg, err := pdf.New(cfg.PDF.Hostname) if err != nil { panic(err) } giteaC, err := gitea.NewClient( - "https://git.schreifuchs.ch", - gitea.SetToken("6a8ea8f9de039b0950c634bfea40c6f97f94b06b"), + cfg.Gitea.URL, + gitea.SetToken(cfg.Gitea.Token), ) if err != nil { panic(err) } invoicer := invoice.New(log, giteaC, gotenberg) - mailer, err := email.New(email.Config{}) + mailer, err := email.New(cfg.Email) if err != nil { return err } diff --git a/internal/config/config.go b/internal/config/config.go new file mode 100644 index 0000000..18ae786 --- /dev/null +++ b/internal/config/config.go @@ -0,0 +1,43 @@ +package config + +import ( + "encoding/json" + "os" + + "git.schreifuchs.ch/lou-taylor/accounting/internal/email" +) + +// PDF holds the configuration for the PDF generator. +type PDF struct { + Hostname string `json:"hostname"` +} + +// Gitea holds the configuration for the Gitea client. +type Gitea struct { + URL string `json:"url"` + Token string `json:"token"` +} + +// Config holds the configuration for the entire application. +type Config struct { + Email email.Config `json:"email"` + PDF PDF `json:"pdf"` + Gitea Gitea `json:"gitea"` +} + +// Load loads the configuration from a JSON file. +func Load(path string) (*Config, error) { + file, err := os.Open(path) + if err != nil { + return nil, err + } + defer file.Close() + + cfg := &Config{} + decoder := json.NewDecoder(file) + if err := decoder.Decode(cfg); err != nil { + return nil, err + } + + return cfg, nil +} diff --git a/internal/jtype/duration.go b/internal/jtype/duration.go new file mode 100644 index 0000000..6cc481b --- /dev/null +++ b/internal/jtype/duration.go @@ -0,0 +1,17 @@ +package jtype + +import ( + "strings" + "time" +) + +type Duration time.Duration + +func (d *Duration) UnmarshalJSON(b []byte) error { + dur, err := time.ParseDuration(strings.ReplaceAll(string(b), `"`, "")) + if err != nil { + return err + } + *d = Duration(dur) + return nil +} diff --git a/pkg/invoice/report/resource.go b/pkg/invoice/report/resource.go index a0f9d00..06f8e09 100644 --- a/pkg/invoice/report/resource.go +++ b/pkg/invoice/report/resource.go @@ -25,7 +25,7 @@ func New(issues []issue.Issue, company, client model.Entity, rate float64) *Repo Company: company, Client: client, } - // r.Invoice = qrbill.New(r.applyRate(r.Total()), r.Company, &r.Client) + r.Invoice = qrbill.New(r.applyRate(r.Total()), r.Company, &r.Client) return r }