infakt-cli/internal/infakt/client.go

206 lines
5.9 KiB
Go

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
reqPerMinute = 150
)
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
}
const endpointCreateInvoice = "/api/v3/invoices.json"
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
}
const endpointGetInvoicePDF = "/api/v3/invoices/%s/pdf.json"
func (c *Client) GetInvoicePDF(ctx context.Context, invoiceUUID, documentType, locale string, w io.Writer) error {
values := urlpkg.Values{
"document_type": []string{documentType},
}
if locale != "" {
values.Set("locale", locale)
}
req, err := http.NewRequestWithContext(ctx, http.MethodGet, fmt.Sprintf(endpointGetInvoicePDF, invoiceUUID)+"?"+values.Encode(), nil)
if err != nil {
return err
}
resp, err := c.do(req)
if err != nil {
return err
}
defer func() {
_, _ = io.Copy(io.Discard, resp.Body)
_ = resp.Body.Close()
}()
if _, err = io.Copy(w, resp.Body); err != nil {
return err
}
return 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)
}
const (
maxErrBodySizeBytes = 250000
//nolint:gosec
apiKeyHeader = "X-inFakt-ApiKey"
)
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(io.LimitReader(resp.Body, maxErrBodySizeBytes))
_, _ = 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
}