206 lines
5.9 KiB
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
|
|
}
|