feat: add a new command - invoice create (#1)

Reviewed-on: #1
This commit is contained in:
Dawid Wysokiński 2023-04-08 05:44:32 +00:00
parent 4aec517c74
commit c51fe15e77
8 changed files with 268 additions and 26 deletions

2
.commitlintrc.yml Normal file
View File

@ -0,0 +1,2 @@
extends:
- "@commitlint/config-conventional"

2
.gitignore vendored
View File

@ -23,3 +23,5 @@ go.work
.idea
bin
invoices.json

View File

@ -58,7 +58,7 @@ linters-settings:
tagliatelle:
case:
rules:
json: camel
json: snake
lll:
line-length: 150
gocyclo:

View File

@ -1,24 +0,0 @@
package main
import (
"log"
"os"
"github.com/urfave/cli/v2"
)
const (
appName = "infakt-cli"
)
func main() {
app := cli.NewApp()
app.Name = appName
app.Usage = "CLI tool for interacting with the InFakt API"
app.HelpName = appName
app.EnableBashCompletion = true
if err := app.Run(os.Args); err != nil {
log.Fatal(err)
}
}

5
go.mod
View File

@ -2,7 +2,10 @@ module gitea.dwysokinski.me/Kichiyaki/infakt-cli
go 1.20
require github.com/urfave/cli/v2 v2.25.1
require (
github.com/urfave/cli/v2 v2.25.1
golang.org/x/time v0.3.0
)
require (
github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect

2
go.sum
View File

@ -6,3 +6,5 @@ github.com/urfave/cli/v2 v2.25.1 h1:zw8dSP7ghX0Gmm8vugrs6q9Ku0wzweqPyshy+syu9Gw=
github.com/urfave/cli/v2 v2.25.1/go.mod h1:GHupkWPMM0M/sj1a2b4wUrWBPzazNrIjouW6fmdJLxc=
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU=
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8=
golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=
golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=

168
internal/infakt/client.go Normal file
View File

@ -0,0 +1,168 @@
package infakt
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
urlpkg "net/url"
"time"
"golang.org/x/time/rate"
)
const (
defaultBaseURL = "https://api.infakt.pl"
defaultTimeout = 10 * time.Second
//nolint:gosec
apiKeyHeader = "X-inFakt-ApiKey"
reqPerMinute = 150
endpointCreateInvoice = "/api/v3/invoices.json"
)
type Client struct {
baseURL string
apiKey string
client *http.Client
rateLimiter *rate.Limiter
}
type ClientOption func(c *Client)
func WithBaseURL(s string) ClientOption {
return func(c *Client) {
c.baseURL = s
}
}
func WithHTTPClient(hc *http.Client) ClientOption {
return func(c *Client) {
c.client = hc
}
}
func NewClient(apiKey string, opts ...ClientOption) *Client {
c := &Client{
baseURL: defaultBaseURL,
apiKey: apiKey,
client: &http.Client{
Timeout: defaultTimeout,
},
rateLimiter: rate.NewLimiter(rate.Every(time.Minute), reqPerMinute),
}
for _, opt := range opts {
opt(c)
}
return c
}
type CreateInvoiceParamsService struct {
Name string `json:"name"`
TaxSymbol string `json:"tax_symbol,omitempty"`
Unit string `json:"unit,omitempty"`
Quantity float64 `json:"quantity,omitempty"`
UnitNetPrice int `json:"unit_net_price,omitempty"`
FlatRateTaxSymbol string `json:"flat_rate_tax_symbol,omitempty"`
}
type CreateInvoiceParams struct {
Currency string `json:"currency,omitempty"`
Kind string `json:"kind,omitempty"`
PaymentMethod string `json:"payment_method,omitempty"`
InvoiceDate string `json:"invoice_date,omitempty"`
SaleDate string `json:"sale_date,omitempty"`
PaymentDate string `json:"payment_date,omitempty"`
ClientID int `json:"client_id,omitempty"`
ClientCompanyName string `json:"client_company_name,omitempty"`
ClientFirstName string `json:"client_first_name,omitempty"`
ClientBusinessActivityKind string `json:"client_business_activity_kind,omitempty"`
ClientStreet string `json:"client_street,omitempty"`
ClientStreetNumber string `json:"client_street_number,omitempty"`
ClientFlatNumber string `json:"client_flat_number,omitempty"`
ClientCity string `json:"client_city,omitempty"`
ClientPostCode string `json:"client_post_code,omitempty"`
ClientTaxCode string `json:"client_tax_code,omitempty"`
ClientCountry string `json:"client_country,omitempty"`
SaleType string `json:"sale_type,omitempty"`
SellerSignature string `json:"seller_signature"`
InvoiceDateKind string `json:"invoice_date_kind,omitempty"`
Services []CreateInvoiceParamsService `json:"services,omitempty"`
VatExemptionReason int `json:"vat_exemption_reason,omitempty"`
BankAccount string `json:"bank_account,omitempty"`
BankName string `json:"bank_name,omitempty"`
Swift string `json:"swift,omitempty"`
}
type Invoice struct {
ID int `json:"id"`
UUID string `json:"uuid"`
Number string `json:"number"`
}
func (c *Client) CreateInvoice(ctx context.Context, params CreateInvoiceParams) (Invoice, error) {
resp, err := c.postJSON(ctx, endpointCreateInvoice, map[string]any{
"invoice": params,
})
if err != nil {
return Invoice{}, err
}
defer func() {
_, _ = io.Copy(io.Discard, resp.Body)
_ = resp.Body.Close()
}()
var invoice Invoice
if err = json.NewDecoder(resp.Body).Decode(&invoice); err != nil {
return Invoice{}, err
}
return invoice, nil
}
func (c *Client) postJSON(ctx context.Context, url string, body any) (*http.Response, error) {
buf := bytes.NewBuffer(nil)
if err := json.NewEncoder(buf).Encode(body); err != nil {
return nil, err
}
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, buf)
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", "application/json")
return c.do(req)
}
func (c *Client) do(req *http.Request) (*http.Response, error) {
url, err := urlpkg.Parse(c.baseURL + req.URL.String())
if err != nil {
return nil, err
}
req.URL = url
req.Header.Set(apiKeyHeader, c.apiKey)
if err = c.rateLimiter.Wait(req.Context()); err != nil {
return nil, err
}
resp, err := c.client.Do(req)
if err != nil {
return nil, err
}
if resp.StatusCode >= http.StatusBadRequest {
b, _ := io.ReadAll(resp.Body)
_, _ = io.Copy(io.Discard, resp.Body)
_ = resp.Body.Close()
return nil, fmt.Errorf("request failed with status=%d and body=%s", resp.StatusCode, b)
}
return resp, nil
}

