parent
4aec517c74
commit
c51fe15e77
|
@ -0,0 +1,2 @@
|
|||
extends:
|
||||
- "@commitlint/config-conventional"
|
|
@ -23,3 +23,5 @@ go.work
|
|||
|
||||
.idea
|
||||
bin
|
||||
|
||||
invoices.json
|
||||
|
|
|
@ -58,7 +58,7 @@ linters-settings:
|
|||
tagliatelle:
|
||||
case:
|
||||
rules:
|
||||
json: camel
|
||||
json: snake
|
||||
lll:
|
||||
line-length: 150
|
||||
gocyclo:
|
||||
|
|
|
@ -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
5
go.mod
|
@ -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
2
go.sum
|
@ -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=
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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, ¶ms); 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,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue