Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Major Refactor #18

Merged
merged 6 commits into from
May 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ jobs:
build:
strategy:
matrix:
go-version: [1.14.x, 1.15.x]
go-version: [1.20.x, 1.21.x, 1.22.x]
os: [ubuntu-latest, macos-latest, windows-latest]
runs-on: ${{ matrix.os }}
steps:
Expand Down
41 changes: 32 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,32 +6,48 @@
quickbooks-go is a Go library that provides access to Intuit's QuickBooks
Online API.

**NOTE:** This library is very incomplete. I just implemented the minimum for my
**NOTE:** This library is incomplete. I implemented the minimum for my
use case. Pull requests welcome :)

# Example

## Authorization flow

Before you can initialize the client, you'll need to obtain an authorization code. You can see an example of this from QuickBooks' [OAuth Playground](https://developer.intuit.com/app/developer/playground).

See [_auth_flow_test.go_](./examples/auth_flow_test.go)
```go
clientId := "<your-client-id>"
clientSecret := "<your-client-secret>"
realmId := "<realm-id>"

qbClient, _ := quickbooks.NewQuickbooksClient(clientId, clientSecret, realmId, false, nil)
qbClient, err := quickbooks.NewClient(clientId, clientSecret, realmId, false, "", nil)
if err != nil {
log.Fatalln(err)
}

// To do first when you receive the authorization code from quickbooks callback
authorizationCode := "<received-from-callback>"
redirectURI := "https://developer.intuit.com/v2/OAuth2Playground/RedirectUrl"
bearerToken, _ := qbClient.RetrieveBearerToken(authorizationCode, redirectURI)

bearerToken, err := qbClient.RetrieveBearerToken(authorizationCode, redirectURI)
if err != nil {
log.Fatalln(err)
}
// Save the bearer token inside a db

// When the token expire, you can use the following function
bearerToken, _ = qbClient.RefreshToken(bearerToken.RefreshToken)
bearerToken, err = qbClient.RefreshToken(bearerToken.RefreshToken)
if err != nil {
log.Fatalln(err)
}

// Make a request!
info, _ := qbClient.FetchCompanyInfo()
info, err := qbClient.FindCompanyInfo()
if err != nil {
log.Fatalln(err)
}

fmt.Println(info)

// Revoke the token, this should be done only if a user unsubscribe from your app
Expand All @@ -47,14 +63,21 @@ clientSecret := "<your-client-secret>"
realmId := "<realm-id>"

token := quickbooks.BearerToken{
RefreshToken: "<saved-refresh-token>",
AccessToken: "<saved-access-token>",
RefreshToken: "<saved-refresh-token>",
AccessToken: "<saved-access-token>",
}

qbClient, _ := quickbooks.NewQuickbooksClient(clientId, clientSecret, realmId, false, &token)
qbClient, err := quickbooks.NewClient(clientId, clientSecret, realmId, false, "", &token)
if err != nil {
log.Fatalln(err)
}

// Make a request!
info, _ := qbClient.FetchCompanyInfo()
info, err := qbClient.FindCompanyInfo()
if err != nil {
log.Fatalln(err)
}

fmt.Println(info)
```

Expand Down
187 changes: 78 additions & 109 deletions account.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
package quickbooks

import (
"bytes"
"encoding/json"
"net/http"
"net/url"
"errors"
"strconv"
)

Expand All @@ -27,7 +25,7 @@ const (
)

type Account struct {
ID string `json:"Id,omitempty"`
Id string `json:"Id,omitempty"`
Name string `json:",omitempty"`
SyncToken string `json:",omitempty"`
AcctNum string `json:",omitempty"`
Expand All @@ -48,151 +46,122 @@ type Account struct {
CurrentBalance json.Number `json:",omitempty"`
}

// CreateAccount creates the account
// CreateAccount creates the given account within QuickBooks
func (c *Client) CreateAccount(account *Account) (*Account, error) {
var u, err = url.Parse(string(c.Endpoint))
if err != nil {
return nil, err
var resp struct {
Account Account
Time Date
}
u.Path = "/v3/company/" + c.RealmID + "/account"
var v = url.Values{}
v.Add("minorversion", minorVersion)
u.RawQuery = v.Encode()
var j []byte
j, err = json.Marshal(account)
if err != nil {

if err := c.post("account", account, &resp, nil); err != nil {
return nil, err
}
var req *http.Request
req, err = http.NewRequest("POST", u.String(), bytes.NewBuffer(j))
if err != nil {
return nil, err

return &resp.Account, nil
}

// FindAccounts gets the full list of Accounts in the QuickBooks account.
func (c *Client) FindAccounts() ([]Account, error) {
var resp struct {
QueryResponse struct {
Accounts []Account `json:"Account"`
MaxResults int
StartPosition int
TotalCount int
}
}
req.Header.Add("Content-Type", "application/json")
req.Header.Add("Accept", "application/json")
var res *http.Response
res, err = c.Client.Do(req)
if err != nil {

if err := c.query("SELECT COUNT(*) FROM Account", &resp); err != nil {
return nil, err
}
defer res.Body.Close()

if res.StatusCode != http.StatusOK {
return nil, parseFailure(res)
if resp.QueryResponse.TotalCount == 0 {
return nil, errors.New("no accounts could be found")
}

var r struct {
accounts := make([]Account, 0, resp.QueryResponse.TotalCount)

for i := 0; i < resp.QueryResponse.TotalCount; i += queryPageSize {
query := "SELECT * FROM Account ORDERBY Id STARTPOSITION " + strconv.Itoa(i+1) + " MAXRESULTS " + strconv.Itoa(queryPageSize)

if err := c.query(query, &resp); err != nil {
return nil, err
}

if resp.QueryResponse.Accounts == nil {
return nil, errors.New("no accounts could be found")
}

accounts = append(accounts, resp.QueryResponse.Accounts...)
}

return accounts, nil
}

// FindAccountById returns an account with a given Id.
func (c *Client) FindAccountById(id string) (*Account, error) {
var resp struct {
Account Account
Time Date
}
err = json.NewDecoder(res.Body).Decode(&r)
return &r.Account, err

if err := c.get("account/"+id, &resp, nil); err != nil {
return nil, err
}

return &resp.Account, nil
}

// QueryAccount gets the account
func (c *Client) QueryAccount(selectStatement string) ([]Account, error) {
var r struct {
// QueryAccounts accepts an SQL query and returns all accounts found using it
func (c *Client) QueryAccounts(query string) ([]Account, error) {
var resp struct {
QueryResponse struct {
Account []Account
Accounts []Account `json:"Account"`
StartPosition int
MaxResults int
}
}
err := c.query(selectStatement, &r)
if err != nil {

if err := c.query(query, &resp); err != nil {
return nil, err
}

if r.QueryResponse.Account == nil {
r.QueryResponse.Account = make([]Account, 0)
if resp.QueryResponse.Accounts == nil {
return nil, errors.New("could not find any accounts")
}
return r.QueryResponse.Account, nil
}

// GetAccounts gets the account
func (c *Client) GetAccounts(startpos int, pagesize int) ([]Account, error) {
q := "SELECT * FROM Account ORDERBY Id STARTPOSITION " +
strconv.Itoa(startpos) + " MAXRESULTS " + strconv.Itoa(pagesize)
return c.QueryAccount(q)
}

// GetAccountByID returns an account with a given ID.
func (c *Client) GetAccountByID(id string) (*Account, error) {
var u, err = url.Parse(string(c.Endpoint))
if err != nil {
return nil, err
}
u.Path = "/v3/company/" + c.RealmID + "/account/" + id
var v = url.Values{}
v.Add("minorversion", minorVersion)
u.RawQuery = v.Encode()
var req *http.Request
req, err = http.NewRequest("GET", u.String(), nil)
if err != nil {
return nil, err
}
req.Header.Add("Accept", "application/json")
var res *http.Response
res, err = c.Client.Do(req)
if err != nil {
return nil, err
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
return nil, parseFailure(res)
}
var r struct {
Account Account
Time Date
}
err = json.NewDecoder(res.Body).Decode(&r)
return &r.Account, err
return resp.QueryResponse.Accounts, nil
}

// UpdateAccount updates the account
func (c *Client) UpdateAccount(account *Account) (*Account, error) {
var u, err = url.Parse(string(c.Endpoint))
if account.Id == "" {
return nil, errors.New("missing account id")
}

existingAccount, err := c.FindAccountById(account.Id)
if err != nil {
return nil, err
}
u.Path = "/v3/company/" + c.RealmID + "/account"
var v = url.Values{}
v.Add("minorversion", minorVersion)
u.RawQuery = v.Encode()
var d = struct {

account.SyncToken = existingAccount.SyncToken

payload := struct {
*Account
Sparse bool `json:"sparse"`
}{
Account: account,
Sparse: true,
}
var j []byte
j, err = json.Marshal(d)
if err != nil {
return nil, err
}
var req *http.Request
req, err = http.NewRequest("POST", u.String(), bytes.NewBuffer(j))
if err != nil {
return nil, err
}
req.Header.Add("Content-Type", "application/json")
req.Header.Add("Accept", "application/json")
var res *http.Response
res, err = c.Client.Do(req)
if err != nil {
return nil, err
}
defer res.Body.Close()

if res.StatusCode != http.StatusOK {
return nil, parseFailure(res)
}

var r struct {
var accountData struct {
Account Account
Time Date
}
err = json.NewDecoder(res.Body).Decode(&r)
return &r.Account, err

if err = c.post("account", payload, &accountData, nil); err != nil {
return nil, err
}

return &accountData.Account, err
}
7 changes: 4 additions & 3 deletions account_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,12 @@ package quickbooks

import (
"encoding/json"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"io/ioutil"
"os"
"testing"

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

func TestAccount(t *testing.T) {
Expand Down Expand Up @@ -35,6 +36,6 @@ func TestAccount(t *testing.T) {
assert.Equal(t, json.Number("0"), r.Account.CurrentBalance)
assert.True(t, r.Account.Active)
assert.Equal(t, "0", r.Account.SyncToken)
assert.Equal(t, "94", r.Account.ID)
assert.Equal(t, "94", r.Account.Id)
assert.False(t, r.Account.SubAccount)
}
22 changes: 0 additions & 22 deletions address.go

This file was deleted.

Loading
Loading