Skip to content
82 changes: 82 additions & 0 deletions shared/management/client/rest/billing.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
package rest

import (
"context"

"github.com/netbirdio/netbird/shared/management/http/api"
)

// BillingAPI APIs for billing and invoices
type BillingAPI struct {
c *Client
}

// GetUsage retrieves current usage statistics for the account
// See more: https://docs.netbird.io/api/resources/billing#get-current-usage
func (a *BillingAPI) GetUsage(ctx context.Context) (*api.UsageStats, error) {
resp, err := a.c.NewRequest(ctx, "GET", "/api/integrations/billing/usage", nil, nil)
if err != nil {
return nil, err
}
if resp.Body != nil {
defer resp.Body.Close()
}
ret, err := parseResponse[api.UsageStats](resp)
return &ret, err
}

// GetSubscription retrieves the current subscription details
// See more: https://docs.netbird.io/api/resources/billing#get-current-subscription
func (a *BillingAPI) GetSubscription(ctx context.Context) (*api.Subscription, error) {
resp, err := a.c.NewRequest(ctx, "GET", "/api/integrations/billing/subscription", nil, nil)
if err != nil {
return nil, err
}
if resp.Body != nil {
defer resp.Body.Close()
}
ret, err := parseResponse[api.Subscription](resp)
return &ret, err
}

// GetInvoices retrieves the account's paid invoices
// See more: https://docs.netbird.io/api/resources/billing#list-all-invoices
func (a *BillingAPI) GetInvoices(ctx context.Context) ([]api.InvoiceResponse, error) {
resp, err := a.c.NewRequest(ctx, "GET", "/api/integrations/billing/invoices", nil, nil)
if err != nil {
return nil, err
}
if resp.Body != nil {
defer resp.Body.Close()
}
ret, err := parseResponse[[]api.InvoiceResponse](resp)
return ret, err
}

// GetInvoicePDF retrieves the invoice PDF URL
// See more: https://docs.netbird.io/api/resources/billing#get-invoice-pdf
func (a *BillingAPI) GetInvoicePDF(ctx context.Context, invoiceID string) (*api.InvoicePDFResponse, error) {
resp, err := a.c.NewRequest(ctx, "GET", "/api/integrations/billing/invoices/"+invoiceID+"/pdf", nil, nil)
if err != nil {
return nil, err
}
if resp.Body != nil {
defer resp.Body.Close()
}
ret, err := parseResponse[api.InvoicePDFResponse](resp)
return &ret, err
}

// GetInvoiceCSV retrieves the invoice CSV content
// See more: https://docs.netbird.io/api/resources/billing#get-invoice-csv
func (a *BillingAPI) GetInvoiceCSV(ctx context.Context, invoiceID string) (string, error) {
resp, err := a.c.NewRequest(ctx, "GET", "/api/integrations/billing/invoices/"+invoiceID+"/csv", nil, nil)
if err != nil {
return "", err
}
if resp.Body != nil {
defer resp.Body.Close()
}
ret, err := parseResponse[string](resp)
return ret, err
}
194 changes: 194 additions & 0 deletions shared/management/client/rest/billing_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
//go:build integration

package rest_test

import (
"context"
"encoding/json"
"net/http"
"testing"
"time"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"github.com/netbirdio/netbird/shared/management/client/rest"
"github.com/netbirdio/netbird/shared/management/http/api"
"github.com/netbirdio/netbird/shared/management/http/util"
)

var (
testUsageStats = api.UsageStats{
ActiveUsers: 15,
TotalUsers: 20,
ActivePeers: 10,
TotalPeers: 25,
}

testSubscription = api.Subscription{
Active: true,
PlanTier: "basic",
PriceId: "price_1HhxOp",
Currency: "USD",
Price: 1000,
Provider: "stripe",
UpdatedAt: time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC),
}

testInvoice = api.InvoiceResponse{
Id: "inv_123",
PeriodStart: time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC),
PeriodEnd: time.Date(2024, 2, 1, 0, 0, 0, 0, time.UTC),
Type: "invoice",
}

