Skip to content

Commit

Permalink
Merge pull request #18 from levelzerotechnology/refactor
Browse files Browse the repository at this point in the history
Major Refactor
  • Loading branch information
rwestlund authored May 8, 2024
2 parents 0c49d03 + e53208f commit 47399ba
Show file tree
Hide file tree
Showing 37 changed files with 2,129 additions and 1,492 deletions.
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

0 comments on commit 47399ba

Please sign in to comment.