From 66bfc87bc9cf546e1ac5f73e2664d7e29c79b546 Mon Sep 17 00:00:00 2001 From: Robert Thomas <31854736+wolveix@users.noreply.github.com> Date: Sun, 18 Sep 2022 17:17:53 +0100 Subject: [PATCH 1/6] Major refactor Additional objects implemented Standardized function naming Fixed missing error handling Support and enforce QB's rate limiting Bumped Go to 1.18 as 1.14 is EOL Ran `gofumpt -w` over the repo to cleanup code formatting --- README.md | 41 ++++- account.go | 187 +++++++++----------- account_test.go | 7 +- address.go | 22 --- attachable.go | 304 ++++++++++++++------------------ attachable_test.go | 13 +- bill.go | 145 ++++++++++++--- bill_test.go | 2 +- client.go | 200 ++++++++++++++------- company.go | 63 ++++++- customer.go | 270 +++++++++++----------------- customer_type.go | 50 ++++++ date.go | 31 ---- defs.go | 107 ++++++++++-- deposit.go | 146 ++++++++++++++++ discovery.go | 57 +++--- email.go | 9 - employee.go | 143 +++++++++++++++ errors.go | 33 ++-- estimate.go | 185 ++++++++++++++++++++ examples/auth_flow_test.go | 9 +- examples/reuse_token_test.go | 7 +- go.mod | 12 +- go.sum | 1 - invoice.go | 329 ++++++++++++++++++----------------- item.go | 143 ++++++++++----- memo.go | 9 - metadata.go | 7 - payment.go | 174 ++++++++++++++++++ reference.go | 11 -- telephone.go | 9 - token.go | 121 +++++++------ vendor.go | 173 +++++++++--------- vendor_test.go | 40 ++--- website.go | 6 - 35 files changed, 1969 insertions(+), 1097 deletions(-) delete mode 100644 address.go create mode 100644 customer_type.go delete mode 100644 date.go create mode 100644 deposit.go delete mode 100644 email.go create mode 100644 employee.go create mode 100644 estimate.go delete mode 100644 memo.go delete mode 100644 metadata.go create mode 100644 payment.go delete mode 100644 reference.go delete mode 100644 telephone.go delete mode 100644 website.go 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..5527841 100644 --- a/client.go +++ b/client.go @@ -14,18 +14,22 @@ use case. Pull requests welcome :) var qb = quickbooks.Client{ Client: client, Endpoint: quickbooks.SandboxEndpoint, - RealmID: "some company account ID"' + RealmId: "some company account Id"' } // Make a request! - var companyInfo, err = qb.FetchCompanyInfo() + var companyInfo, err = qb.FindCompanyInfo() */ package quickbooks import ( + "bytes" "encoding/json" + "errors" + "fmt" "net/http" "net/url" + "time" ) // Client is your handle to the QuickBooks API. @@ -33,99 +37,165 @@ 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) + break + default: + return parseFailure(resp) } - return json.NewDecoder(res.Body).Decode(out) + 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 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/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..b280e37 --- /dev/null +++ b/customer_type.go @@ -0,0 +1,50 @@ +package quickbooks + +import ( + "errors" +) + +type CustomerType struct { + SyncToken string `json:",omitempty"` + Domain string `json:"domain,omitempty"` + Name string `json:",omitempty"` + Sparse bool `json:"sparse,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..c02f977 --- /dev/null +++ b/deposit.go @@ -0,0 +1,146 @@ +package quickbooks + +import ( + "errors" + "strconv" +) + +type Deposit struct { + SyncToken string `json:",omitempty"` + Domain string `json:"domain,omitempty"` + DepositToAccountRef ReferenceType `json:",omitempty"` + TxnDate string `json:",omitempty"` + TotalAmt float64 `json:",omitempty"` + Sparse bool `json:"sparse,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..c957119 --- /dev/null +++ b/employee.go @@ -0,0 +1,143 @@ +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"` + Sparse bool `json:"sparse,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..4f0bc02 --- /dev/null +++ b/estimate.go @@ -0,0 +1,185 @@ +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 string `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"` + Sparse bool `json:"sparse,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..8be5c53 100644 --- a/go.mod +++ b/go.mod @@ -1,9 +1,19 @@ module github.com/rwestlund/quickbooks-go -go 1.14 +go 1.18 require ( github.com/stretchr/testify v1.6.1 golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5 gopkg.in/guregu/null.v4 v4.0.0 ) + +require ( + github.com/davecgh/go-spew v1.1.0 // indirect + github.com/golang/protobuf v1.4.2 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + golang.org/x/net v0.0.0-20200822124328-c89045814202 // indirect + google.golang.org/appengine v1.6.6 // indirect + google.golang.org/protobuf v1.25.0 // indirect + gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c // indirect +) diff --git a/go.sum b/go.sum index e71bc36..94768b8 100644 --- a/go.sum +++ b/go.sum @@ -352,7 +352,6 @@ google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpAD 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= -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= diff --git a/invoice.go b/invoice.go index 95575d1..9643d35 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,6 +53,16 @@ type Invoice struct { DepositToAccountRef ReferenceType `json:",omitempty"` } +type DeliveryInfo struct { + DeliveryType string + DeliveryTime Date +} + +type LinkedTxn struct { + TxnID string `json:"TxnId"` + TxnType string `json:"TxnType"` +} + // TxnTaxDetail ... type TxnTaxDetail struct { TxnTaxCodeRef ReferenceType `json:",omitempty"` @@ -66,17 +74,17 @@ type TxnTaxDetail struct { 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 @@ -91,8 +99,8 @@ type Line struct { 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 +110,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 +126,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..5b328b2 --- /dev/null +++ b/payment.go @@ -0,0 +1,174 @@ +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 string `json:",omitempty"` + TotalAmt float64 `json:",omitempty"` + ProcessPayment bool `json:",omitempty"` + Sparse bool `json:"sparse,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 []struct { + TxnLineId string `json:",omitempty"` + TxnId string `json:",omitempty"` + TxnType string `json:",omitempty"` + } `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 +} + +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 an 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 payment +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 +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 -} From 906fdd46d34fb6609e711553057ab0db8bea3e0e Mon Sep 17 00:00:00 2001 From: Robert Thomas <31854736+wolveix@users.noreply.github.com> Date: Mon, 19 Sep 2022 03:20:04 +0100 Subject: [PATCH 2/6] Fix incorrect Payment.TxnDate type --- payment.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/payment.go b/payment.go index 5b328b2..eaf6a66 100644 --- a/payment.go +++ b/payment.go @@ -10,7 +10,7 @@ type Payment struct { Domain string `json:"domain,omitempty"` DepositToAccountRef ReferenceType `json:",omitempty"` UnappliedAmt float64 `json:",omitempty"` - TxnDate string `json:",omitempty"` + TxnDate Date `json:",omitempty"` TotalAmt float64 `json:",omitempty"` ProcessPayment bool `json:",omitempty"` Sparse bool `json:"sparse,omitempty"` From d7868b05e0cfdc7b0d4416dbdf6148fa133e4464 Mon Sep 17 00:00:00 2001 From: Robert Thomas <31854736+wolveix@users.noreply.github.com> Date: Mon, 19 Sep 2022 03:29:15 +0100 Subject: [PATCH 3/6] Fix minor errors with new entities --- customer_type.go | 1 - deposit.go | 3 +-- employee.go | 1 - estimate.go | 3 +-- payment.go | 1 - 5 files changed, 2 insertions(+), 7 deletions(-) diff --git a/customer_type.go b/customer_type.go index b280e37..cd742cc 100644 --- a/customer_type.go +++ b/customer_type.go @@ -8,7 +8,6 @@ type CustomerType struct { SyncToken string `json:",omitempty"` Domain string `json:"domain,omitempty"` Name string `json:",omitempty"` - Sparse bool `json:"sparse,omitempty"` Active bool `json:",omitempty"` Id string `json:",omitempty"` MetaData MetaData `json:",omitempty"` diff --git a/deposit.go b/deposit.go index c02f977..e56eb41 100644 --- a/deposit.go +++ b/deposit.go @@ -9,9 +9,8 @@ type Deposit struct { SyncToken string `json:",omitempty"` Domain string `json:"domain,omitempty"` DepositToAccountRef ReferenceType `json:",omitempty"` - TxnDate string `json:",omitempty"` + TxnDate Date `json:",omitempty"` TotalAmt float64 `json:",omitempty"` - Sparse bool `json:"sparse,omitempty"` Line []PaymentLine `json:",omitempty"` Id string `json:",omitempty"` MetaData MetaData `json:",omitempty"` diff --git a/employee.go b/employee.go index c957119..fe16cb2 100644 --- a/employee.go +++ b/employee.go @@ -15,7 +15,6 @@ type Employee struct { Active bool `json:",omitempty"` SSN string `json:",omitempty"` PrimaryAddr PhysicalAddress `json:",omitempty"` - Sparse bool `json:"sparse,omitempty"` BillableTime bool `json:",omitempty"` GivenName string `json:",omitempty"` Id string `json:",omitempty"` diff --git a/estimate.go b/estimate.go index 4f0bc02..b22b856 100644 --- a/estimate.go +++ b/estimate.go @@ -11,14 +11,13 @@ type Estimate struct { Domain string `json:"domain,omitempty"` TxnStatus string `json:",omitempty"` BillEmail EmailAddress `json:",omitempty"` - TxnDate string `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"` - Sparse bool `json:"sparse,omitempty"` EmailStatus string `json:",omitempty"` Line []Line `json:",omitempty"` ApplyTaxAfterDiscount bool `json:",omitempty"` diff --git a/payment.go b/payment.go index eaf6a66..78402a4 100644 --- a/payment.go +++ b/payment.go @@ -13,7 +13,6 @@ type Payment struct { TxnDate Date `json:",omitempty"` TotalAmt float64 `json:",omitempty"` ProcessPayment bool `json:",omitempty"` - Sparse bool `json:"sparse,omitempty"` Line []PaymentLine `json:",omitempty"` CustomerRef ReferenceType `json:",omitempty"` Id string `json:",omitempty"` From 055f8a4e4f8d2de47436b7fdf7786ad63b05453b Mon Sep 17 00:00:00 2001 From: Robert Thomas <31854736+wolveix@users.noreply.github.com> Date: Mon, 19 Sep 2022 03:36:50 +0100 Subject: [PATCH 4/6] Remove unnecessary struct declaration --- payment.go | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/payment.go b/payment.go index 78402a4..986152d 100644 --- a/payment.go +++ b/payment.go @@ -20,14 +20,9 @@ type Payment struct { } type PaymentLine struct { - Amount float64 `json:",omitempty"` - LinkedTxn []struct { - TxnLineId string `json:",omitempty"` - TxnId string `json:",omitempty"` - TxnType string `json:",omitempty"` - } `json:",omitempty"` + 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 { From 47bae90fe253bbf07c5aed1577a6f060c8a81690 Mon Sep 17 00:00:00 2001 From: Robert Thomas <31854736+wolveix@users.noreply.github.com> Date: Fri, 5 Apr 2024 11:31:54 +0100 Subject: [PATCH 5/6] Update branch from changes on `main` --- client.go | 21 --- credit_memo.go | 162 +++++++++++++++++++++ go.mod | 14 +- go.sum | 378 ++----------------------------------------------- invoice.go | 13 +- payment.go | 10 +- 6 files changed, 188 insertions(+), 410 deletions(-) create mode 100644 credit_memo.go diff --git a/client.go b/client.go index 5527841..f5cf08e 100644 --- a/client.go +++ b/client.go @@ -1,25 +1,5 @@ // 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.FindCompanyInfo() -*/ package quickbooks import ( @@ -173,7 +153,6 @@ func (c *Client) req(method string, endpoint string, payloadData interface{}, re time.Sleep(1 * time.Minute) c.throttled = false }(c) - break default: return parseFailure(resp) } diff --git a/credit_memo.go b/credit_memo.go new file mode 100644 index 0000000..56cf3e8 --- /dev/null +++ b/credit_memo.go @@ -0,0 +1,162 @@ +package quickbooks + +import ( + "encoding/json" + "errors" + "strconv" +) + +type CreditMemo struct { + SyncToken string `json:",omitempty"` + DocNumber string + CustomMemo string `json:",omitempty"` + TxnDate *Date `json:",omitempty"` + TotalAmt json.Number `json:",omitempty"` + CustomRef *ReferenceType `json:",omitempty"` + Line []Line + CurrencyRef *ReferenceType `json:",omitempty"` + APAccountRef *ReferenceType `json:",omitempty"` + SalesTermRef *ReferenceType `json:",omitempty"` + LinkedTxn []LinkedTxn `json:",omitempty"` + TransactionLocationType string `json:",omitempty"` + DueDate Date `json:",omitempty"` + TxnTaxDetail *TxnTaxDetail `json:",omitempty"` + ExchangeRate json.Number `json:",omitempty"` + DepartmentRef *ReferenceType `json:",omitempty"` + IncludeInAnnualTPAR bool `json:",omitempty"` + HomeBalance json.Number `json:",omitempty"` + RecurDataRef *ReferenceType `json:",omitempty"` + Balance json.Number `json:",omitempty"` + Id string `json:",omitempty"` + MetaData MetaData `json:",omitempty"` +} + +// CreateCreditMemo creates the given CreditMemo on the QuickBooks server, returning +// the resulting CreditMemo object. +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 creditMemos 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 creditMemos 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 creditMemos") + } + + 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 creditMemo 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/go.mod b/go.mod index 8be5c53..ec22901 100644 --- a/go.mod +++ b/go.mod @@ -1,19 +1,15 @@ module github.com/rwestlund/quickbooks-go -go 1.18 +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.0 // indirect - github.com/golang/protobuf v1.4.2 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - golang.org/x/net v0.0.0-20200822124328-c89045814202 // indirect - google.golang.org/appengine v1.6.6 // indirect - google.golang.org/protobuf v1.25.0 // indirect - gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 94768b8..a260b36 100644 --- a/go.sum +++ b/go.sum @@ -1,373 +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 9643d35..254071f 100644 --- a/invoice.go +++ b/invoice.go @@ -44,7 +44,7 @@ type Invoice struct { BillEmail EmailAddress `json:",omitempty"` BillEmailCC EmailAddress `json:"BillEmailCc,omitempty"` BillEmailBCC EmailAddress `json:"BillEmailBcc,omitempty"` - DeliveryInfo DeliveryInfo `json:",omitempty"` + DeliveryInfo *DeliveryInfo `json:",omitempty"` Balance json.Number `json:",omitempty"` TxnSource string `json:",omitempty"` AllowOnlineCreditCardPayment bool `json:",omitempty"` @@ -63,14 +63,12 @@ type LinkedTxn struct { TxnType string `json:"TxnType"` } -// TxnTaxDetail ... 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"` @@ -82,17 +80,16 @@ type AccountBasedExpenseLineDetail struct { // CustomerRef ReferenceType `json:",omitempty"` } -// Line ... type Line struct { 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 ... diff --git a/payment.go b/payment.go index 986152d..a1acc09 100644 --- a/payment.go +++ b/payment.go @@ -23,7 +23,8 @@ type PaymentLine struct { Amount float64 `json:",omitempty"` LinkedTxn []LinkedTxn `json:",omitempty"` } -// CreatePayment creates the given payment within QuickBooks + +// CreatePayment creates the given payment within QuickBooks. func (c *Client) CreatePayment(payment *Payment) (*Payment, error) { var resp struct { Payment Payment @@ -37,6 +38,7 @@ func (c *Client) CreatePayment(payment *Payment) (*Payment, error) { 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") @@ -97,7 +99,7 @@ func (c *Client) FindPaymentById(id string) (*Payment, error) { return &resp.Payment, nil } -// QueryPayments accepts an SQL query and returns all payments found using it +// 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 { @@ -118,7 +120,7 @@ func (c *Client) QueryPayments(query string) ([]Payment, error) { return resp.QueryResponse.Payments, nil } -// UpdatePayment updates the payment +// 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") @@ -151,7 +153,7 @@ func (c *Client) UpdatePayment(payment *Payment) (*Payment, error) { return &paymentData.Payment, err } -// VoidPayment voids the given payment +// VoidPayment voids the given payment in QuickBooks. func (c *Client) VoidPayment(payment Payment) error { if payment.Id == "" { return errors.New("missing payment id") From e53208f5ad8975be17b4d0304063527c02ddd516 Mon Sep 17 00:00:00 2001 From: Robert Thomas <31854736+wolveix@users.noreply.github.com> Date: Mon, 15 Apr 2024 14:52:53 +0100 Subject: [PATCH 6/6] Fix credit memos + bump Go version in GH action --- .github/workflows/build.yml | 2 +- credit_memo.go | 61 ++++++++++++++++++------------------- 2 files changed, 30 insertions(+), 33 deletions(-) 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/credit_memo.go b/credit_memo.go index 56cf3e8..8c2744a 100644 --- a/credit_memo.go +++ b/credit_memo.go @@ -7,39 +7,36 @@ import ( ) type CreditMemo struct { - SyncToken string `json:",omitempty"` - DocNumber string - CustomMemo string `json:",omitempty"` - TxnDate *Date `json:",omitempty"` - TotalAmt json.Number `json:",omitempty"` - CustomRef *ReferenceType `json:",omitempty"` - Line []Line - CurrencyRef *ReferenceType `json:",omitempty"` - APAccountRef *ReferenceType `json:",omitempty"` - SalesTermRef *ReferenceType `json:",omitempty"` - LinkedTxn []LinkedTxn `json:",omitempty"` - TransactionLocationType string `json:",omitempty"` - DueDate Date `json:",omitempty"` - TxnTaxDetail *TxnTaxDetail `json:",omitempty"` - ExchangeRate json.Number `json:",omitempty"` - DepartmentRef *ReferenceType `json:",omitempty"` - IncludeInAnnualTPAR bool `json:",omitempty"` - HomeBalance json.Number `json:",omitempty"` - RecurDataRef *ReferenceType `json:",omitempty"` - Balance json.Number `json:",omitempty"` - Id string `json:",omitempty"` - MetaData MetaData `json:",omitempty"` + 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 on the QuickBooks server, returning -// the resulting CreditMemo object. +// 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 { + if err := c.post("creditmemo", creditMemo, &resp, nil); err != nil { return nil, err } @@ -52,7 +49,7 @@ func (c *Client) DeleteCreditMemo(creditMemo *CreditMemo) error { return errors.New("missing id/sync token") } - return c.post("creditMemo", creditMemo, nil, map[string]string{"operation": "delete"}) + return c.post("creditmemo", creditMemo, nil, map[string]string{"operation": "delete"}) } // FindCreditMemos retrieves the full list of credit memos from QuickBooks. @@ -71,7 +68,7 @@ func (c *Client) FindCreditMemos() ([]CreditMemo, error) { } if resp.QueryResponse.TotalCount == 0 { - return nil, errors.New("no creditMemos could be found") + return nil, errors.New("no credit memos could be found") } creditMemos := make([]CreditMemo, 0, resp.QueryResponse.TotalCount) @@ -84,7 +81,7 @@ func (c *Client) FindCreditMemos() ([]CreditMemo, error) { } if resp.QueryResponse.CreditMemos == nil { - return nil, errors.New("no creditMemos could be found") + return nil, errors.New("no credit memos could be found") } creditMemos = append(creditMemos, resp.QueryResponse.CreditMemos...) @@ -100,7 +97,7 @@ func (c *Client) FindCreditMemoById(id string) (*CreditMemo, error) { Time Date } - if err := c.get("creditMemo/"+id, &resp, nil); err != nil { + if err := c.get("creditmemo/"+id, &resp, nil); err != nil { return nil, err } @@ -122,7 +119,7 @@ func (c *Client) QueryCreditMemos(query string) ([]CreditMemo, error) { } if resp.QueryResponse.CreditMemos == nil { - return nil, errors.New("could not find any creditMemos") + return nil, errors.New("could not find any credit memos") } return resp.QueryResponse.CreditMemos, nil @@ -131,7 +128,7 @@ func (c *Client) QueryCreditMemos(query string) ([]CreditMemo, error) { // UpdateCreditMemo updates the given credit memo. func (c *Client) UpdateCreditMemo(creditMemo *CreditMemo) (*CreditMemo, error) { if creditMemo.Id == "" { - return nil, errors.New("missing creditMemo id") + return nil, errors.New("missing credit memo id") } existingCreditMemo, err := c.FindCreditMemoById(creditMemo.Id) @@ -154,7 +151,7 @@ func (c *Client) UpdateCreditMemo(creditMemo *CreditMemo) (*CreditMemo, error) { Time Date } - if err = c.post("creditMemo", payload, &creditMemoData, nil); err != nil { + if err = c.post("creditmemo", payload, &creditMemoData, nil); err != nil { return nil, err }