testInvoicePDF = api.InvoicePDFResponse{
Url: "https://example.com/invoice.pdf",
}
)

func TestBilling_GetUsage_200(t *testing.T) {
withMockClient(func(c *rest.Client, mux *http.ServeMux) {
mux.HandleFunc("/api/integrations/billing/usage", func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, "GET", r.Method)
retBytes, _ := json.Marshal(testUsageStats)
_, err := w.Write(retBytes)
require.NoError(t, err)
})
ret, err := c.Billing.GetUsage(context.Background())
require.NoError(t, err)
assert.Equal(t, testUsageStats, *ret)
})
}

func TestBilling_GetUsage_Err(t *testing.T) {
withMockClient(func(c *rest.Client, mux *http.ServeMux) {
mux.HandleFunc("/api/integrations/billing/usage", func(w http.ResponseWriter, r *http.Request) {
retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400})
w.WriteHeader(400)
_, err := w.Write(retBytes)
require.NoError(t, err)
})
ret, err := c.Billing.GetUsage(context.Background())
assert.Error(t, err)
assert.Equal(t, "No", err.Error())
assert.Nil(t, ret)
})
}

func TestBilling_GetSubscription_200(t *testing.T) {
withMockClient(func(c *rest.Client, mux *http.ServeMux) {
mux.HandleFunc("/api/integrations/billing/subscription", func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, "GET", r.Method)
retBytes, _ := json.Marshal(testSubscription)
_, err := w.Write(retBytes)
require.NoError(t, err)
})
ret, err := c.Billing.GetSubscription(context.Background())
require.NoError(t, err)
assert.Equal(t, testSubscription, *ret)
})
}

func TestBilling_GetSubscription_Err(t *testing.T) {
withMockClient(func(c *rest.Client, mux *http.ServeMux) {
mux.HandleFunc("/api/integrations/billing/subscription", func(w http.ResponseWriter, r *http.Request) {
retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400})
w.WriteHeader(400)
_, err := w.Write(retBytes)
require.NoError(t, err)
})
ret, err := c.Billing.GetSubscription(context.Background())
assert.Error(t, err)
assert.Equal(t, "No", err.Error())
assert.Nil(t, ret)
})
}

func TestBilling_GetInvoices_200(t *testing.T) {
withMockClient(func(c *rest.Client, mux *http.ServeMux) {
mux.HandleFunc("/api/integrations/billing/invoices", func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, "GET", r.Method)
retBytes, _ := json.Marshal([]api.InvoiceResponse{testInvoice})
_, err := w.Write(retBytes)
require.NoError(t, err)
})
ret, err := c.Billing.GetInvoices(context.Background())
require.NoError(t, err)
assert.Len(t, ret, 1)
assert.Equal(t, testInvoice, ret[0])
})
}

func TestBilling_GetInvoices_Err(t *testing.T) {
withMockClient(func(c *rest.Client, mux *http.ServeMux) {
mux.HandleFunc("/api/integrations/billing/invoices", func(w http.ResponseWriter, r *http.Request) {
retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400})
w.WriteHeader(400)
_, err := w.Write(retBytes)
require.NoError(t, err)
})
ret, err := c.Billing.GetInvoices(context.Background())
assert.Error(t, err)
assert.Equal(t, "No", err.Error())
assert.Empty(t, ret)
})
}

func TestBilling_GetInvoicePDF_200(t *testing.T) {
withMockClient(func(c *rest.Client, mux *http.ServeMux) {
mux.HandleFunc("/api/integrations/billing/invoices/inv_123/pdf", func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, "GET", r.Method)
retBytes, _ := json.Marshal(testInvoicePDF)
_, err := w.Write(retBytes)
require.NoError(t, err)
})
ret, err := c.Billing.GetInvoicePDF(context.Background(), "inv_123")
require.NoError(t, err)
assert.Equal(t, testInvoicePDF, *ret)
})
}

