diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 100f5d5..243d4cc 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -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: diff --git a/README.md b/README.md index 0556077..ebf1533 100644 --- a/README.md +++ b/README.md @@ -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 := "" clientSecret := "" realmId := "" -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 := "" 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 @@ -47,14 +63,21 @@ clientSecret := "" realmId := "" token := quickbooks.BearerToken{ -RefreshToken: "", -AccessToken: "", + RefreshToken: "", + AccessToken: "", } -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) ``` diff --git a/account.go b/account.go index cc3d504..067cf65 100644 --- a/account.go +++ b/account.go @@ -1,10 +1,8 @@ package quickbooks import ( - "bytes" "encoding/json" - "net/http" - "net/url" + "errors" "strconv" ) @@ -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"` @@ -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 } diff --git a/account_test.go b/account_test.go index 1faf164..34f6f10 100644 --- a/account_test.go +++ b/account_test.go @@ -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) { @@ -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) } diff --git a/address.go b/address.go deleted file mode 100644 index 7506d5d..0000000 --- a/address.go +++ /dev/null @@ -1,22 +0,0 @@ -// Copyright (c) 2018, Randy Westlund. All rights reserved. -// This code is under the BSD-2-Clause license. - -package quickbooks - -// PhysicalAddress represents a QuickBooks address. -type PhysicalAddress struct { - ID string `json:"Id,omitempty"` - // These lines are context-dependent! Read the QuickBooks API carefully. - Line1 string `json:",omitempty"` - Line2 string `json:",omitempty"` - Line3 string `json:",omitempty"` - Line4 string `json:",omitempty"` - Line5 string `json:",omitempty"` - City string `json:",omitempty"` - Country string `json:",omitempty"` - // A.K.A. State. - CountrySubDivisionCode string `json:",omitempty"` - PostalCode string `json:",omitempty"` - Lat string `json:",omitempty"` - Long string `json:",omitempty"` -} diff --git a/attachable.go b/attachable.go index 30b56cf..cba4e1c 100644 --- a/attachable.go +++ b/attachable.go @@ -3,6 +3,7 @@ package quickbooks import ( "bytes" "encoding/json" + "errors" "fmt" "io" "io/ioutil" @@ -36,7 +37,7 @@ const ( ) type Attachable struct { - ID string `json:"Id,omitempty"` + Id string `json:"Id,omitempty"` SyncToken string `json:",omitempty"` FileName string `json:",omitempty"` Note string `json:",omitempty"` @@ -64,233 +65,177 @@ type AttachableRef struct { EntityRef ReferenceType `json:",omitempty"` } -// CreateAttachable creates the attachable +// CreateAttachable creates the given Attachable on the QuickBooks server, +// returning the resulting Attachable object. func (c *Client) CreateAttachable(attachable *Attachable) (*Attachable, error) { - var u, err = url.Parse(string(c.Endpoint)) - if err != nil { - return nil, err - } - u.Path = "/v3/company/" + c.RealmID + "/attachable" - var v = url.Values{} - v.Add("minorversion", minorVersion) - u.RawQuery = v.Encode() - var j []byte - j, err = json.Marshal(attachable) - 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 + var resp struct { + Attachable Attachable + Time Date } - defer res.Body.Close() - if res.StatusCode != http.StatusOK { - return nil, parseFailure(res) + if err := c.post("attachable", attachable, &resp, nil); err != nil { + return nil, err } - var r struct { - Attachable Attachable - Time Date - } - err = json.NewDecoder(res.Body).Decode(&r) - return &r.Attachable, err + return &resp.Attachable, nil } // DeleteAttachable deletes the attachable func (c *Client) DeleteAttachable(attachable *Attachable) error { - var u, err = url.Parse(string(c.Endpoint)) - if err != nil { - return err - } - u.Path = "/v3/company/" + c.RealmID + "/attachable" - var v = url.Values{} - v.Add("minorversion", minorVersion) - v.Add("operation", "delete") - u.RawQuery = v.Encode() - var j []byte - j, err = json.Marshal(attachable) - if err != nil { - return err - } - var req *http.Request - req, err = http.NewRequest("POST", u.String(), bytes.NewBuffer(j)) - if err != nil { - return err + if attachable.Id == "" || attachable.SyncToken == "" { + return errors.New("missing id/sync token") } - 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 err - } - defer res.Body.Close() - if res.StatusCode != http.StatusOK { - return parseFailure(res) - } - - return nil + return c.post("attachable", attachable, nil, map[string]string{"operation": "delete"}) } // DownloadAttachable downloads the attachable -func (c *Client) DownloadAttachable(attachableId string) (string, error) { +func (c *Client) DownloadAttachable(id string) (string, error) { + endpointUrl := *c.endpoint + endpointUrl.Path += "download/" + id - var u, err = url.Parse(string(c.Endpoint)) - if err != nil { - return "", err - } - u.Path = "/v3/company/" + c.RealmID + "/download/" + attachableId - var v = url.Values{} - v.Add("minorversion", minorVersion) - u.RawQuery = v.Encode() - var req *http.Request - req, err = http.NewRequest("GET", u.String(), nil) + urlValues := url.Values{} + urlValues.Add("minorversion", c.minorVersion) + endpointUrl.RawQuery = urlValues.Encode() + + req, err := http.NewRequest("GET", endpointUrl.String(), nil) if err != nil { return "", err } - var res *http.Response - res, err = c.Client.Do(req) + + resp, err := c.Client.Do(req) if err != nil { return "", err } - defer res.Body.Close() - if res.StatusCode != http.StatusOK { - return "", parseFailure(res) + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return "", parseFailure(resp) } - url, err := ioutil.ReadAll(res.Body) + + downloadUrl, err := ioutil.ReadAll(resp.Body) if err != nil { return "", err } - return string(url), err -} -// GetAttachables gets the attachables -func (c *Client) GetAttachables(startpos int) ([]Attachable, error) { + return string(downloadUrl), err +} - var r struct { +// FindAttachables gets the full list of Attachables in the QuickBooks attachable. +func (c *Client) FindAttachables() ([]Attachable, error) { + var resp struct { QueryResponse struct { - Attachable []Attachable - StartPosition int + Attachables []Attachable `json:"Attachable"` MaxResults int + StartPosition int + TotalCount int } } - q := "SELECT * FROM Attachable ORDERBY Id STARTPOSITION " + - strconv.Itoa(startpos) + " MAXRESULTS " + strconv.Itoa(queryPageSize) - err := c.query(q, &r) - if err != nil { + + if err := c.query("SELECT COUNT(*) FROM Attachable", &resp); err != nil { return nil, err } - if r.QueryResponse.Attachable == nil { - r.QueryResponse.Attachable = make([]Attachable, 0) + if resp.QueryResponse.TotalCount == 0 { + return nil, errors.New("no attachables could be found") } - return r.QueryResponse.Attachable, nil + + attachables := make([]Attachable, 0, resp.QueryResponse.TotalCount) + + for i := 0; i < resp.QueryResponse.TotalCount; i += queryPageSize { + query := "SELECT * FROM Attachable 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.Attachables == nil { + return nil, errors.New("no attachables could be found") + } + + attachables = append(attachables, resp.QueryResponse.Attachables...) + } + + return attachables, nil } -// GetAttachable gets the attachable -func (c *Client) GetAttachable(attachableId string) (*Attachable, error) { - var u, err = url.Parse(string(c.Endpoint)) - if err != nil { - return nil, err +// FindAttachableById finds the attachable by the given id +func (c *Client) FindAttachableById(id string) (*Attachable, error) { + var resp struct { + Attachable Attachable + Time Date } - u.Path = "/v3/company/" + c.RealmID + "/attachable/" + attachableId - 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 { + + if err := c.get("attachable/"+id, &resp, nil); 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 + + return &resp.Attachable, nil +} + +// QueryAttachables accepts an SQL query and returns all attachables found using it +func (c *Client) QueryAttachables(query string) ([]Attachable, error) { + var resp struct { + QueryResponse struct { + Attachables []Attachable `json:"Attachable"` + StartPosition int + MaxResults int + } } - defer res.Body.Close() - if res.StatusCode != http.StatusOK { - return nil, parseFailure(res) + if err := c.query(query, &resp); err != nil { + return nil, err } - var r struct { - Attachable Attachable - Time Date + if resp.QueryResponse.Attachables == nil { + return nil, errors.New("could not find any attachables") } - err = json.NewDecoder(res.Body).Decode(&r) - return &r.Attachable, err + + return resp.QueryResponse.Attachables, nil } // UpdateAttachable updates the attachable func (c *Client) UpdateAttachable(attachable *Attachable) (*Attachable, error) { - var u, err = url.Parse(string(c.Endpoint)) + if attachable.Id == "" { + return nil, errors.New("missing attachable id") + } + + existingAttachable, err := c.FindAttachableById(attachable.Id) if err != nil { return nil, err } - u.Path = "/v3/company/" + c.RealmID + "/attachable" - var v = url.Values{} - v.Add("minorversion", minorVersion) - u.RawQuery = v.Encode() - var d = struct { + + attachable.SyncToken = existingAttachable.SyncToken + + payload := struct { *Attachable Sparse bool `json:"sparse"` }{ Attachable: attachable, 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 attachableData struct { Attachable Attachable Time Date } - err = json.NewDecoder(res.Body).Decode(&r) - return &r.Attachable, err + + if err = c.post("attachable", payload, &attachableData, nil); err != nil { + return nil, err + } + + return &attachableData.Attachable, err } // UploadAttachable uploads the attachable func (c *Client) UploadAttachable(attachable *Attachable, data io.Reader) (*Attachable, error) { - var u, err = url.Parse(string(c.Endpoint)) - if err != nil { - return nil, err - } - u.Path = "/v3/company/" + c.RealmID + "/upload" - var v = url.Values{} - v.Add("minorversion", minorVersion) - u.RawQuery = v.Encode() + endpointUrl := *c.endpoint + endpointUrl.Path += "upload" + + urlValues := url.Values{} + urlValues.Add("minorversion", c.minorVersion) + endpointUrl.RawQuery = urlValues.Encode() var buffer bytes.Buffer mWriter := multipart.NewWriter(&buffer) @@ -299,15 +244,17 @@ func (c *Client) UploadAttachable(attachable *Attachable, data io.Reader) (*Atta metadataHeader := make(textproto.MIMEHeader) metadataHeader.Set("Content-Disposition", fmt.Sprintf(`form-data; name="%s"; filename="%s"`, "file_metadata_01", "attachment.json")) metadataHeader.Set("Content-Type", "application/json") - var metadataContent io.Writer - if metadataContent, err = mWriter.CreatePart(metadataHeader); err != nil { + + metadataContent, err := mWriter.CreatePart(metadataHeader) + if err != nil { return nil, err } - var j []byte - j, err = json.Marshal(attachable) + + j, err := json.Marshal(attachable) if err != nil { return nil, err } + if _, err = metadataContent.Write(j); err != nil { return nil, err } @@ -316,18 +263,19 @@ func (c *Client) UploadAttachable(attachable *Attachable, data io.Reader) (*Atta fileHeader := make(textproto.MIMEHeader) fileHeader.Set("Content-Disposition", fmt.Sprintf(`form-data; name="%s"; filename="%s"`, "file_content_01", attachable.FileName)) fileHeader.Set("Content-Type", string(attachable.ContentType)) - var fileContent io.Writer - if fileContent, err = mWriter.CreatePart(fileHeader); err != nil { + + fileContent, err := mWriter.CreatePart(fileHeader) + if err != nil { return nil, err } + if _, err = io.Copy(fileContent, data); err != nil { return nil, err } mWriter.Close() - var req *http.Request - req, err = http.NewRequest("POST", u.String(), &buffer) + req, err := http.NewRequest("POST", endpointUrl.String(), &buffer) if err != nil { return nil, err } @@ -335,15 +283,15 @@ func (c *Client) UploadAttachable(attachable *Attachable, data io.Reader) (*Atta req.Header.Add("Content-Type", mWriter.FormDataContentType()) req.Header.Add("Accept", "application/json") - var res *http.Response - res, err = c.Client.Do(req) + resp, err := c.Client.Do(req) if err != nil { return nil, err } - defer res.Body.Close() - if res.StatusCode != http.StatusOK { - return nil, parseFailure(res) + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, parseFailure(resp) } var r struct { @@ -352,6 +300,10 @@ func (c *Client) UploadAttachable(attachable *Attachable, data io.Reader) (*Atta } Time Date } - err = json.NewDecoder(res.Body).Decode(&r) - return &r.AttachableResponse[0].Attachable, err + + if err = json.NewDecoder(resp.Body).Decode(&r); err != nil { + return nil, err + } + + return &r.AttachableResponse[0].Attachable, nil } diff --git a/attachable_test.go b/attachable_test.go index 731220a..2fc4037 100644 --- a/attachable_test.go +++ b/attachable_test.go @@ -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 TestAttachable(t *testing.T) { @@ -14,21 +15,21 @@ func TestAttachable(t *testing.T) { require.NoError(t, err) defer jsonFile.Close() - byteValue, _ := ioutil.ReadAll(jsonFile) + byteValue, err := ioutil.ReadAll(jsonFile) require.NoError(t, err) var r struct { Attachable Attachable Time Date } - err = json.Unmarshal(byteValue, &r) - require.NoError(t, err) + + require.NoError(t, json.Unmarshal(byteValue, &r)) assert.Equal(t, "0", r.Attachable.SyncToken) assert.False(t, r.Attachable.AttachableRef[0].IncludeOnSend) assert.Equal(t, "95", r.Attachable.AttachableRef[0].EntityRef.Value) assert.Equal(t, "This is an attached note.", r.Attachable.Note) - assert.Equal(t, "200900000000000008541", r.Attachable.ID) + assert.Equal(t, "200900000000000008541", r.Attachable.Id) assert.Equal(t, "2015-11-17T11:05:15-08:00", r.Attachable.MetaData.CreateTime.String()) assert.Equal(t, "2015-11-17T11:05:15-08:00", r.Attachable.MetaData.LastUpdatedTime.String()) } diff --git a/bill.go b/bill.go index c764a0c..51605e4 100644 --- a/bill.go +++ b/bill.go @@ -1,14 +1,13 @@ package quickbooks import ( - "bytes" "encoding/json" - "net/http" - "net/url" + "errors" + "strconv" ) type Bill struct { - ID string `json:"Id,omitempty"` + Id string `json:"Id,omitempty"` VendorRef ReferenceType `json:",omitempty"` Line []Line SyncToken string `json:",omitempty"` @@ -16,8 +15,8 @@ type Bill struct { TxnDate Date `json:",omitempty"` APAccountRef ReferenceType `json:",omitempty"` SalesTermRef ReferenceType `json:",omitempty"` - //LinkedTxn - //GlobalTaxCalculation + LinkedTxn []LinkedTxn `json:",omitempty"` + // GlobalTaxCalculation TotalAmt json.Number `json:",omitempty"` TransactionLocationType string `json:",omitempty"` DueDate Date `json:",omitempty"` @@ -33,42 +32,132 @@ type Bill struct { Balance json.Number `json:",omitempty"` } +// CreateBill creates the given Bill on the QuickBooks server, returning +// the resulting Bill object. func (c *Client) CreateBill(bill *Bill) (*Bill, error) { - var u, err = url.Parse(string(c.Endpoint)) - if err != nil { + var resp struct { + Bill Bill + Time Date + } + + if err := c.post("bill", bill, &resp, nil); err != nil { return nil, err } - u.Path = "/v3/company/" + c.RealmID + "/bill" - var v = url.Values{} - v.Add("minorversion", "55") - u.RawQuery = v.Encode() - var j []byte - j, err = json.Marshal(bill) - if err != nil { + + return &resp.Bill, nil +} + +// DeleteBill deletes the bill +func (c *Client) DeleteBill(bill *Bill) error { + if bill.Id == "" || bill.SyncToken == "" { + return errors.New("missing id/sync token") + } + + return c.post("bill", bill, nil, map[string]string{"operation": "delete"}) +} + +// FindBills gets the full list of Bills in the QuickBooks account. +func (c *Client) FindBills() ([]Bill, error) { + var resp struct { + QueryResponse struct { + Bills []Bill `json:"Bill"` + MaxResults int + StartPosition int + TotalCount int + } + } + + if err := c.query("SELECT COUNT(*) FROM Bill", &resp); err != nil { return nil, err } - var req *http.Request - req, err = http.NewRequest("POST", u.String(), bytes.NewBuffer(j)) - if err != nil { + + if resp.QueryResponse.TotalCount == 0 { + return nil, errors.New("no bills could be found") + } + + bills := make([]Bill, 0, resp.QueryResponse.TotalCount) + + for i := 0; i < resp.QueryResponse.TotalCount; i += queryPageSize { + query := "SELECT * FROM Bill 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.Bills == nil { + return nil, errors.New("no bills could be found") + } + + bills = append(bills, resp.QueryResponse.Bills...) + } + + return bills, nil +} + +// FindBillById finds the bill by the given id +func (c *Client) FindBillById(id string) (*Bill, error) { + var resp struct { + Bill Bill + Time Date + } + + if err := c.get("bill/"+id, &resp, nil); err != nil { + return nil, err + } + + return &resp.Bill, nil +} + +// QueryBills accepts an SQL query and returns all bills found using it +func (c *Client) QueryBills(query string) ([]Bill, error) { + var resp struct { + QueryResponse struct { + Bills []Bill `json:"Bill"` + StartPosition int + MaxResults int + } + } + + if err := c.query(query, &resp); 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 resp.QueryResponse.Bills == nil { + return nil, errors.New("could not find any bills") + } + + return resp.QueryResponse.Bills, nil +} + +// UpdateBill updates the bill +func (c *Client) UpdateBill(bill *Bill) (*Bill, error) { + if bill.Id == "" { + return nil, errors.New("missing bill id") + } + + existingBill, err := c.FindBillById(bill.Id) if err != nil { return nil, err } - defer res.Body.Close() - if res.StatusCode != http.StatusOK { - return nil, parseFailure(res) + bill.SyncToken = existingBill.SyncToken + + payload := struct { + *Bill + Sparse bool `json:"sparse"` + }{ + Bill: bill, + Sparse: true, } - var r struct { + var billData struct { Bill Bill Time Date } - err = json.NewDecoder(res.Body).Decode(&r) - return &r.Bill, err + + if err = c.post("bill", payload, &billData, nil); err != nil { + return nil, err + } + + return &billData.Bill, err } diff --git a/bill_test.go b/bill_test.go index 2ade46a..b5d2e4c 100644 --- a/bill_test.go +++ b/bill_test.go @@ -46,7 +46,7 @@ func TestBill(t *testing.T) { assert.Equal(t, 1, len(r.Bill.Line)) balance, _ := r.Bill.Balance.Int64() assert.Equal(t, int64(0), balance) - assert.Equal(t, "25", r.Bill.ID) + assert.Equal(t, "25", r.Bill.Id) assert.Equal(t, "2014-11-06T15:37:25-08:00", r.Bill.MetaData.CreateTime.String()) assert.Equal(t, "2015-02-09T10:11:11-08:00", r.Bill.MetaData.LastUpdatedTime.String()) } diff --git a/client.go b/client.go index 8074b20..f5cf08e 100644 --- a/client.go +++ b/client.go @@ -1,31 +1,15 @@ // Copyright (c) 2018, Randy Westlund. All rights reserved. // This code is under the BSD-2-Clause license. - -/* -Package quickbooks provides access to Intuit's QuickBooks Online API. - -NOTE: This library is very incomplete. I just implemented the minimum for my -use case. Pull requests welcome :) - - // Do this after you go through the normal OAuth process. - var client = oauth2.NewClient(ctx, tokenSource) - - // Initialize the client handle. - var qb = quickbooks.Client{ - Client: client, - Endpoint: quickbooks.SandboxEndpoint, - RealmID: "some company account ID"' - } - - // Make a request! - var companyInfo, err = qb.FetchCompanyInfo() -*/ package quickbooks import ( + "bytes" "encoding/json" + "errors" + "fmt" "net/http" "net/url" + "time" ) // Client is your handle to the QuickBooks API. @@ -33,99 +17,164 @@ type Client struct { // Get this from oauth2.NewClient(). Client *http.Client // Set to ProductionEndpoint or SandboxEndpoint. - Endpoint EndpointURL + endpoint *url.URL // The set of quickbooks APIs discoveryAPI *DiscoveryAPI - // The client ID + // The client Id clientId string // The client Secret clientSecret string - // The account ID you're connecting to. - RealmID string + // The minor version of the QB API + minorVersion string + // The account Id you're connecting to. + realmId string + // Flag set if the limit of 500req/s has been hit (source: https://developer.intuit.com/app/developer/qbo/docs/learn/rest-api-features#limits-and-throttles) + throttled bool } -func NewQuickbooksClient(clientId string, clientSecret string, realmID string, isProduction bool, token *BearerToken) (c *Client, err error) { - var client Client - client.clientId = clientId - client.clientSecret = clientSecret - client.RealmID = realmID +// NewClient initializes a new QuickBooks client for interacting with their Online API +func NewClient(clientId string, clientSecret string, realmId string, isProduction bool, minorVersion string, token *BearerToken) (c *Client, err error) { + if minorVersion == "" { + minorVersion = "65" + } + + client := Client{ + clientId: clientId, + clientSecret: clientSecret, + minorVersion: minorVersion, + realmId: realmId, + throttled: false, + } + if isProduction { - client.Endpoint = ProductionEndpoint - client.discoveryAPI = CallDiscoveryAPI(DiscoveryProductionEndpoint) + client.endpoint, err = url.Parse(ProductionEndpoint.String() + "/v3/company/" + realmId + "/") + if err != nil { + return nil, fmt.Errorf("failed to parse API endpoint: %v", err) + } + + client.discoveryAPI, err = CallDiscoveryAPI(DiscoveryProductionEndpoint) + if err != nil { + return nil, fmt.Errorf("failed to obtain discovery endpoint: %v", err) + } } else { - client.Endpoint = SandboxEndpoint - client.discoveryAPI = CallDiscoveryAPI(DiscoverySandboxEndpoint) + client.endpoint, err = url.Parse(SandboxEndpoint.String() + "/v3/company/" + realmId + "/") + if err != nil { + return nil, fmt.Errorf("failed to parse API endpoint: %v", err) + } + + client.discoveryAPI, err = CallDiscoveryAPI(DiscoverySandboxEndpoint) + if err != nil { + return nil, fmt.Errorf("failed to obtain discovery endpoint: %v", err) + } } + if token != nil { client.Client = getHttpClient(token) } + return &client, nil } -// FetchCompanyInfo returns the QuickBooks CompanyInfo object. This is a good -// test to check whether you're connected. -func (c *Client) FetchCompanyInfo() (*CompanyInfo, error) { - var u, err = url.Parse(string(c.Endpoint)) - if err != nil { - return nil, err - } - u.Path = "/v3/company/" + c.RealmID + "/companyinfo/" + c.RealmID - 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) +// FindAuthorizationUrl compiles the authorization url from the discovery api's auth endpoint. +// +// Example: qbClient.FindAuthorizationUrl("com.intuit.quickbooks.accounting", "security_token", "https://developer.intuit.com/v2/OAuth2Playground/RedirectUrl") +// +// You can find live examples from https://developer.intuit.com/app/developer/playground +func (c *Client) FindAuthorizationUrl(scope string, state string, redirectUri string) (string, error) { + var authorizationUrl *url.URL + + authorizationUrl, err := url.Parse(c.discoveryAPI.AuthorizationEndpoint) if err != nil { - return nil, err + return "", fmt.Errorf("failed to parse auth endpoint: %v", err) } - defer res.Body.Close() - if res.StatusCode != http.StatusOK { - return nil, parseFailure(res) + urlValues := url.Values{} + urlValues.Add("client_id", c.clientId) + urlValues.Add("response_type", "code") + urlValues.Add("scope", scope) + urlValues.Add("redirect_uri", redirectUri) + urlValues.Add("state", state) + authorizationUrl.RawQuery = urlValues.Encode() + + return authorizationUrl.String(), nil +} + +func (c *Client) req(method string, endpoint string, payloadData interface{}, responseObject interface{}, queryParameters map[string]string) error { + // TODO: possibly just wait until c.throttled is false, and continue the request? + if c.throttled { + return errors.New("waiting for rate limit") } - var r struct { - CompanyInfo CompanyInfo - Time Date + endpointUrl := *c.endpoint + endpointUrl.Path += endpoint + urlValues := url.Values{} + + if len(queryParameters) > 0 { + for param, value := range queryParameters { + urlValues.Add(param, value) + } } - err = json.NewDecoder(res.Body).Decode(&r) - return &r.CompanyInfo, err -} -// query makes the specified QBO `query` and unmarshals the result into `out` -func (c *Client) query(query string, out interface{}) error { - var u, err = url.Parse(string(c.Endpoint)) - if err != nil { - return err + urlValues.Set("minorversion", c.minorVersion) + urlValues.Encode() + endpointUrl.RawQuery = urlValues.Encode() + + var err error + var marshalledJson []byte + + if payloadData != nil { + marshalledJson, err = json.Marshal(payloadData) + if err != nil { + return fmt.Errorf("failed to marshal payload: %v", err) + } } - u.Path = "/v3/company/" + c.RealmID + "/query" - - var v = url.Values{} - v.Add("minorversion", minorVersion) - v.Add("query", query) - u.RawQuery = v.Encode() - var req *http.Request - req, err = http.NewRequest("GET", u.String(), nil) + + req, err := http.NewRequest(method, endpointUrl.String(), bytes.NewBuffer(marshalledJson)) if err != nil { - return err + return fmt.Errorf("failed to create request: %v", err) } + req.Header.Add("Accept", "application/json") - var res *http.Response - res, err = c.Client.Do(req) + req.Header.Add("Content-Type", "application/json") + + resp, err := c.Client.Do(req) if err != nil { - return err + return fmt.Errorf("failed to make request: %v", err) } - defer res.Body.Close() - if res.StatusCode != http.StatusOK { - return parseFailure(res) + defer resp.Body.Close() + + switch resp.StatusCode { + case http.StatusOK: + break + case http.StatusTooManyRequests: + c.throttled = true + go func(c *Client) { + time.Sleep(1 * time.Minute) + c.throttled = false + }(c) + default: + return parseFailure(resp) + } + + if responseObject != nil { + if err = json.NewDecoder(resp.Body).Decode(&responseObject); err != nil { + return fmt.Errorf("failed to unmarshal response into object: %v", err) + } } - return json.NewDecoder(res.Body).Decode(out) + return nil +} + +func (c *Client) get(endpoint string, responseObject interface{}, queryParameters map[string]string) error { + return c.req("GET", endpoint, nil, responseObject, queryParameters) +} + +func (c *Client) post(endpoint string, payloadData interface{}, responseObject interface{}, queryParameters map[string]string) error { + return c.req("POST", endpoint, payloadData, responseObject, queryParameters) +} + +// query makes the specified QBO `query` and unmarshals the result into `responseObject` +func (c *Client) query(query string, responseObject interface{}) error { + return c.get("query", responseObject, map[string]string{"query": query}) } diff --git a/company.go b/company.go index 4498b27..5f368a9 100644 --- a/company.go +++ b/company.go @@ -7,20 +7,65 @@ package quickbooks type CompanyInfo struct { CompanyName string LegalName string - //CompanyAddr - //CustomerCommunicationAddr - //LegalAddr - //PrimaryPhone - //CompanyStartDate Date + // CompanyAddr + // CustomerCommunicationAddr + // LegalAddr + // PrimaryPhone + // CompanyStartDate Date CompanyStartDate string FiscalYearStartMonth string Country string - //Email - //WebAddr + // Email + // WebAddr SupportedLanguages string - //NameValue + // NameValue Domain string - ID string `json:"Id"` + Id string SyncToken string Metadata MetaData `json:",omitempty"` } + +// FindCompanyInfo returns the QuickBooks CompanyInfo object. This is a good +// test to check whether you're connected. +func (c *Client) FindCompanyInfo() (*CompanyInfo, error) { + var resp struct { + CompanyInfo CompanyInfo + Time Date + } + + if err := c.get("companyinfo/"+c.realmId, &resp, nil); err != nil { + return nil, err + } + + return &resp.CompanyInfo, nil +} + +// UpdateCompanyInfo updates the company info +func (c *Client) UpdateCompanyInfo(companyInfo *CompanyInfo) (*CompanyInfo, error) { + existingCompanyInfo, err := c.FindCompanyInfo() + if err != nil { + return nil, err + } + + companyInfo.Id = existingCompanyInfo.Id + companyInfo.SyncToken = existingCompanyInfo.SyncToken + + payload := struct { + *CompanyInfo + Sparse bool `json:"sparse"` + }{ + CompanyInfo: companyInfo, + Sparse: true, + } + + var companyInfoData struct { + CompanyInfo CompanyInfo + Time Date + } + + if err = c.post("companyInfo", payload, &companyInfoData, nil); err != nil { + return nil, err + } + + return &companyInfoData.CompanyInfo, err +} diff --git a/credit_memo.go b/credit_memo.go new file mode 100644 index 0000000..8c2744a --- /dev/null +++ b/credit_memo.go @@ -0,0 +1,159 @@ +package quickbooks + +import ( + "encoding/json" + "errors" + "strconv" +) + +type CreditMemo struct { + TotalAmt float64 `json:",omitempty"` + RemainingCredit json.Number `json:",omitempty"` + Line []Line `json:",omitempty"` + ApplyTaxAfterDiscount bool `json:",omitempty"` + DocNumber string `json:",omitempty"` + TxnDate Date `json:",omitempty"` + Sparse bool `json:"sparse,omitempty"` + CustomerMemo MemoRef `json:",omitempty"` + ProjectRef ReferenceType `json:",omitempty"` + Balance json.Number `json:",omitempty"` + CustomerRef ReferenceType `json:",omitempty"` + TxnTaxDetail *TxnTaxDetail `json:",omitempty"` + SyncToken string `json:",omitempty"` + CustomField []CustomField `json:",omitempty"` + ShipAddr PhysicalAddress `json:",omitempty"` + EmailStatus string `json:",omitempty"` + BillAddr PhysicalAddress `json:",omitempty"` + MetaData MetaData `json:",omitempty"` + BillEmail EmailAddress `json:",omitempty"` + Id string `json:",omitempty"` +} + +// CreateCreditMemo creates the given CreditMemo witin QuickBooks. +func (c *Client) CreateCreditMemo(creditMemo *CreditMemo) (*CreditMemo, error) { + var resp struct { + CreditMemo CreditMemo + Time Date + } + + if err := c.post("creditmemo", creditMemo, &resp, nil); err != nil { + return nil, err + } + + return &resp.CreditMemo, nil +} + +// DeleteCreditMemo deletes the given credit memo. +func (c *Client) DeleteCreditMemo(creditMemo *CreditMemo) error { + if creditMemo.Id == "" || creditMemo.SyncToken == "" { + return errors.New("missing id/sync token") + } + + return c.post("creditmemo", creditMemo, nil, map[string]string{"operation": "delete"}) +} + +// FindCreditMemos retrieves the full list of credit memos from QuickBooks. +func (c *Client) FindCreditMemos() ([]CreditMemo, error) { + var resp struct { + QueryResponse struct { + CreditMemos []CreditMemo `json:"CreditMemo"` + MaxResults int + StartPosition int + TotalCount int + } + } + + if err := c.query("SELECT COUNT(*) FROM CreditMemo", &resp); err != nil { + return nil, err + } + + if resp.QueryResponse.TotalCount == 0 { + return nil, errors.New("no credit memos could be found") + } + + creditMemos := make([]CreditMemo, 0, resp.QueryResponse.TotalCount) + + for i := 0; i < resp.QueryResponse.TotalCount; i += queryPageSize { + query := "SELECT * FROM CreditMemo 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.CreditMemos == nil { + return nil, errors.New("no credit memos could be found") + } + + creditMemos = append(creditMemos, resp.QueryResponse.CreditMemos...) + } + + return creditMemos, nil +} + +// FindCreditMemoById retrieves the given credit memo from QuickBooks. +func (c *Client) FindCreditMemoById(id string) (*CreditMemo, error) { + var resp struct { + CreditMemo CreditMemo + Time Date + } + + if err := c.get("creditmemo/"+id, &resp, nil); err != nil { + return nil, err + } + + return &resp.CreditMemo, nil +} + +// QueryCreditMemos accepts n SQL query and returns all credit memos found using it. +func (c *Client) QueryCreditMemos(query string) ([]CreditMemo, error) { + var resp struct { + QueryResponse struct { + CreditMemos []CreditMemo `json:"CreditMemo"` + StartPosition int + MaxResults int + } + } + + if err := c.query(query, &resp); err != nil { + return nil, err + } + + if resp.QueryResponse.CreditMemos == nil { + return nil, errors.New("could not find any credit memos") + } + + return resp.QueryResponse.CreditMemos, nil +} + +// UpdateCreditMemo updates the given credit memo. +func (c *Client) UpdateCreditMemo(creditMemo *CreditMemo) (*CreditMemo, error) { + if creditMemo.Id == "" { + return nil, errors.New("missing credit memo id") + } + + existingCreditMemo, err := c.FindCreditMemoById(creditMemo.Id) + if err != nil { + return nil, err + } + + creditMemo.SyncToken = existingCreditMemo.SyncToken + + payload := struct { + *CreditMemo + Sparse bool `json:"sparse"` + }{ + CreditMemo: creditMemo, + Sparse: true, + } + + var creditMemoData struct { + CreditMemo CreditMemo + Time Date + } + + if err = c.post("creditmemo", payload, &creditMemoData, nil); err != nil { + return nil, err + } + + return &creditMemoData.CreditMemo, err +} diff --git a/customer.go b/customer.go index 375b1c1..ac01bf8 100644 --- a/customer.go +++ b/customer.go @@ -4,41 +4,40 @@ package quickbooks import ( - "bytes" "encoding/json" "errors" - "net/http" - "net/url" + "fmt" "strconv" "strings" - null "gopkg.in/guregu/null.v4" + "gopkg.in/guregu/null.v4" ) // Customer represents a QuickBooks Customer object. type Customer struct { - ID string `json:"Id,omitempty"` + Id string `json:",omitempty"` SyncToken string `json:",omitempty"` MetaData MetaData `json:",omitempty"` - Title null.String `json:",omitempty"` - GivenName null.String `json:",omitempty"` - MiddleName null.String `json:",omitempty"` - FamilyName null.String `json:",omitempty"` - Suffix null.String `json:",omitempty"` + Title string `json:",omitempty"` + GivenName string `json:",omitempty"` + MiddleName string `json:",omitempty"` + FamilyName string `json:",omitempty"` + Suffix string `json:",omitempty"` DisplayName string `json:",omitempty"` - FullyQualifiedName null.String `json:",omitempty"` - CompanyName null.String `json:",omitempty"` + FullyQualifiedName string `json:",omitempty"` + CompanyName string `json:",omitempty"` PrintOnCheckName string `json:",omitempty"` Active bool `json:",omitempty"` PrimaryPhone TelephoneNumber `json:",omitempty"` AlternatePhone TelephoneNumber `json:",omitempty"` Mobile TelephoneNumber `json:",omitempty"` Fax TelephoneNumber `json:",omitempty"` + CustomerTypeRef ReferenceType `json:",omitempty"` PrimaryEmailAddr *EmailAddress `json:",omitempty"` WebAddr *WebSiteAddress `json:",omitempty"` - //DefaultTaxCodeRef + // DefaultTaxCodeRef Taxable *bool `json:",omitempty"` - TaxExemptionReasonID *string `json:"TaxExemptionReasonId,omitempty"` + TaxExemptionReasonId *string `json:",omitempty"` BillAddr *PhysicalAddress `json:",omitempty"` ShipAddr *PhysicalAddress `json:",omitempty"` Notes string `json:",omitempty"` @@ -46,16 +45,16 @@ type Customer struct { BillWithParent bool `json:",omitempty"` ParentRef ReferenceType `json:",omitempty"` Level int `json:",omitempty"` - //SalesTermRef - //PaymentMethodRef + // SalesTermRef + // PaymentMethodRef Balance json.Number `json:",omitempty"` OpenBalanceDate Date `json:",omitempty"` BalanceWithJobs json.Number `json:",omitempty"` - //CurrencyRef + // CurrencyRef } // GetAddress prioritizes the ship address, but falls back on bill address -func (c Customer) GetAddress() PhysicalAddress { +func (c *Customer) GetAddress() PhysicalAddress { if c.ShipAddr != nil { return *c.ShipAddr } @@ -66,7 +65,7 @@ func (c Customer) GetAddress() PhysicalAddress { } // GetWebsite de-nests the Website object -func (c Customer) GetWebsite() string { +func (c *Customer) GetWebsite() string { if c.WebAddr != nil { return c.WebAddr.URI } @@ -74,213 +73,154 @@ func (c Customer) GetWebsite() string { } // GetPrimaryEmail de-nests the PrimaryEmailAddr object -func (c Customer) GetPrimaryEmail() string { +func (c *Customer) GetPrimaryEmail() string { if c.PrimaryEmailAddr != nil { return c.PrimaryEmailAddr.Address } return "" } -// QueryCustomerByName gets a customer with a given name. -func (c *Client) QueryCustomerByName(name string) (*Customer, error) { - - var r struct { - QueryResponse struct { - Customer []Customer - TotalCount int - } +// CreateCustomer creates the given Customer on the QuickBooks server, +// returning the resulting Customer object. +func (c *Client) CreateCustomer(customer *Customer) (*Customer, error) { + var resp struct { + Customer Customer + Time Date } - err := c.query("SELECT * FROM Customer WHERE DisplayName = '"+ - strings.Replace(name, "'", "''", -1)+"'", &r) - if err != nil { + + if err := c.post("customer", customer, &resp, nil); err != nil { return nil, err } - // var customers = make([]Customer, 0, r.QueryResponse.TotalCount) - // for i := 0; i < r.QueryResponse.TotalCount; i += queryPageSize { - // var page, err = c.fetchCustomerPage(i + 1) - // if err != nil { - // return nil, err - // } - // customers = append(customers, page...) - // } - return &r.QueryResponse.Customer[0], nil + return &resp.Customer, nil } -// FetchCustomers gets the full list of Customers in the QuickBooks account. -func (c *Client) FetchCustomers() ([]Customer, error) { - - // See how many customers there are. - var r struct { +// FindCustomers gets the full list of Customers in the QuickBooks account. +func (c *Client) FindCustomers() ([]Customer, error) { + var resp struct { QueryResponse struct { - TotalCount int + Customers []Customer `json:"Customer"` + MaxResults int + StartPosition int + TotalCount int } } - err := c.query("SELECT COUNT(*) FROM Customer", &r) - if err != nil { + + if err := c.query("SELECT COUNT(*) FROM Customer", &resp); err != nil { return nil, err } - if r.QueryResponse.TotalCount == 0 { - return make([]Customer, 0), nil + if resp.QueryResponse.TotalCount == 0 { + return nil, errors.New("no customers could be found") } - var customers = make([]Customer, 0, r.QueryResponse.TotalCount) - for i := 0; i < r.QueryResponse.TotalCount; i += queryPageSize { - var page, err = c.fetchCustomerPage(i + 1) - if err != nil { + customers := make([]Customer, 0, resp.QueryResponse.TotalCount) + + for i := 0; i < resp.QueryResponse.TotalCount; i += queryPageSize { + query := "SELECT * FROM Customer ORDERBY Id STARTPOSITION " + strconv.Itoa(i+1) + " MAXRESULTS " + strconv.Itoa(queryPageSize) + + if err := c.query(query, &resp); err != nil { return nil, err } - customers = append(customers, page...) + + if resp.QueryResponse.Customers == nil { + return nil, errors.New("no customers could be found") + } + + customers = append(customers, resp.QueryResponse.Customers...) } + return customers, nil } -// Fetch one page of results, because we can't get them all in one query. -func (c *Client) fetchCustomerPage(startpos int) ([]Customer, error) { - +// FindCustomerById returns a customer with a given Id. +func (c *Client) FindCustomerById(id string) (*Customer, error) { var r struct { - QueryResponse struct { - Customer []Customer - StartPosition int - MaxResults int - } + Customer Customer + Time Date } - q := "SELECT * FROM Customer ORDERBY Id STARTPOSITION " + - strconv.Itoa(startpos) + " MAXRESULTS " + strconv.Itoa(queryPageSize) - err := c.query(q, &r) - if err != nil { + + if err := c.get("customer/"+id, &r, nil); err != nil { return nil, err } - // Make sure we don't return nil if there are no customers. - if r.QueryResponse.Customer == nil { - r.QueryResponse.Customer = make([]Customer, 0) - } - return r.QueryResponse.Customer, nil + return &r.Customer, nil } -// FetchCustomerByID returns a customer with a given ID. -func (c *Client) FetchCustomerByID(id string) (*Customer, error) { - var u, err = url.Parse(string(c.Endpoint)) - if err != nil { - return nil, err - } - u.Path = "/v3/company/" + c.RealmID + "/customer/" + 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 +// FindCustomerByName gets a customer with a given name. +func (c *Client) FindCustomerByName(name string) (*Customer, error) { + var resp struct { + QueryResponse struct { + Customer []Customer + TotalCount int + } } - req.Header.Add("Accept", "application/json") - var res *http.Response - res, err = c.Client.Do(req) - if err != nil { + + query := "SELECT * FROM Customer WHERE DisplayName = '" + strings.Replace(name, "'", "''", -1) + "'" + + if err := c.query(query, &resp); err != nil { return nil, err } - defer res.Body.Close() - if res.StatusCode != http.StatusOK { - return nil, errors.New("Got status code " + strconv.Itoa(res.StatusCode)) - } - var r struct { - Customer Customer - Time Date + + if len(resp.QueryResponse.Customer) == 0 { + return nil, errors.New("no customers could be found") } - err = json.NewDecoder(res.Body).Decode(&r) - return &r.Customer, err + + return &resp.QueryResponse.Customer[0], nil } -// CreateCustomer creates the given Customer on the QuickBooks server, -// returning the resulting Customer object. -func (c *Client) CreateCustomer(customer *Customer) (*Customer, error) { - var u, err = url.Parse(string(c.Endpoint)) - if err != nil { - return nil, err - } - u.Path = "/v3/company/" + c.RealmID + "/customer" - var v = url.Values{} - v.Add("minorversion", minorVersion) - u.RawQuery = v.Encode() - var j []byte - j, err = json.Marshal(customer) - 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 +// QueryCustomers accepts an SQL query and returns all customers found using it +func (c *Client) QueryCustomers(query string) ([]Customer, error) { + var resp struct { + QueryResponse struct { + Customers []Customer `json:"Customer"` + StartPosition int + MaxResults 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(query, &resp); err != nil { return nil, err } - defer res.Body.Close() - if res.StatusCode != http.StatusOK { - return nil, parseFailure(res) + if resp.QueryResponse.Customers == nil { + return nil, errors.New("could not find any customers") } - var r struct { - Customer Customer - Time Date - } - err = json.NewDecoder(res.Body).Decode(&r) - return &r.Customer, err + return resp.QueryResponse.Customers, nil } // UpdateCustomer updates the given Customer on the QuickBooks server, // returning the resulting Customer object. It's a sparse update, as not all QB // fields are present in our Customer object. func (c *Client) UpdateCustomer(customer *Customer) (*Customer, error) { - var u, err = url.Parse(string(c.Endpoint)) + if customer.Id == "" { + return nil, errors.New("missing customer id") + } + + existingCustomer, err := c.FindCustomerById(customer.Id) if err != nil { - return nil, err + return nil, fmt.Errorf("failed to find existing customer: %v", err) } - u.Path = "/v3/company/" + c.RealmID + "/customer" - var v = url.Values{} - v.Add("minorversion", minorVersion) - u.RawQuery = v.Encode() - var d = struct { + + customer.SyncToken = existingCustomer.SyncToken + + payload := struct { *Customer Sparse bool `json:"sparse"` }{ Customer: customer, 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 customerData struct { Customer Customer Time Date } - err = json.NewDecoder(res.Body).Decode(&r) - return &r.Customer, err + + if err = c.post("customer", payload, &customerData, nil); err != nil { + return nil, err + } + + return &customerData.Customer, nil } diff --git a/customer_type.go b/customer_type.go new file mode 100644 index 0000000..cd742cc --- /dev/null +++ b/customer_type.go @@ -0,0 +1,49 @@ +package quickbooks + +import ( + "errors" +) + +type CustomerType struct { + SyncToken string `json:",omitempty"` + Domain string `json:"domain,omitempty"` + Name string `json:",omitempty"` + Active bool `json:",omitempty"` + Id string `json:",omitempty"` + MetaData MetaData `json:",omitempty"` +} + +// FindCustomerTypeById returns a customerType with a given Id. +func (c *Client) FindCustomerTypeById(id string) (*CustomerType, error) { + var r struct { + CustomerType CustomerType + Time Date + } + + if err := c.get("customertype/"+id, &r, nil); err != nil { + return nil, err + } + + return &r.CustomerType, nil +} + +// QueryCustomerTypes accepts an SQL query and returns all customerTypes found using it +func (c *Client) QueryCustomerTypes(query string) ([]CustomerType, error) { + var resp struct { + QueryResponse struct { + CustomerTypes []CustomerType `json:"CustomerType"` + StartPosition int + MaxResults int + } + } + + if err := c.query(query, &resp); err != nil { + return nil, err + } + + if resp.QueryResponse.CustomerTypes == nil { + return nil, errors.New("could not find any customerTypes") + } + + return resp.QueryResponse.CustomerTypes, nil +} diff --git a/date.go b/date.go deleted file mode 100644 index 19e48fe..0000000 --- a/date.go +++ /dev/null @@ -1,31 +0,0 @@ -package quickbooks - -import ( - "time" -) - -const secondFormat = "2006-01-02" -const format = "2006-01-02T15:04:05-07:00" - -// Date represents a Quickbooks date -type Date struct { - time.Time -} - -// UnmarshalJSON removes time from parsed date -func (d *Date) UnmarshalJSON(b []byte) (err error) { - if b[0] == '"' && b[len(b)-1] == '"' { - b = b[1 : len(b)-1] - } - - d.Time, err = time.Parse(format, string(b)) - if err != nil { - d.Time, err = time.Parse(secondFormat, string(b)) - } - - return err -} - -func (d Date) String() string { - return d.Format(format) -} diff --git a/defs.go b/defs.go index d76b84b..127dc94 100644 --- a/defs.go +++ b/defs.go @@ -3,20 +3,107 @@ package quickbooks -// EndpointURL specifies the endpoint to connect to. -type EndpointURL string +import "time" + +type CustomField struct { + DefinitionId string `json:"DefinitionId,omitempty"` + StringValue string `json:"StringValue,omitempty"` + Type string `json:"Type,omitempty"` + Name string `json:"Name,omitempty"` +} + +// Date represents a Quickbooks date +type Date struct { + time.Time `json:",omitempty"` +} + +// UnmarshalJSON removes time from parsed date +func (d *Date) UnmarshalJSON(b []byte) (err error) { + if b[0] == '"' && b[len(b)-1] == '"' { + b = b[1 : len(b)-1] + } + + d.Time, err = time.Parse(format, string(b)) + if err != nil { + d.Time, err = time.Parse(secondFormat, string(b)) + } + + return err +} + +func (d Date) String() string { + return d.Format(format) +} + +// EmailAddress represents a QuickBooks email address. +type EmailAddress struct { + Address string `json:",omitempty"` +} + +// EndpointUrl specifies the endpoint to connect to +type EndpointUrl string const ( + // DiscoveryProductionEndpoint is for live apps. + DiscoveryProductionEndpoint EndpointUrl = "https://developer.api.intuit.com/.well-known/openid_configuration" + // DiscoverySandboxEndpoint is for testing. + DiscoverySandboxEndpoint EndpointUrl = "https://developer.api.intuit.com/.well-known/openid_sandbox_configuration" // ProductionEndpoint is for live apps. - ProductionEndpoint EndpointURL = "https://quickbooks.api.intuit.com" + ProductionEndpoint EndpointUrl = "https://quickbooks.api.intuit.com" // SandboxEndpoint is for testing. - SandboxEndpoint EndpointURL = "https://sandbox-quickbooks.api.intuit.com" - // DiscoverySandboxEndpoint is for testing. - DiscoverySandboxEndpoint EndpointURL = "https://developer.api.intuit.com/.well-known/openid_sandbox_configuration" - // DiscoveryProductionEndpoint is for live apps. - DiscoveryProductionEndpoint EndpointURL = "https://developer.api.intuit.com/.well-known/openid_configuration" + SandboxEndpoint EndpointUrl = "https://sandbox-quickbooks.api.intuit.com" + + format = "2006-01-02T15:04:05-07:00" + queryPageSize = 1000 + secondFormat = "2006-01-02" ) -const queryPageSize = 1000 +func (u EndpointUrl) String() string { + return string(u) +} + +// MemoRef represents a QuickBooks MemoRef object. +type MemoRef struct { + Value string `json:"value,omitempty"` +} + +// MetaData is a timestamp of genesis and last change of a Quickbooks object +type MetaData struct { + CreateTime Date `json:",omitempty"` + LastUpdatedTime Date `json:",omitempty"` +} + +// PhysicalAddress represents a QuickBooks address. +type PhysicalAddress struct { + Id string `json:"Id,omitempty"` + // These lines are context-dependent! Read the QuickBooks API carefully. + Line1 string `json:",omitempty"` + Line2 string `json:",omitempty"` + Line3 string `json:",omitempty"` + Line4 string `json:",omitempty"` + Line5 string `json:",omitempty"` + City string `json:",omitempty"` + Country string `json:",omitempty"` + // A.K.A. State. + CountrySubDivisionCode string `json:",omitempty"` + PostalCode string `json:",omitempty"` + Lat string `json:",omitempty"` + Long string `json:",omitempty"` +} + +// ReferenceType represents a QuickBooks reference to another object. +type ReferenceType struct { + Value string `json:"value,omitempty"` + Name string `json:"name,omitempty"` + Type string `json:"type,omitempty"` +} + +// TelephoneNumber represents a QuickBooks phone number. +type TelephoneNumber struct { + FreeFormNumber string `json:",omitempty"` +} -const minorVersion = "52" +// WebSiteAddress represents a Quickbooks Website +type WebSiteAddress struct { + URI string `json:",omitempty"` +} diff --git a/deposit.go b/deposit.go new file mode 100644 index 0000000..e56eb41 --- /dev/null +++ b/deposit.go @@ -0,0 +1,145 @@ +package quickbooks + +import ( + "errors" + "strconv" +) + +type Deposit struct { + SyncToken string `json:",omitempty"` + Domain string `json:"domain,omitempty"` + DepositToAccountRef ReferenceType `json:",omitempty"` + TxnDate Date `json:",omitempty"` + TotalAmt float64 `json:",omitempty"` + Line []PaymentLine `json:",omitempty"` + Id string `json:",omitempty"` + MetaData MetaData `json:",omitempty"` +} + +// CreateDeposit creates the given deposit within QuickBooks +func (c *Client) CreateDeposit(deposit *Deposit) (*Deposit, error) { + var resp struct { + Deposit Deposit + Time Date + } + + if err := c.post("deposit", deposit, &resp, nil); err != nil { + return nil, err + } + + return &resp.Deposit, nil +} + +func (c *Client) DeleteDeposit(deposit *Deposit) error { + if deposit.Id == "" || deposit.SyncToken == "" { + return errors.New("missing id/sync token") + } + + return c.post("deposit", deposit, nil, map[string]string{"operation": "delete"}) +} + +// FindDeposits gets the full list of Deposits in the QuickBooks account. +func (c *Client) FindDeposits() ([]Deposit, error) { + var resp struct { + QueryResponse struct { + Deposits []Deposit `json:"Deposit"` + MaxResults int + StartPosition int + TotalCount int + } + } + + if err := c.query("SELECT COUNT(*) FROM Deposit", &resp); err != nil { + return nil, err + } + + if resp.QueryResponse.TotalCount == 0 { + return nil, errors.New("no deposits could be found") + } + + deposits := make([]Deposit, 0, resp.QueryResponse.TotalCount) + + for i := 0; i < resp.QueryResponse.TotalCount; i += queryPageSize { + query := "SELECT * FROM Deposit 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.Deposits == nil { + return nil, errors.New("no deposits could be found") + } + + deposits = append(deposits, resp.QueryResponse.Deposits...) + } + + return deposits, nil +} + +// FindDepositById returns an deposit with a given Id. +func (c *Client) FindDepositById(id string) (*Deposit, error) { + var resp struct { + Deposit Deposit + Time Date + } + + if err := c.get("deposit/"+id, &resp, nil); err != nil { + return nil, err + } + + return &resp.Deposit, nil +} + +// QueryDeposits accepts an SQL query and returns all deposits found using it +func (c *Client) QueryDeposits(query string) ([]Deposit, error) { + var resp struct { + QueryResponse struct { + Deposits []Deposit `json:"Deposit"` + StartPosition int + MaxResults int + } + } + + if err := c.query(query, &resp); err != nil { + return nil, err + } + + if resp.QueryResponse.Deposits == nil { + return nil, errors.New("could not find any deposits") + } + + return resp.QueryResponse.Deposits, nil +} + +// UpdateDeposit updates the deposit +func (c *Client) UpdateDeposit(deposit *Deposit) (*Deposit, error) { + if deposit.Id == "" { + return nil, errors.New("missing deposit id") + } + + existingDeposit, err := c.FindDepositById(deposit.Id) + if err != nil { + return nil, err + } + + deposit.SyncToken = existingDeposit.SyncToken + + payload := struct { + *Deposit + Sparse bool `json:"sparse"` + }{ + Deposit: deposit, + Sparse: true, + } + + var depositData struct { + Deposit Deposit + Time Date + } + + if err = c.post("deposit", payload, &depositData, nil); err != nil { + return nil, err + } + + return &depositData.Deposit, err +} diff --git a/discovery.go b/discovery.go index e5c110a..6a0e203 100644 --- a/discovery.go +++ b/discovery.go @@ -2,34 +2,11 @@ package quickbooks import ( "encoding/json" + "fmt" "io/ioutil" - "log" "net/http" ) -// Call the discovery API. -// See https://developer.intuit.com/app/developer/qbo/docs/develop/authentication-and-authorization/openid-connect#discovery-document -// -func CallDiscoveryAPI(discoveryEndpoint EndpointURL) *DiscoveryAPI { - log.Println("Entering CallDiscoveryAPI ") - client := &http.Client{} - request, err := http.NewRequest("GET", string(discoveryEndpoint), nil) - if err != nil { - log.Fatalln(err) - } - //set header - request.Header.Set("accept", "application/json") - - resp, err := client.Do(request) - defer resp.Body.Close() - body, err := ioutil.ReadAll(resp.Body) - if err != nil { - log.Fatalln(err) - } - discoveryAPIResponse, err := getDiscoveryAPIResponse(body) - return discoveryAPIResponse -} - type DiscoveryAPI struct { Issuer string `json:"issuer"` AuthorizationEndpoint string `json:"authorization_endpoint"` @@ -39,11 +16,33 @@ type DiscoveryAPI struct { JwksUri string `json:"jwks_uri"` } -func getDiscoveryAPIResponse(body []byte) (*DiscoveryAPI, error) { - var s = new(DiscoveryAPI) - err := json.Unmarshal(body, &s) +// CallDiscoveryAPI +// See https://developer.intuit.com/app/developer/qbo/docs/develop/authentication-and-authorization/openid-connect#discovery-document +func CallDiscoveryAPI(discoveryEndpoint EndpointUrl) (*DiscoveryAPI, error) { + client := &http.Client{} + req, err := http.NewRequest("GET", string(discoveryEndpoint), nil) + if err != nil { + return nil, fmt.Errorf("failed to create req: %v", err) + } + + req.Header.Set("Accept", "application/json") + + resp, err := client.Do(req) if err != nil { - log.Fatalln("error getting DiscoveryAPIResponse:", err) + return nil, fmt.Errorf("failed to make req: %v", err) } - return s, err + + defer resp.Body.Close() + + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("failed to read body: %v", err) + } + + respData := DiscoveryAPI{} + if err = json.Unmarshal(body, &respData); err != nil { + return nil, fmt.Errorf("error getting DiscoveryAPIResponse: %v", err) + } + + return &respData, nil } diff --git a/email.go b/email.go deleted file mode 100644 index af1ecc4..0000000 --- a/email.go +++ /dev/null @@ -1,9 +0,0 @@ -// Copyright (c) 2018, Randy Westlund. All rights reserved. -// This code is under the BSD-2-Clause license. - -package quickbooks - -// EmailAddress represents a QuickBooks email address. -type EmailAddress struct { - Address string `json:",omitempty"` -} diff --git a/employee.go b/employee.go new file mode 100644 index 0000000..fe16cb2 --- /dev/null +++ b/employee.go @@ -0,0 +1,142 @@ +package quickbooks + +import ( + "errors" + "strconv" +) + +type Employee struct { + SyncToken string `json:",omitempty"` + Domain string `json:"domain,omitempty"` + DisplayName string `json:",omitempty"` + PrimaryPhone TelephoneNumber `json:",omitempty"` + PrintOnCheckName string `json:",omitempty"` + FamilyName string `json:",omitempty"` + Active bool `json:",omitempty"` + SSN string `json:",omitempty"` + PrimaryAddr PhysicalAddress `json:",omitempty"` + BillableTime bool `json:",omitempty"` + GivenName string `json:",omitempty"` + Id string `json:",omitempty"` + MetaData MetaData `json:",omitempty"` +} + +// CreateEmployee creates the given employee within QuickBooks +func (c *Client) CreateEmployee(employee *Employee) (*Employee, error) { + var resp struct { + Employee Employee + Time Date + } + + if err := c.post("employee", employee, &resp, nil); err != nil { + return nil, err + } + + return &resp.Employee, nil +} + +// FindEmployees gets the full list of Employees in the QuickBooks account. +func (c *Client) FindEmployees() ([]Employee, error) { + var resp struct { + QueryResponse struct { + Employees []Employee `json:"Employee"` + MaxResults int + StartPosition int + TotalCount int + } + } + + if err := c.query("SELECT COUNT(*) FROM Employee", &resp); err != nil { + return nil, err + } + + if resp.QueryResponse.TotalCount == 0 { + return nil, errors.New("no employees could be found") + } + + employees := make([]Employee, 0, resp.QueryResponse.TotalCount) + + for i := 0; i < resp.QueryResponse.TotalCount; i += queryPageSize { + query := "SELECT * FROM Employee 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.Employees == nil { + return nil, errors.New("no employees could be found") + } + + employees = append(employees, resp.QueryResponse.Employees...) + } + + return employees, nil +} + +// FindEmployeeById returns an employee with a given Id. +func (c *Client) FindEmployeeById(id string) (*Employee, error) { + var resp struct { + Employee Employee + Time Date + } + + if err := c.get("employee/"+id, &resp, nil); err != nil { + return nil, err + } + + return &resp.Employee, nil +} + +// QueryEmployees accepts an SQL query and returns all employees found using it +func (c *Client) QueryEmployees(query string) ([]Employee, error) { + var resp struct { + QueryResponse struct { + Employees []Employee `json:"Employee"` + StartPosition int + MaxResults int + } + } + + if err := c.query(query, &resp); err != nil { + return nil, err + } + + if resp.QueryResponse.Employees == nil { + return nil, errors.New("could not find any employees") + } + + return resp.QueryResponse.Employees, nil +} + +// UpdateEmployee updates the employee +func (c *Client) UpdateEmployee(employee *Employee) (*Employee, error) { + if employee.Id == "" { + return nil, errors.New("missing employee id") + } + + existingEmployee, err := c.FindEmployeeById(employee.Id) + if err != nil { + return nil, err + } + + employee.SyncToken = existingEmployee.SyncToken + + payload := struct { + *Employee + Sparse bool `json:"sparse"` + }{ + Employee: employee, + Sparse: true, + } + + var employeeData struct { + Employee Employee + Time Date + } + + if err = c.post("employee", payload, &employeeData, nil); err != nil { + return nil, err + } + + return &employeeData.Employee, err +} diff --git a/errors.go b/errors.go index 36f2d9a..7cb6be7 100644 --- a/errors.go +++ b/errors.go @@ -6,20 +6,12 @@ package quickbooks import ( "encoding/json" "errors" + "fmt" "io/ioutil" "net/http" "strconv" ) -// Error implements the error interface. -func (f Failure) Error() string { - var text, err = json.Marshal(f) - if err != nil { - return "When marshalling error:" + err.Error() - } - return string(text) -} - // Failure is the outermost struct that holds an error response. type Failure struct { Fault struct { @@ -34,17 +26,28 @@ type Failure struct { Time Date `json:"time"` } +// Error implements the error interface. +func (f Failure) Error() string { + text, err := json.Marshal(f) + if err != nil { + return fmt.Sprintf("unexpected error while marshalling error: %v", err) + } + + return string(text) +} + // parseFailure takes a response reader and tries to parse a Failure. -func parseFailure(res *http.Response) error { - var msg, err = ioutil.ReadAll(res.Body) +func parseFailure(resp *http.Response) error { + msg, err := ioutil.ReadAll(resp.Body) if err != nil { return errors.New("When reading response body:" + err.Error()) } + var errStruct Failure - err = json.Unmarshal(msg, &errStruct) - if err != nil { - return errors.New(strconv.Itoa(res.StatusCode) + - " " + string(msg)) + + if err = json.Unmarshal(msg, &errStruct); err != nil { + return errors.New(strconv.Itoa(resp.StatusCode) + " " + string(msg)) } + return errStruct } diff --git a/estimate.go b/estimate.go new file mode 100644 index 0000000..b22b856 --- /dev/null +++ b/estimate.go @@ -0,0 +1,184 @@ +package quickbooks + +import ( + "errors" + "strconv" +) + +type Estimate struct { + DocNumber string `json:",omitempty"` + SyncToken string `json:",omitempty"` + Domain string `json:"domain,omitempty"` + TxnStatus string `json:",omitempty"` + BillEmail EmailAddress `json:",omitempty"` + TxnDate Date `json:",omitempty"` + TotalAmt float64 `json:",omitempty"` + CustomerRef ReferenceType `json:",omitempty"` + CustomerMemo MemoRef `json:",omitempty"` + ShipAddr PhysicalAddress `json:",omitempty"` + PrintStatus string `json:",omitempty"` + BillAddr PhysicalAddress `json:",omitempty"` + EmailStatus string `json:",omitempty"` + Line []Line `json:",omitempty"` + ApplyTaxAfterDiscount bool `json:",omitempty"` + CustomField []CustomField `json:",omitempty"` + Id string `json:",omitempty"` + TxnTaxDetail TxnTaxDetail `json:",omitempty"` + MetaData MetaData `json:",omitempty"` +} + +// CreateEstimate creates the given Estimate on the QuickBooks server, returning +// the resulting Estimate object. +func (c *Client) CreateEstimate(estimate *Estimate) (*Estimate, error) { + var resp struct { + Estimate Estimate + Time Date + } + + if err := c.post("estimate", estimate, &resp, nil); err != nil { + return nil, err + } + + return &resp.Estimate, nil +} + +// DeleteEstimate deletes the estimate +func (c *Client) DeleteEstimate(estimate *Estimate) error { + if estimate.Id == "" || estimate.SyncToken == "" { + return errors.New("missing id/sync token") + } + + return c.post("estimate", estimate, nil, map[string]string{"operation": "delete"}) +} + +// FindEstimates gets the full list of Estimates in the QuickBooks account. +func (c *Client) FindEstimates() ([]Estimate, error) { + var resp struct { + QueryResponse struct { + Estimates []Estimate `json:"Estimate"` + MaxResults int + StartPosition int + TotalCount int + } + } + + if err := c.query("SELECT COUNT(*) FROM Estimate", &resp); err != nil { + return nil, err + } + + if resp.QueryResponse.TotalCount == 0 { + return nil, errors.New("no estimates could be found") + } + + estimates := make([]Estimate, 0, resp.QueryResponse.TotalCount) + + for i := 0; i < resp.QueryResponse.TotalCount; i += queryPageSize { + query := "SELECT * FROM Estimate 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.Estimates == nil { + return nil, errors.New("no estimates could be found") + } + + estimates = append(estimates, resp.QueryResponse.Estimates...) + } + + return estimates, nil +} + +// FindEstimateById finds the estimate by the given id +func (c *Client) FindEstimateById(id string) (*Estimate, error) { + var resp struct { + Estimate Estimate + Time Date + } + + if err := c.get("estimate/"+id, &resp, nil); err != nil { + return nil, err + } + + return &resp.Estimate, nil +} + +// QueryEstimates accepts an SQL query and returns all estimates found using it +func (c *Client) QueryEstimates(query string) ([]Estimate, error) { + var resp struct { + QueryResponse struct { + Estimates []Estimate `json:"Estimate"` + StartPosition int + MaxResults int + } + } + + if err := c.query(query, &resp); err != nil { + return nil, err + } + + if resp.QueryResponse.Estimates == nil { + return nil, errors.New("could not find any estimates") + } + + return resp.QueryResponse.Estimates, nil +} + +// SendEstimate sends the estimate to the Estimate.BillEmail if emailAddress is left empty +func (c *Client) SendEstimate(estimateId string, emailAddress string) error { + queryParameters := make(map[string]string) + + if emailAddress != "" { + queryParameters["sendTo"] = emailAddress + } + + return c.post("estimate/"+estimateId+"/send", nil, nil, queryParameters) +} + +// UpdateEstimate updates the estimate +func (c *Client) UpdateEstimate(estimate *Estimate) (*Estimate, error) { + if estimate.Id == "" { + return nil, errors.New("missing estimate id") + } + + existingEstimate, err := c.FindEstimateById(estimate.Id) + if err != nil { + return nil, err + } + + estimate.SyncToken = existingEstimate.SyncToken + + payload := struct { + *Estimate + Sparse bool `json:"sparse"` + }{ + Estimate: estimate, + Sparse: true, + } + + var estimateData struct { + Estimate Estimate + Time Date + } + + if err = c.post("estimate", payload, &estimateData, nil); err != nil { + return nil, err + } + + return &estimateData.Estimate, err +} + +func (c *Client) VoidEstimate(estimate Estimate) error { + if estimate.Id == "" { + return errors.New("missing estimate id") + } + + existingEstimate, err := c.FindEstimateById(estimate.Id) + if err != nil { + return err + } + + estimate.SyncToken = existingEstimate.SyncToken + + return c.post("estimate", estimate, nil, map[string]string{"operation": "void"}) +} diff --git a/examples/auth_flow_test.go b/examples/auth_flow_test.go index b532835..5012300 100644 --- a/examples/auth_flow_test.go +++ b/examples/auth_flow_test.go @@ -2,9 +2,10 @@ package examples import ( "fmt" + "testing" + "github.com/rwestlund/quickbooks-go" "github.com/stretchr/testify/require" - "testing" ) func TestAuthorizationFlow(t *testing.T) { @@ -12,7 +13,7 @@ func TestAuthorizationFlow(t *testing.T) { clientSecret := "" realmId := "" - qbClient, err := quickbooks.NewQuickbooksClient(clientId, clientSecret, realmId, false, nil) + qbClient, err := quickbooks.NewClient(clientId, clientSecret, realmId, false, "", nil) require.NoError(t, err) // To do first when you receive the authorization code from quickbooks callback @@ -27,10 +28,10 @@ func TestAuthorizationFlow(t *testing.T) { require.NoError(t, err) // Make a request! - info, err := qbClient.FetchCompanyInfo() + info, err := qbClient.FindCompanyInfo() require.NoError(t, err) fmt.Println(info) // Revoke the token, this should be done only if a user unsubscribe from your app - qbClient.RevokeToken(bearerToken.RefreshToken) + require.NoError(t, qbClient.RevokeToken(bearerToken.RefreshToken)) } diff --git a/examples/reuse_token_test.go b/examples/reuse_token_test.go index 06e8069..5691fe1 100644 --- a/examples/reuse_token_test.go +++ b/examples/reuse_token_test.go @@ -2,9 +2,10 @@ package examples import ( "fmt" + "testing" + "github.com/rwestlund/quickbooks-go" "github.com/stretchr/testify/require" - "testing" ) func TestReuseToken(t *testing.T) { @@ -17,11 +18,11 @@ func TestReuseToken(t *testing.T) { AccessToken: "", } - qbClient, err := quickbooks.NewQuickbooksClient(clientId, clientSecret, realmId, false, &token) + qbClient, err := quickbooks.NewClient(clientId, clientSecret, realmId, false, "", &token) require.NoError(t, err) // Make a request! - info, err := qbClient.FetchCompanyInfo() + info, err := qbClient.FindCompanyInfo() require.NoError(t, err) fmt.Println(info) } diff --git a/go.mod b/go.mod index c567275..ec22901 100644 --- a/go.mod +++ b/go.mod @@ -1,9 +1,15 @@ module github.com/rwestlund/quickbooks-go -go 1.14 +go 1.20 require ( - github.com/stretchr/testify v1.6.1 - golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5 + github.com/stretchr/testify v1.9.0 + golang.org/x/oauth2 v0.19.0 gopkg.in/guregu/null.v4 v4.0.0 ) + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/go.sum b/go.sum index e71bc36..a260b36 100644 --- a/go.sum +++ b/go.sum @@ -1,374 +1,15 @@ -cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= -cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= -cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= -cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= -cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= -cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= -cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= -cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= -cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= -cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= -cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= -cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= -cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= -cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= -cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= -cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= -cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= -cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= -cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= -cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= -cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= -cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= -cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= -cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= -cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= -cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= -cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= -cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= -cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= -cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= -dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= -github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= -github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= -github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= -github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= -github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= -github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= -github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= -github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= -github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= -github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= -github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= -github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= -github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= -github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= -github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= -github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= -github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= -github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= -github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= -github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= -github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= -github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= -github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= -github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= -github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= -github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= -github.com/golang/protobuf v1.4.2 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0= -github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= -github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= -github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= -github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= -github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.1 h1:JFrFEBb2xKufg6XkJsJr+WbKb4FQlURi5RUcBveYu9k= -github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= -github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= -github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= -github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= -github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= -github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= -github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= -github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= -github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= -github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= -github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= -github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= -github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= -github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= -github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= -github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= -github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= -github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= -go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= -go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= -golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= -golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= -golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= -golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= -golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= -golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= -golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= -golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= -golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= -golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= -golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= -golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= -golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= -golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= -golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= -golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= -golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= -golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20200822124328-c89045814202 h1:VvcQYSHwXgi7W+TpUR6A9g6Up98WAHf3f/ulnJ62IyA= -golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5 h1:Lm4OryKCca1vehdsWogr9N4t7NfZxLbJoc/H0w4K4S4= -golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= -golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= -golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= -golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= -golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= -golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= -google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= -google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= -google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= -google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= -google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= -google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= -google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= -google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= -google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= -google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/appengine v1.6.6 h1:lMO5rYAqUxkmaj76jAkRUvt5JZgFymx/+Q5Mzfivuhc= -google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= -google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= -google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= -google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= -google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= -google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= -google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= -google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= -google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= -google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= -google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= -google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= -google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= -google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= -google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= -google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= -google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= -google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= -google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= -google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= -google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= -google.golang.org/protobuf v1.25.0 h1:Ejskq+SyPohKW+1uil0JJMtmHCgJPJ/qWTxr8qp+R4c= -google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +golang.org/x/oauth2 v0.19.0 h1:9+E/EZBCbTLNrbN35fHv/a/d/mOBatymz1zbtQrXpIg= +golang.org/x/oauth2 v0.19.0/go.mod h1:vYi7skDa1x015PmRRYZ7+s1cWyPgrPiSYRe4rnsexc8= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/guregu/null.v4 v4.0.0 h1:1Wm3S1WEA2I26Kq+6vcW+w0gcDo44YKYD7YIEJNHDjg= gopkg.in/guregu/null.v4 v4.0.0/go.mod h1:YoQhUrADuG3i9WqesrCmpNRwm1ypAgSHYqoOcTu/JrI= -gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= -honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= -honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= -rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= -rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= -rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/invoice.go b/invoice.go index 95575d1..254071f 100644 --- a/invoice.go +++ b/invoice.go @@ -4,49 +4,47 @@ package quickbooks import ( - "bytes" "encoding/json" - "net/http" - "net/url" + "errors" "strconv" ) // Invoice represents a QuickBooks Invoice object. type Invoice struct { - ID string `json:"Id,omitempty"` - SyncToken string `json:",omitempty"` - MetaData MetaData `json:",omitempty"` - //CustomField - DocNumber string `json:",omitempty"` - TxnDate Date `json:",omitempty"` - //DepartmentRef - PrivateNote string `json:",omitempty"` - //LinkedTxn - Line []Line - TxnTaxDetail TxnTaxDetail `json:",omitempty"` - CustomerRef ReferenceType - CustomerMemo MemoRef `json:",omitempty"` - BillAddr PhysicalAddress `json:",omitempty"` - ShipAddr PhysicalAddress `json:",omitempty"` - ClassRef ReferenceType `json:",omitempty"` - SalesTermRef ReferenceType `json:",omitempty"` - DueDate Date `json:",omitempty"` - //GlobalTaxCalculation - ShipMethodRef ReferenceType `json:",omitempty"` - ShipDate Date `json:",omitempty"` - TrackingNum string `json:",omitempty"` - TotalAmt json.Number `json:",omitempty"` - //CurrencyRef - ExchangeRate json.Number `json:",omitempty"` - HomeAmtTotal json.Number `json:",omitempty"` - HomeBalance json.Number `json:",omitempty"` - ApplyTaxAfterDiscount bool `json:",omitempty"` - PrintStatus string `json:",omitempty"` - EmailStatus string `json:",omitempty"` - BillEmail EmailAddress `json:",omitempty"` - BillEmailCC EmailAddress `json:"BillEmailCc,omitempty"` - BillEmailBCC EmailAddress `json:"BillEmailBcc,omitempty"` - //DeliveryInfo + Id string `json:"Id,omitempty"` + SyncToken string `json:",omitempty"` + MetaData MetaData `json:",omitempty"` + CustomField []CustomField `json:",omitempty"` + DocNumber string `json:",omitempty"` + TxnDate Date `json:",omitempty"` + DepartmentRef ReferenceType `json:",omitempty"` + PrivateNote string `json:",omitempty"` + LinkedTxn []LinkedTxn `json:"LinkedTxn"` + Line []Line + TxnTaxDetail TxnTaxDetail `json:",omitempty"` + CustomerRef ReferenceType + CustomerMemo MemoRef `json:",omitempty"` + BillAddr PhysicalAddress `json:",omitempty"` + ShipAddr PhysicalAddress `json:",omitempty"` + ClassRef ReferenceType `json:",omitempty"` + SalesTermRef ReferenceType `json:",omitempty"` + DueDate Date `json:",omitempty"` + // GlobalTaxCalculation + ShipMethodRef ReferenceType `json:",omitempty"` + ShipDate Date `json:",omitempty"` + TrackingNum string `json:",omitempty"` + TotalAmt json.Number `json:",omitempty"` + CurrencyRef ReferenceType `json:",omitempty"` + ExchangeRate json.Number `json:",omitempty"` + HomeAmtTotal json.Number `json:",omitempty"` + HomeBalance json.Number `json:",omitempty"` + ApplyTaxAfterDiscount bool `json:",omitempty"` + PrintStatus string `json:",omitempty"` + EmailStatus string `json:",omitempty"` + BillEmail EmailAddress `json:",omitempty"` + BillEmailCC EmailAddress `json:"BillEmailCc,omitempty"` + BillEmailBCC EmailAddress `json:"BillEmailBcc,omitempty"` + DeliveryInfo *DeliveryInfo `json:",omitempty"` Balance json.Number `json:",omitempty"` TxnSource string `json:",omitempty"` AllowOnlineCreditCardPayment bool `json:",omitempty"` @@ -55,44 +53,51 @@ type Invoice struct { DepositToAccountRef ReferenceType `json:",omitempty"` } -// TxnTaxDetail ... +type DeliveryInfo struct { + DeliveryType string + DeliveryTime Date +} + +type LinkedTxn struct { + TxnID string `json:"TxnId"` + TxnType string `json:"TxnType"` +} + type TxnTaxDetail struct { TxnTaxCodeRef ReferenceType `json:",omitempty"` TotalTax json.Number `json:",omitempty"` TaxLine []Line `json:",omitempty"` } -// AccountBasedExpenseLineDetail type AccountBasedExpenseLineDetail struct { AccountRef ReferenceType TaxAmount json.Number `json:",omitempty"` - //TaxInclusiveAmt json.Number `json:",omitempty"` - //ClassRef ReferenceType `json:",omitempty"` - //TaxCodeRef ReferenceType `json:",omitempty"` + // TaxInclusiveAmt json.Number `json:",omitempty"` + // ClassRef ReferenceType `json:",omitempty"` + // TaxCodeRef ReferenceType `json:",omitempty"` // MarkupInfo MarkupInfo `json:",omitempty"` - //BillableStatus BillableStatusEnum `json:",omitempty"` - //CustomerRef ReferenceType `json:",omitempty"` + // BillableStatus BillableStatusEnum `json:",omitempty"` + // CustomerRef ReferenceType `json:",omitempty"` } -// Line ... type Line struct { - ID string `json:"Id,omitempty"` + Id string `json:",omitempty"` LineNum int `json:",omitempty"` Description string `json:",omitempty"` Amount json.Number DetailType string - AccountBasedExpenseLineDetail AccountBasedExpenseLineDetail - SalesItemLineDetail SalesItemLineDetail `json:",omitempty"` - DiscountLineDetail DiscountLineDetail `json:",omitempty"` - TaxLineDetail TaxLineDetail `json:",omitempty"` + AccountBasedExpenseLineDetail AccountBasedExpenseLineDetail `json:",omitempty"` + SalesItemLineDetail SalesItemLineDetail `json:",omitempty"` + DiscountLineDetail DiscountLineDetail `json:",omitempty"` + TaxLineDetail TaxLineDetail `json:",omitempty"` } // TaxLineDetail ... type TaxLineDetail struct { PercentBased bool `json:",omitempty"` NetAmountTaxable json.Number `json:",omitempty"` - //TaxInclusiveAmount json.Number `json:",omitempty"` - //OverrideDeltaAmount + // TaxInclusiveAmount json.Number `json:",omitempty"` + // OverrideDeltaAmount TaxPercent json.Number `json:",omitempty"` TaxRateRef ReferenceType } @@ -102,7 +107,7 @@ type SalesItemLineDetail struct { ItemRef ReferenceType `json:",omitempty"` ClassRef ReferenceType `json:",omitempty"` UnitPrice json.Number `json:",omitempty"` - //MarkupInfo + // MarkupInfo Qty float32 `json:",omitempty"` ItemAccountRef ReferenceType `json:",omitempty"` TaxCodeRef ReferenceType `json:",omitempty"` @@ -118,161 +123,166 @@ type DiscountLineDetail struct { DiscountPercent float32 `json:",omitempty"` } -// FetchInvoices gets the full list of Invoices in the QuickBooks account. -func (c *Client) FetchInvoices() ([]Invoice, error) { +// CreateInvoice creates the given Invoice on the QuickBooks server, returning +// the resulting Invoice object. +func (c *Client) CreateInvoice(invoice *Invoice) (*Invoice, error) { + var resp struct { + Invoice Invoice + Time Date + } - // See how many invoices there are. - var r struct { + if err := c.post("invoice", invoice, &resp, nil); err != nil { + return nil, err + } + + return &resp.Invoice, nil +} + +// DeleteInvoice deletes the invoice +// +// If the invoice was already deleted, QuickBooks returns 400 :( +// The response looks like this: +// {"Fault":{"Error":[{"Message":"Object Not Found","Detail":"Object Not Found : Something you're trying to use has been made inactive. Check the fields with accounts, invoices, items, vendors or employees.","code":"610","element":""}],"type":"ValidationFault"},"time":"2018-03-20T20:15:59.571-07:00"} +// +// This is slightly horrifying and not documented in their API. When this +// happens we just return success; the goal of deleting it has been +// accomplished, just not by us. +func (c *Client) DeleteInvoice(invoice *Invoice) error { + if invoice.Id == "" || invoice.SyncToken == "" { + return errors.New("missing id/sync token") + } + + return c.post("invoice", invoice, nil, map[string]string{"operation": "delete"}) +} + +// FindInvoices gets the full list of Invoices in the QuickBooks account. +func (c *Client) FindInvoices() ([]Invoice, error) { + var resp struct { QueryResponse struct { - TotalCount int + Invoices []Invoice `json:"Invoice"` + MaxResults int + StartPosition int + TotalCount int } } - err := c.query("SELECT COUNT(*) FROM Invoice", &r) - if err != nil { + + if err := c.query("SELECT COUNT(*) FROM Invoice", &resp); err != nil { return nil, err } - if r.QueryResponse.TotalCount == 0 { - return make([]Invoice, 0), nil + if resp.QueryResponse.TotalCount == 0 { + return nil, errors.New("no invoices could be found") } - var invoices = make([]Invoice, 0, r.QueryResponse.TotalCount) - for i := 0; i < r.QueryResponse.TotalCount; i += queryPageSize { - var page, err = c.fetchInvoicePage(i + 1) - if err != nil { + invoices := make([]Invoice, 0, resp.QueryResponse.TotalCount) + + for i := 0; i < resp.QueryResponse.TotalCount; i += queryPageSize { + query := "SELECT * FROM Invoice ORDERBY Id STARTPOSITION " + strconv.Itoa(i+1) + " MAXRESULTS " + strconv.Itoa(queryPageSize) + + if err := c.query(query, &resp); err != nil { return nil, err } - invoices = append(invoices, page...) + + if resp.QueryResponse.Invoices == nil { + return nil, errors.New("no invoices could be found") + } + + invoices = append(invoices, resp.QueryResponse.Invoices...) } + return invoices, nil } -// Fetch one page of results, because we can't get them all in one query. -func (c *Client) fetchInvoicePage(startpos int) ([]Invoice, error) { +// FindInvoiceById finds the invoice by the given id +func (c *Client) FindInvoiceById(id string) (*Invoice, error) { + var resp struct { + Invoice Invoice + Time Date + } - var r struct { + if err := c.get("invoice/"+id, &resp, nil); err != nil { + return nil, err + } + + return &resp.Invoice, nil +} + +// QueryInvoices accepts an SQL query and returns all invoices found using it +func (c *Client) QueryInvoices(query string) ([]Invoice, error) { + var resp struct { QueryResponse struct { - Invoice []Invoice + Invoices []Invoice `json:"Invoice"` StartPosition int MaxResults int } } - q := "SELECT * FROM Invoice ORDERBY Id STARTPOSITION " + - strconv.Itoa(startpos) + " MAXRESULTS " + strconv.Itoa(queryPageSize) - err := c.query(q, &r) - if err != nil { + + if err := c.query(query, &resp); err != nil { return nil, err } - // Make sure we don't return nil if there are no invoices. - if r.QueryResponse.Invoice == nil { - r.QueryResponse.Invoice = make([]Invoice, 0) + if resp.QueryResponse.Invoices == nil { + return nil, errors.New("could not find any invoices") } - return r.QueryResponse.Invoice, nil + + return resp.QueryResponse.Invoices, nil } -// CreateInvoice creates the given Invoice on the QuickBooks server, returning -// the resulting Invoice object. -func (c *Client) CreateInvoice(inv *Invoice) (*Invoice, error) { - var u, err = url.Parse(string(c.Endpoint)) - if err != nil { - return nil, err - } - u.Path = "/v3/company/" + c.RealmID + "/invoice" - var v = url.Values{} - v.Add("minorversion", minorVersion) - u.RawQuery = v.Encode() - var j []byte - j, err = json.Marshal(inv) - if err != nil { - return nil, err +// SendInvoice sends the invoice to the Invoice.BillEmail if emailAddress is left empty +func (c *Client) SendInvoice(invoiceId string, emailAddress string) error { + queryParameters := make(map[string]string) + + if emailAddress != "" { + queryParameters["sendTo"] = emailAddress } - var req *http.Request - req, err = http.NewRequest("POST", u.String(), bytes.NewBuffer(j)) - if err != nil { - return nil, err + + return c.post("invoice/"+invoiceId+"/send", nil, nil, queryParameters) +} + +// UpdateInvoice updates the invoice +func (c *Client) UpdateInvoice(invoice *Invoice) (*Invoice, error) { + if invoice.Id == "" { + return nil, errors.New("missing invoice id") } - req.Header.Add("Content-Type", "application/json") - req.Header.Add("Accept", "application/json") - var res *http.Response - res, err = c.Client.Do(req) + + existingInvoice, err := c.FindInvoiceById(invoice.Id) if err != nil { return nil, err } - defer res.Body.Close() - if res.StatusCode != http.StatusOK { - return nil, parseFailure(res) + invoice.SyncToken = existingInvoice.SyncToken + + payload := struct { + *Invoice + Sparse bool `json:"sparse"` + }{ + Invoice: invoice, + Sparse: true, } - var r struct { + var invoiceData struct { Invoice Invoice Time Date } - err = json.NewDecoder(res.Body).Decode(&r) - return &r.Invoice, err -} -// DeleteInvoice deletes the given Invoice by ID and sync token from the -// QuickBooks server. -func (c *Client) DeleteInvoice(id, syncToken string) error { - var u, err = url.Parse(string(c.Endpoint)) - if err != nil { - return err - } - u.Path = "/v3/company/" + c.RealmID + "/invoice" - var v = url.Values{} - v.Add("minorversion", minorVersion) - v.Add("operation", "delete") - u.RawQuery = v.Encode() - var j []byte - j, err = json.Marshal(struct { - ID string `json:"Id"` - SyncToken string - }{ - ID: id, - SyncToken: syncToken, - }) - if err != nil { - return err + if err = c.post("invoice", payload, &invoiceData, nil); err != nil { + return nil, err } - var req *http.Request - req, err = http.NewRequest("POST", u.String(), bytes.NewBuffer(j)) - if err != nil { - return err + + return &invoiceData.Invoice, err +} + +func (c *Client) VoidInvoice(invoice Invoice) error { + if invoice.Id == "" { + return errors.New("missing invoice id") } - req.Header.Add("Content-Type", "application/json") - req.Header.Add("Accept", "application/json") - var res *http.Response - res, err = c.Client.Do(req) + + existingInvoice, err := c.FindInvoiceById(invoice.Id) if err != nil { return err } - defer res.Body.Close() - //var b, _ = ioutil.ReadAll(res.Body) - //log.Println(string(b)) - - // If the invoice was already deleted, QuickBooks returns 400 :( - // The response looks like this: - // {"Fault":{"Error":[{"Message":"Object Not Found","Detail":"Object Not Found : Something you're trying to use has been made inactive. Check the fields with accounts, invoices, items, vendors or employees.","code":"610","element":""}],"type":"ValidationFault"},"time":"2018-03-20T20:15:59.571-07:00"} - - // This is slightly horrifying and not documented in their API. When this - // happens we just return success; the goal of deleting it has been - // accomplished, just not by us. - if res.StatusCode == http.StatusBadRequest { - var r Failure - err = json.NewDecoder(res.Body).Decode(&r) - if err != nil { - return err - } - if r.Fault.Error[0].Message == "Object Not Found" { - return nil - } - } - if res.StatusCode != http.StatusOK { - return parseFailure(res) - } - // TODO they send something back, but is it useful? - return nil + invoice.SyncToken = existingInvoice.SyncToken + + return c.post("invoice", invoice, nil, map[string]string{"operation": "void"}) } diff --git a/item.go b/item.go index 427e349..1e88eb4 100644 --- a/item.go +++ b/item.go @@ -5,24 +5,23 @@ package quickbooks import ( "encoding/json" - "net/http" - "net/url" + "errors" "strconv" ) // Item represents a QuickBooks Item object (a product type). type Item struct { - ID string `json:"Id,omitempty"` - SyncToken string `json:",omitempty"` - //MetaData + Id string `json:"Id,omitempty"` + SyncToken string `json:",omitempty"` + MetaData MetaData `json:",omitempty"` Name string SKU string `json:"Sku,omitempty"` Description string `json:",omitempty"` Active bool `json:",omitempty"` - //SubItem - //ParentRef - //Level - //FullyQualifiedName + // SubItem + // ParentRef + // Level + // FullyQualifiedName Taxable bool `json:",omitempty"` SalesTaxIncluded bool `json:",omitempty"` UnitPrice json.Number `json:",omitempty"` @@ -34,69 +33,127 @@ type Item struct { PurchaseCost json.Number `json:",omitempty"` AssetAccountRef ReferenceType TrackQtyOnHand bool `json:",omitempty"` - //InvStartDate Date + // InvStartDate Date QtyOnHand json.Number `json:",omitempty"` SalesTaxCodeRef ReferenceType `json:",omitempty"` PurchaseTaxCodeRef ReferenceType `json:",omitempty"` } -// FetchItems returns the list of Items in the QuickBooks account. These are -// basically product types, and you need them to create invoices. -func (c *Client) FetchItems() ([]Item, error) { - var r struct { +func (c *Client) CreateItem(item *Item) (*Item, error) { + var resp struct { + Item Item + Time Date + } + + if err := c.post("item", item, &resp, nil); err != nil { + return nil, err + } + + return &resp.Item, nil +} + +// FindItems gets the full list of Items in the QuickBooks account. +func (c *Client) FindItems() ([]Item, error) { + var resp struct { QueryResponse struct { - Item []Item - StartPosition int + Items []Item `json:"Item"` MaxResults int + StartPosition int + TotalCount int } } - err := c.query("SELECT * FROM Item MAXRESULTS "+strconv.Itoa(queryPageSize), &r) - if err != nil { + + if err := c.query("SELECT COUNT(*) FROM Item", &resp); err != nil { return nil, err } - // Make sure we don't return nil if there are no items. - if r.QueryResponse.Item == nil { - r.QueryResponse.Item = make([]Item, 0) + if resp.QueryResponse.TotalCount == 0 { + return nil, errors.New("no items could be found") + } + + items := make([]Item, 0, resp.QueryResponse.TotalCount) + + for i := 0; i < resp.QueryResponse.TotalCount; i += queryPageSize { + query := "SELECT * FROM Item 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.Items == nil { + return nil, errors.New("no items could be found") + } + + items = append(items, resp.QueryResponse.Items...) } - return r.QueryResponse.Item, nil + + return items, nil } -// FetchItem returns just one particular Item from QuickBooks, by ID. -func (c *Client) FetchItem(id string) (*Item, error) { - var u, err = url.Parse(string(c.Endpoint)) - if err != nil { +// FindItemById returns an item with a given Id. +func (c *Client) FindItemById(id string) (*Item, error) { + var resp struct { + Item Item + Time Date + } + + if err := c.get("item/"+id, &resp, nil); err != nil { return nil, err } - u.Path = "/v3/company/" + c.RealmID + "/item/" + 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 &resp.Item, nil +} + +// QueryItems accepts an SQL query and returns all items found using it +func (c *Client) QueryItems(query string) ([]Item, error) { + var resp struct { + QueryResponse struct { + Items []Item `json:"Item"` + StartPosition int + MaxResults int + } + } + + if err := c.query(query, &resp); err != nil { return nil, err } - req.Header.Add("Accept", "application/json") - var res *http.Response - res, err = c.Client.Do(req) + + if resp.QueryResponse.Items == nil { + return nil, errors.New("could not find any items") + } + + return resp.QueryResponse.Items, nil +} + +// UpdateItem updates the item +func (c *Client) UpdateItem(item *Item) (*Item, error) { + if item.Id == "" { + return nil, errors.New("missing item id") + } + + existingItem, err := c.FindItemById(item.Id) if err != nil { return nil, err } - defer res.Body.Close() - if res.StatusCode != http.StatusOK { - return nil, parseFailure(res) + item.SyncToken = existingItem.SyncToken + + payload := struct { + *Item + Sparse bool `json:"sparse"` + }{ + Item: item, + Sparse: true, } - var r struct { + var itemData struct { Item Item Time Date } - err = json.NewDecoder(res.Body).Decode(&r) - if err != nil { + + if err = c.post("item", payload, &itemData, nil); err != nil { return nil, err } - return &r.Item, nil + + return &itemData.Item, err } diff --git a/memo.go b/memo.go deleted file mode 100644 index 004eef5..0000000 --- a/memo.go +++ /dev/null @@ -1,9 +0,0 @@ -// Copyright (c) 2018, Randy Westlund. All rights reserved. -// This code is under the BSD-2-Clause license. - -package quickbooks - -// MemoRef represents a QuickBooks MemoRef object. -type MemoRef struct { - Value string `json:"value,omitempty"` -} diff --git a/metadata.go b/metadata.go deleted file mode 100644 index 2509694..0000000 --- a/metadata.go +++ /dev/null @@ -1,7 +0,0 @@ -package quickbooks - -// MetaData is a timestamp of genesis and last change of a Quickbooks object -type MetaData struct { - CreateTime Date - LastUpdatedTime Date -} diff --git a/payment.go b/payment.go new file mode 100644 index 0000000..a1acc09 --- /dev/null +++ b/payment.go @@ -0,0 +1,170 @@ +package quickbooks + +import ( + "errors" + "strconv" +) + +type Payment struct { + SyncToken string `json:",omitempty"` + Domain string `json:"domain,omitempty"` + DepositToAccountRef ReferenceType `json:",omitempty"` + UnappliedAmt float64 `json:",omitempty"` + TxnDate Date `json:",omitempty"` + TotalAmt float64 `json:",omitempty"` + ProcessPayment bool `json:",omitempty"` + Line []PaymentLine `json:",omitempty"` + CustomerRef ReferenceType `json:",omitempty"` + Id string `json:",omitempty"` + MetaData MetaData `json:",omitempty"` +} + +type PaymentLine struct { + Amount float64 `json:",omitempty"` + LinkedTxn []LinkedTxn `json:",omitempty"` +} + +// CreatePayment creates the given payment within QuickBooks. +func (c *Client) CreatePayment(payment *Payment) (*Payment, error) { + var resp struct { + Payment Payment + Time Date + } + + if err := c.post("payment", payment, &resp, nil); err != nil { + return nil, err + } + + return &resp.Payment, nil +} + +// DeletePayment deletes the given payment from QuickBooks. +func (c *Client) DeletePayment(payment *Payment) error { + if payment.Id == "" || payment.SyncToken == "" { + return errors.New("missing id/sync token") + } + + return c.post("payment", payment, nil, map[string]string{"operation": "delete"}) +} + +// FindPayments gets the full list of Payments in the QuickBooks account. +func (c *Client) FindPayments() ([]Payment, error) { + var resp struct { + QueryResponse struct { + Payments []Payment `json:"Payment"` + MaxResults int + StartPosition int + TotalCount int + } + } + + if err := c.query("SELECT COUNT(*) FROM Payment", &resp); err != nil { + return nil, err + } + + if resp.QueryResponse.TotalCount == 0 { + return nil, errors.New("no payments could be found") + } + + payments := make([]Payment, 0, resp.QueryResponse.TotalCount) + + for i := 0; i < resp.QueryResponse.TotalCount; i += queryPageSize { + query := "SELECT * FROM Payment 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.Payments == nil { + return nil, errors.New("no payments could be found") + } + + payments = append(payments, resp.QueryResponse.Payments...) + } + + return payments, nil +} + +// FindPaymentById returns an payment with a given Id. +func (c *Client) FindPaymentById(id string) (*Payment, error) { + var resp struct { + Payment Payment + Time Date + } + + if err := c.get("payment/"+id, &resp, nil); err != nil { + return nil, err + } + + return &resp.Payment, nil +} + +// QueryPayments accepts a SQL query and returns all payments found using it. +func (c *Client) QueryPayments(query string) ([]Payment, error) { + var resp struct { + QueryResponse struct { + Payments []Payment `json:"Payment"` + StartPosition int + MaxResults int + } + } + + if err := c.query(query, &resp); err != nil { + return nil, err + } + + if resp.QueryResponse.Payments == nil { + return nil, errors.New("could not find any payments") + } + + return resp.QueryResponse.Payments, nil +} + +// UpdatePayment updates the given payment in QuickBooks. +func (c *Client) UpdatePayment(payment *Payment) (*Payment, error) { + if payment.Id == "" { + return nil, errors.New("missing payment id") + } + + existingPayment, err := c.FindPaymentById(payment.Id) + if err != nil { + return nil, err + } + + payment.SyncToken = existingPayment.SyncToken + + payload := struct { + *Payment + Sparse bool `json:"sparse"` + }{ + Payment: payment, + Sparse: true, + } + + var paymentData struct { + Payment Payment + Time Date + } + + if err = c.post("payment", payload, &paymentData, nil); err != nil { + return nil, err + } + + return &paymentData.Payment, err +} + +// VoidPayment voids the given payment in QuickBooks. +func (c *Client) VoidPayment(payment Payment) error { + if payment.Id == "" { + return errors.New("missing payment id") + } + + existingPayment, err := c.FindPaymentById(payment.Id) + if err != nil { + return err + } + + payment.SyncToken = existingPayment.SyncToken + + return c.post("payment", payment, nil, map[string]string{"operation": "update", "include": "void"}) +} diff --git a/reference.go b/reference.go deleted file mode 100644 index 09b50ea..0000000 --- a/reference.go +++ /dev/null @@ -1,11 +0,0 @@ -// Copyright (c) 2018, Randy Westlund. All rights reserved. -// This code is under the BSD-2-Clause license. - -package quickbooks - -// ReferenceType represents a QuickBooks reference to another object. -type ReferenceType struct { - Value string `json:"value,omitempty"` - Name string `json:"name,omitempty"` - Type string `json:"type,omitempty"` -} diff --git a/telephone.go b/telephone.go deleted file mode 100644 index 65438b9..0000000 --- a/telephone.go +++ /dev/null @@ -1,9 +0,0 @@ -// Copyright (c) 2018, Randy Westlund. All rights reserved. -// This code is under the BSD-2-Clause license. - -package quickbooks - -// TelephoneNumber represents a QuickBooks phone number. -type TelephoneNumber struct { - FreeFormNumber string -} diff --git a/token.go b/token.go index a20988d..6967366 100644 --- a/token.go +++ b/token.go @@ -6,10 +6,11 @@ import ( "encoding/base64" "encoding/json" "errors" - "golang.org/x/oauth2" "io/ioutil" "net/http" "net/url" + + "golang.org/x/oauth2" ) type BearerToken struct { @@ -21,29 +22,30 @@ type BearerToken struct { XRefreshTokenExpiresIn int64 `json:"x_refresh_token_expires_in"` } -// -// Method to retrieve access token (bearer token) -// This method can only be called once -// -func (c *Client) RetrieveBearerToken(authorizationCode, redirectURI string) (*BearerToken, error) { +// RefreshToken +// Call the refresh endpoint to generate new tokens +func (c *Client) RefreshToken(refreshToken string) (*BearerToken, error) { client := &http.Client{} - data := url.Values{} - //set parameters - data.Set("grant_type", "authorization_code") - data.Add("code", authorizationCode) - data.Add("redirect_uri", redirectURI) + urlValues := url.Values{} + urlValues.Set("grant_type", "refresh_token") + urlValues.Add("refresh_token", refreshToken) + + req, err := http.NewRequest("POST", c.discoveryAPI.TokenEndpoint, bytes.NewBufferString(urlValues.Encode())) + if err != nil { + return nil, err + } + + req.Header.Set("Accept", "application/json") + req.Header.Set("Content-Type", "application/x-www-form-urlencoded;charset=UTF-8") + req.Header.Set("Authorization", "Basic "+basicAuth(c)) - request, err := http.NewRequest("POST", string(c.discoveryAPI.TokenEndpoint), bytes.NewBufferString(data.Encode())) + resp, err := client.Do(req) if err != nil { return nil, err } - //set headers - request.Header.Set("accept", "application/json") - request.Header.Set("Content-Type", "application/x-www-form-urlencoded;charset=UTF-8") - request.Header.Set("Authorization", "Basic "+basicAuth(c)) - resp, err := client.Do(request) defer resp.Body.Close() + body, err := ioutil.ReadAll(resp.Body) if err != nil { return nil, err @@ -53,68 +55,76 @@ func (c *Client) RetrieveBearerToken(authorizationCode, redirectURI string) (*Be return nil, errors.New(string(body)) } - bearerTokenResponse, err := getBearerTokenResponse([]byte(body)) + bearerTokenResponse, err := getBearerTokenResponse(body) + c.Client = getHttpClient(bearerTokenResponse) + return bearerTokenResponse, err } -// -// Call the refresh endpoint to generate new tokens -// -func (c *Client) RefreshToken(refreshToken string) (*BearerToken, error) { +// RetrieveBearerToken +// Method to retrieve access token (bearer token). +// This method can only be called once +func (c *Client) RetrieveBearerToken(authorizationCode, redirectURI string) (*BearerToken, error) { client := &http.Client{} - data := url.Values{} + urlValues := url.Values{} + // set parameters + urlValues.Add("code", authorizationCode) + urlValues.Set("grant_type", "authorization_code") + urlValues.Add("redirect_uri", redirectURI) + + req, err := http.NewRequest("POST", c.discoveryAPI.TokenEndpoint, bytes.NewBufferString(urlValues.Encode())) + if err != nil { + return nil, err + } - //add parameters - data.Set("grant_type", "refresh_token") - data.Add("refresh_token", refreshToken) + req.Header.Set("Accept", "application/json") + req.Header.Set("Content-Type", "application/x-www-form-urlencoded;charset=UTF-8") + req.Header.Set("Authorization", "Basic "+basicAuth(c)) - request, err := http.NewRequest("POST", string(c.discoveryAPI.TokenEndpoint), bytes.NewBufferString(data.Encode())) + resp, err := client.Do(req) if err != nil { return nil, err } - //set the headers - request.Header.Set("accept", "application/json") - request.Header.Set("Content-Type", "application/x-www-form-urlencoded;charset=UTF-8") - request.Header.Set("Authorization", "Basic "+basicAuth(c)) - resp, err := client.Do(request) defer resp.Body.Close() + body, err := ioutil.ReadAll(resp.Body) if err != nil { return nil, err } if resp.StatusCode != http.StatusOK { - return nil, errors.New(string(body)) + return nil, parseFailure(resp) } - bearerTokenResponse, err := getBearerTokenResponse([]byte(body)) - c.Client = getHttpClient(bearerTokenResponse) + bearerTokenResponse, err := getBearerTokenResponse(body) + return bearerTokenResponse, err } -// +// RevokeToken // Call the revoke endpoint to revoke tokens -// func (c *Client) RevokeToken(refreshToken string) error { client := &http.Client{} - data := url.Values{} + urlValues := url.Values{} + urlValues.Add("token", refreshToken) + + req, err := http.NewRequest("POST", c.discoveryAPI.RevocationEndpoint, bytes.NewBufferString(urlValues.Encode())) + if err != nil { + return err + } - //add parameters - data.Add("token", refreshToken) + req.Header.Set("Accept", "application/json") + req.Header.Set("Content-Type", "application/x-www-form-urlencoded;charset=UTF-8") + req.Header.Set("Authorization", "Basic "+basicAuth(c)) - revokeEndpoint := c.discoveryAPI.RevocationEndpoint - request, err := http.NewRequest("POST", revokeEndpoint, bytes.NewBufferString(data.Encode())) + resp, err := client.Do(req) if err != nil { return err } - //set headers - request.Header.Set("accept", "application/json") - request.Header.Set("Content-Type", "application/x-www-form-urlencoded;charset=UTF-8") - request.Header.Set("Authorization", "Basic "+basicAuth(c)) - resp, err := client.Do(request) defer resp.Body.Close() + body, err := ioutil.ReadAll(resp.Body) if err != nil { return err @@ -125,21 +135,22 @@ func (c *Client) RevokeToken(refreshToken string) error { } c.Client = nil + return nil } +func basicAuth(c *Client) string { + return base64.StdEncoding.EncodeToString([]byte(c.clientId + ":" + c.clientSecret)) +} + func getBearerTokenResponse(body []byte) (*BearerToken, error) { - var s = new(BearerToken) - err := json.Unmarshal(body, &s) - if err != nil { + token := BearerToken{} + + if err := json.Unmarshal(body, &token); err != nil { return nil, errors.New(string(body)) } - return s, err -} -func basicAuth(c *Client) string { - auth := c.clientId + ":" + c.clientSecret - return base64.StdEncoding.EncodeToString([]byte(auth)) + return &token, nil } func getHttpClient(bearerToken *BearerToken) *http.Client { diff --git a/vendor.go b/vendor.go index a76689b..8c956a2 100644 --- a/vendor.go +++ b/vendor.go @@ -1,16 +1,14 @@ package quickbooks import ( - "bytes" "encoding/json" - "net/http" - "net/url" + "errors" "strconv" ) // Vendor describes a vendor. type Vendor struct { - ID string `json:"Id,omitempty"` + Id string `json:"Id,omitempty"` SyncToken string `json:",omitempty"` Title string `json:",omitempty"` GivenName string `json:",omitempty"` @@ -20,12 +18,12 @@ type Vendor struct { PrimaryEmailAddr EmailAddress `json:",omitempty"` DisplayName string `json:",omitempty"` // ContactInfo - APAccountRef ReferenceType `json:",omitempty"` - TermRef ReferenceType `json:",omitempty"` - GSTIN string `json:",omitempty"` - Fax TelephoneNumber `json:",omitempty"` - BusinessNumber string `json:",omitempty"` - // CurrencyRef + APAccountRef ReferenceType `json:",omitempty"` + TermRef ReferenceType `json:",omitempty"` + GSTIN string `json:",omitempty"` + Fax TelephoneNumber `json:",omitempty"` + BusinessNumber string `json:",omitempty"` + CurrencyRef ReferenceType `json:",omitempty"` HasTPAR bool `json:",omitempty"` TaxReportingBasis string `json:",omitempty"` Mobile TelephoneNumber `json:",omitempty"` @@ -46,114 +44,123 @@ type Vendor struct { Balance json.Number `json:",omitempty"` } -// GetVendors gets the vendors -func (c *Client) GetVendors(startpos int) ([]Vendor, error) { +// CreateVendor creates the given Vendor on the QuickBooks server, returning +// the resulting Vendor object. +func (c *Client) CreateVendor(vendor *Vendor) (*Vendor, error) { + var resp struct { + Vendor Vendor + Time Date + } + + if err := c.post("vendor", vendor, &resp, nil); err != nil { + return nil, err + } + + return &resp.Vendor, nil +} - var r struct { +// FindVendors gets the full list of Vendors in the QuickBooks account. +func (c *Client) FindVendors() ([]Vendor, error) { + var resp struct { QueryResponse struct { - Vendor []Vendor - StartPosition int + Vendors []Vendor `json:"Vendor"` MaxResults int + StartPosition int + TotalCount int } } - q := "SELECT * FROM Vendor ORDERBY Id STARTPOSITION " + - strconv.Itoa(startpos) + " MAXRESULTS " + strconv.Itoa(queryPageSize) - err := c.query(q, &r) - if err != nil { + + if err := c.query("SELECT COUNT(*) FROM Vendor", &resp); err != nil { return nil, err } - if r.QueryResponse.Vendor == nil { - r.QueryResponse.Vendor = make([]Vendor, 0) + if resp.QueryResponse.TotalCount == 0 { + return nil, errors.New("no vendors could be found") } - return r.QueryResponse.Vendor, nil + + vendors := make([]Vendor, 0, resp.QueryResponse.TotalCount) + + for i := 0; i < resp.QueryResponse.TotalCount; i += queryPageSize { + query := "SELECT * FROM Vendor 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.Vendors == nil { + return nil, errors.New("no vendors could be found") + } + + vendors = append(vendors, resp.QueryResponse.Vendors...) + } + + return vendors, nil } -// CreateVendor creates the vendor -func (c *Client) CreateVendor(vendor *Vendor) (*Vendor, error) { - var u, err = url.Parse(string(c.Endpoint)) - if err != nil { - return nil, err +// FindVendorById finds the vendor by the given id +func (c *Client) FindVendorById(id string) (*Vendor, error) { + var resp struct { + Vendor Vendor + Time Date } - u.Path = "/v3/company/" + c.RealmID + "/vendor" - var v = url.Values{} - v.Add("minorversion", minorVersion) - u.RawQuery = v.Encode() - var j []byte - j, err = json.Marshal(vendor) - if err != nil { + + if err := c.get("vendor/"+id, &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.Vendor, nil +} + +// QueryVendors accepts an SQL query and returns all vendors found using it +func (c *Client) QueryVendors(query string) ([]Vendor, error) { + var resp struct { + QueryResponse struct { + Vendors []Vendor `json:"Vendor"` + StartPosition int + MaxResults 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(query, &resp); err != nil { return nil, err } - defer res.Body.Close() - if res.StatusCode != http.StatusOK { - return nil, parseFailure(res) + if resp.QueryResponse.Vendors == nil { + return nil, errors.New("could not find any vendors") } - var r struct { - Vendor Vendor - Time Date - } - err = json.NewDecoder(res.Body).Decode(&r) - return &r.Vendor, err + return resp.QueryResponse.Vendors, nil } // UpdateVendor updates the vendor func (c *Client) UpdateVendor(vendor *Vendor) (*Vendor, error) { - var u, err = url.Parse(string(c.Endpoint)) + if vendor.Id == "" { + return nil, errors.New("missing vendor id") + } + + existingVendor, err := c.FindVendorById(vendor.Id) if err != nil { return nil, err } - u.Path = "/v3/company/" + c.RealmID + "/vendor" - var v = url.Values{} - v.Add("minorversion", minorVersion) - u.RawQuery = v.Encode() - var d = struct { + + vendor.SyncToken = existingVendor.SyncToken + + payload := struct { *Vendor Sparse bool `json:"sparse"` }{ Vendor: vendor, 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 vendorData struct { Vendor Vendor Time Date } - err = json.NewDecoder(res.Body).Decode(&r) - return &r.Vendor, err + + if err = c.post("vendor", payload, &vendorData, nil); err != nil { + return nil, err + } + + return &vendorData.Vendor, err } diff --git a/vendor_test.go b/vendor_test.go index 523247f..d4fd957 100644 --- a/vendor_test.go +++ b/vendor_test.go @@ -18,27 +18,27 @@ func TestVendor(t *testing.T) { byteValue, err := ioutil.ReadAll(jsonFile) require.NoError(t, err) - var r struct { + var resp struct { Vendor Vendor Time Date } - err = json.Unmarshal(byteValue, &r) - require.NoError(t, err) - assert.NotNil(t, r.Vendor.PrimaryEmailAddr) - assert.False(t, r.Vendor.Vendor1099) - assert.Equal(t, "Bessie", r.Vendor.GivenName) - assert.Equal(t, "Books by Bessie", r.Vendor.DisplayName) - assert.NotNil(t, r.Vendor.BillAddr) - assert.Equal(t, "0", r.Vendor.SyncToken) - assert.Equal(t, "Books by Bessie", r.Vendor.PrintOnCheckName) - assert.Equal(t, "Williams", r.Vendor.FamilyName) - assert.NotNil(t, r.Vendor.PrimaryPhone) - assert.Equal(t, "1345", r.Vendor.AcctNum) - assert.Equal(t, "Books by Bessie", r.Vendor.CompanyName) - assert.NotNil(t, r.Vendor.WebAddr) - assert.True(t, r.Vendor.Active) - assert.Equal(t, "0", r.Vendor.Balance.String()) - assert.Equal(t, "30", r.Vendor.ID) - assert.Equal(t, "2014-09-12T10:07:56-07:00", r.Vendor.MetaData.CreateTime.String()) - assert.Equal(t, "2014-09-17T11:13:46-07:00", r.Vendor.MetaData.LastUpdatedTime.String()) + + require.NoError(t, json.Unmarshal(byteValue, &resp)) + assert.NotNil(t, resp.Vendor.PrimaryEmailAddr) + assert.False(t, resp.Vendor.Vendor1099) + assert.Equal(t, "Bessie", resp.Vendor.GivenName) + assert.Equal(t, "Books by Bessie", resp.Vendor.DisplayName) + assert.NotNil(t, resp.Vendor.BillAddr) + assert.Equal(t, "0", resp.Vendor.SyncToken) + assert.Equal(t, "Books by Bessie", resp.Vendor.PrintOnCheckName) + assert.Equal(t, "Williams", resp.Vendor.FamilyName) + assert.NotNil(t, resp.Vendor.PrimaryPhone) + assert.Equal(t, "1345", resp.Vendor.AcctNum) + assert.Equal(t, "Books by Bessie", resp.Vendor.CompanyName) + assert.NotNil(t, resp.Vendor.WebAddr) + assert.True(t, resp.Vendor.Active) + assert.Equal(t, "0", resp.Vendor.Balance.String()) + assert.Equal(t, "30", resp.Vendor.Id) + assert.Equal(t, "2014-09-12T10:07:56-07:00", resp.Vendor.MetaData.CreateTime.String()) + assert.Equal(t, "2014-09-17T11:13:46-07:00", resp.Vendor.MetaData.LastUpdatedTime.String()) } diff --git a/website.go b/website.go deleted file mode 100644 index a776ccb..0000000 --- a/website.go +++ /dev/null @@ -1,6 +0,0 @@ -package quickbooks - -// WebSiteAddress represents a Quickbooks Website -type WebSiteAddress struct { - URI string -}