feat(api): fix POST /invoice
This commit is contained in:
17
.vscode/launch.json
vendored
Normal file
17
.vscode/launch.json
vendored
Normal file
@@ -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}"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
64
README.md
64
README.md
@@ -1 +1,65 @@
|
|||||||
docker run --rm -p 3030:3000 gotenberg/gotenberg:8
|
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
|
||||||
|
```
|
||||||
|
|||||||
@@ -5,22 +5,15 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
|
|
||||||
"git.schreifuchs.ch/lou-taylor/accounting/internal/api"
|
"git.schreifuchs.ch/lou-taylor/accounting/internal/api"
|
||||||
|
"git.schreifuchs.ch/lou-taylor/accounting/internal/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
// var cfg invoice.Config
|
cfg, err := config.Load("config.json")
|
||||||
// file, err := os.Open("config.json")
|
if err != nil {
|
||||||
// if err != nil {
|
panic(err)
|
||||||
// panic(err)
|
}
|
||||||
// }
|
|
||||||
// defer file.Close()
|
|
||||||
// decoder := json.NewDecoder(file)
|
|
||||||
// err = decoder.Decode(&cfg)
|
|
||||||
// if err != nil {
|
|
||||||
// panic(err)
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// invoice.Generate(cfg)
|
|
||||||
log := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug}))
|
log := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug}))
|
||||||
api.Start(log, ":8080")
|
api.Start(log, ":8080", cfg)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,60 +1,18 @@
|
|||||||
{
|
{
|
||||||
"gitea_url": "https://git.schreifuchs.ch",
|
"email": {
|
||||||
"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": {
|
|
||||||
"smtp": {
|
"smtp": {
|
||||||
"host": "mail.your-server.de",
|
"host": "smtp.example.com",
|
||||||
"port": "465",
|
"port": "587",
|
||||||
"user": "",
|
"username": "user",
|
||||||
"password": ""
|
"password": "password"
|
||||||
},
|
},
|
||||||
"from": ""
|
"from": "from@example.com"
|
||||||
},
|
},
|
||||||
"mail": {
|
"pdf": {
|
||||||
"to": "",
|
"hostname": "http://localhost:3030"
|
||||||
"subject": "",
|
|
||||||
"body": ""
|
|
||||||
},
|
},
|
||||||
"mail_bcc": [""]
|
"gitea": {
|
||||||
|
"url": "https://gitea.example.com",
|
||||||
|
"token": "your-gitea-token"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,11 +5,13 @@ import (
|
|||||||
"log/slog"
|
"log/slog"
|
||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"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()
|
mux := http.NewServeMux()
|
||||||
RegisterRoutes(log, mux)
|
RegisterRoutes(log, mux, cfg)
|
||||||
|
|
||||||
s := &http.Server{
|
s := &http.Server{
|
||||||
Addr: address,
|
Addr: address,
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ func (s Service) createInvoice(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
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 {
|
if err != nil {
|
||||||
log.Println("error while processing invoice:", err)
|
log.Println("error while processing invoice:", err)
|
||||||
fmt.Fprint(w, "internal server error")
|
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())
|
s.sendErr(w, 500, err.Error())
|
||||||
return
|
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 {
|
if err != nil {
|
||||||
s.sendErr(w, http.StatusInternalServerError, "error while processing invoice:", err)
|
s.sendErr(w, http.StatusInternalServerError, "error while processing invoice:", err)
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -5,19 +5,19 @@ import (
|
|||||||
"log/slog"
|
"log/slog"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
|
||||||
|
|
||||||
"git.schreifuchs.ch/lou-taylor/accounting/internal/email"
|
"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"
|
||||||
"git.schreifuchs.ch/lou-taylor/accounting/pkg/invoice/model"
|
"git.schreifuchs.ch/lou-taylor/accounting/pkg/invoice/model"
|
||||||
)
|
)
|
||||||
|
|
||||||
type invoiceReq struct {
|
type invoiceReq struct {
|
||||||
Debtor model.Entity `json:"debtor"`
|
Debtor model.Entity `json:"debtor"`
|
||||||
Creditor model.Entity `json:"creditor"`
|
Creditor model.Entity `json:"creditor"`
|
||||||
DurationThreshold time.Duration `json:"durationThreshold"`
|
DurationThreshold jtype.Duration `json:"durationThreshold"`
|
||||||
HourlyRate float64 `json:"hourlyRate"`
|
HourlyRate float64 `json:"hourlyRate"`
|
||||||
Repos []string `json:"repositories"`
|
Repos []string `json:"repositories"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (i invoiceReq) GetRepos() (repos []invoice.Repo, err error) {
|
func (i invoiceReq) GetRepos() (repos []invoice.Repo, err error) {
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// RegisterRoutes registers the HTTP routes for the invoice service.
|
||||||
func (s Service) RegisterRoutes(mux *http.ServeMux) {
|
func (s Service) RegisterRoutes(mux *http.ServeMux) {
|
||||||
mux.HandleFunc("POST /invoice", s.createInvoice)
|
mux.HandleFunc("POST /invoice", s.createInvoice)
|
||||||
|
mux.HandleFunc("POST /invoice/send", s.sendInvoice)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,26 +6,27 @@ import (
|
|||||||
|
|
||||||
"code.gitea.io/sdk/gitea"
|
"code.gitea.io/sdk/gitea"
|
||||||
"git.schreifuchs.ch/lou-taylor/accounting/internal/api/httpinvoce"
|
"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/internal/email"
|
||||||
"git.schreifuchs.ch/lou-taylor/accounting/pdf"
|
"git.schreifuchs.ch/lou-taylor/accounting/pdf"
|
||||||
"git.schreifuchs.ch/lou-taylor/accounting/pkg/invoice"
|
"git.schreifuchs.ch/lou-taylor/accounting/pkg/invoice"
|
||||||
)
|
)
|
||||||
|
|
||||||
func RegisterRoutes(log *slog.Logger, mux *http.ServeMux) error {
|
func RegisterRoutes(log *slog.Logger, mux *http.ServeMux, cfg *config.Config) error {
|
||||||
gotenberg, err := pdf.New("http://localhost:3030")
|
gotenberg, err := pdf.New(cfg.PDF.Hostname)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
giteaC, err := gitea.NewClient(
|
giteaC, err := gitea.NewClient(
|
||||||
"https://git.schreifuchs.ch",
|
cfg.Gitea.URL,
|
||||||
gitea.SetToken("6a8ea8f9de039b0950c634bfea40c6f97f94b06b"),
|
gitea.SetToken(cfg.Gitea.Token),
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
invoicer := invoice.New(log, giteaC, gotenberg)
|
invoicer := invoice.New(log, giteaC, gotenberg)
|
||||||
mailer, err := email.New(email.Config{})
|
mailer, err := email.New(cfg.Email)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
43
internal/config/config.go
Normal file
43
internal/config/config.go
Normal file
@@ -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
|
||||||
|
}
|
||||||
17
internal/jtype/duration.go
Normal file
17
internal/jtype/duration.go
Normal file
@@ -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
|
||||||
|
}
|
||||||
@@ -25,7 +25,7 @@ func New(issues []issue.Issue, company, client model.Entity, rate float64) *Repo
|
|||||||
Company: company,
|
Company: company,
|
||||||
Client: client,
|
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
|
return r
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user