func TestBilling_GetInvoicePDF_Err(t *testing.T) {
withMockClient(func(c *rest.Client, mux *http.ServeMux) {
mux.HandleFunc("/api/integrations/billing/invoices/inv_123/pdf", func(w http.ResponseWriter, r *http.Request) {
retBytes, _ := json.Marshal(util.ErrorResponse{Message: "Not found", Code: 404})
w.WriteHeader(404)
_, err := w.Write(retBytes)
require.NoError(t, err)
})
ret, err := c.Billing.GetInvoicePDF(context.Background(), "inv_123")
assert.Error(t, err)
assert.Equal(t, "Not found", err.Error())
assert.Nil(t, ret)
})
}

func TestBilling_GetInvoiceCSV_200(t *testing.T) {
withMockClient(func(c *rest.Client, mux *http.ServeMux) {
mux.HandleFunc("/api/integrations/billing/invoices/inv_123/csv", func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, "GET", r.Method)
retBytes, _ := json.Marshal("col1,col2\nval1,val2")
_, err := w.Write(retBytes)
require.NoError(t, err)
})
ret, err := c.Billing.GetInvoiceCSV(context.Background(), "inv_123")
require.NoError(t, err)
assert.Equal(t, "col1,col2\nval1,val2", ret)
})
}

func TestBilling_GetInvoiceCSV_Err(t *testing.T) {
withMockClient(func(c *rest.Client, mux *http.ServeMux) {
mux.HandleFunc("/api/integrations/billing/invoices/inv_123/csv", func(w http.ResponseWriter, r *http.Request) {
retBytes, _ := json.Marshal(util.ErrorResponse{Message: "Not found", Code: 404})
w.WriteHeader(404)
_, err := w.Write(retBytes)
require.NoError(t, err)
})
ret, err := c.Billing.GetInvoiceCSV(context.Background(), "inv_123")
assert.Error(t, err)
assert.Equal(t, "Not found", err.Error())
assert.Empty(t, ret)
})
}
40 changes: 40 additions & 0 deletions shared/management/client/rest/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,38 @@ type Client struct {
// Events NetBird Events APIs
// see more: https://docs.netbird.io/api/resources/events
Events *EventsAPI

// Billing NetBird Billing APIs for subscriptions, plans, and invoices
// see more: https://docs.netbird.io/api/resources/billing
Billing *BillingAPI

// MSP NetBird MSP tenant management APIs
// see more: https://docs.netbird.io/api/resources/msp
MSP *MSPAPI

// EDR NetBird EDR integration APIs (Intune, SentinelOne, Falcon, Huntress)
// see more: https://docs.netbird.io/api/resources/edr
EDR *EDRAPI

// SCIM NetBird SCIM IDP integration APIs
// see more: https://docs.netbird.io/api/resources/scim
SCIM *SCIMAPI

// EventStreaming NetBird Event Streaming integration APIs
// see more: https://docs.netbird.io/api/resources/event-streaming
EventStreaming *EventStreamingAPI

// IdentityProviders NetBird Identity Providers APIs
// see more: https://docs.netbird.io/api/resources/identity-providers
IdentityProviders *IdentityProvidersAPI

// Ingress NetBird Ingress Peers APIs
// see more: https://docs.netbird.io/api/resources/ingress-ports
Ingress *IngressAPI

// Instance NetBird Instance API
// see more: https://docs.netbird.io/api/resources/instance
Instance *InstanceAPI
}

// New initialize new Client instance using PAT token
Expand Down Expand Up @@ -120,6 +152,14 @@ func (c *Client) initialize() {
c.DNSZones = &DNSZonesAPI{c}
c.GeoLocation = &GeoLocationAPI{c}
c.Events = &EventsAPI{c}
c.Billing = &BillingAPI{c}
c.MSP = &MSPAPI{c}
c.EDR = &EDRAPI{c}
c.SCIM = &SCIMAPI{c}
c.EventStreaming = &EventStreamingAPI{c}
c.IdentityProviders = &IdentityProvidersAPI{c}
c.Ingress = &IngressAPI{c}
c.Instance = &InstanceAPI{c}
}

// NewRequest creates and executes new management API request
Expand Down
Loading
Loading