89
main.go Normal file
View File

@ -0,0 +1,89 @@
package main
import (
"encoding/json"
"log"
"os"
"gitea.dwysokinski.me/Kichiyaki/infakt-cli/internal/infakt"
"github.com/urfave/cli/v2"
)
const (
appName = "infakt-cli"
)
func main() {
app := cli.NewApp()
app.Name = appName
app.Usage = "CLI tool for interacting with the InFakt API"
app.HelpName = appName
app.Flags = []cli.Flag{
&cli.StringFlag{
Name: "url",
Aliases: []string{"u"},
DefaultText: "https://api.infakt.pl",
},
&cli.StringFlag{
Name: "apiKey",
EnvVars: []string{"INFAKT_API_KEY"},
Required: true,
},
}
app.Commands = []*cli.Command{
newInvoiceCmd(),
}
app.EnableBashCompletion = true
if err := app.Run(os.Args); err != nil {
log.Fatal(err)
}
}
func newInvoiceCmd() *cli.Command {
return &cli.Command{
Name: "invoice",
Subcommands: []*cli.Command{
{
Name: "create",
Action: func(c *cli.Context) error {
var clientOpts []infakt.ClientOption
if url := c.String("url"); url != "" {
clientOpts = append(clientOpts, infakt.WithBaseURL(url))
}
b, err := os.ReadFile(c.Path("file"))
if err != nil {
return err
}
var params []infakt.CreateInvoiceParams
if err = json.Unmarshal(b, &params); err != nil {
return err
}
client := infakt.NewClient(c.String("apiKey"), clientOpts...)
for i, p := range params {
invoice, err := client.CreateInvoice(c.Context, p)
if err != nil {
log.Printf("couldn't create invoice #%d: %s", i+1, err)
continue
}
log.Printf("invoice #%d created with number %s", i+1, invoice.Number)
}
return nil
},
Flags: []cli.Flag{
&cli.PathFlag{
Name: "file",
Aliases: []string{"f"},
Usage: "Path to .json file with invoice params",
Required: true,
},
},
},
},
}
}