From 8f080be6cdc094f95e02ec2721db98d7f7e2f193 Mon Sep 17 00:00:00 2001 From: Bruno M V Souza Date: Thu, 20 Dec 2018 17:52:04 +0100 Subject: [PATCH 1/5] Make DateFormat and CurrencyFormat compliant with YNAB API changes --- api/budget/entity.go | 31 ++- api/budget/service_test.go | 485 +++++++++++++++++++++++++++++++++---- 2 files changed, 468 insertions(+), 48 deletions(-) diff --git a/api/budget/entity.go b/api/budget/entity.go index 1912400..d2c0f21 100644 --- a/api/budget/entity.go +++ b/api/budget/entity.go @@ -18,10 +18,8 @@ import ( // Budget represents a budget type Budget struct { - ID string `json:"id"` - Name string `json:"name"` - DateFormat DateFormat `json:"date_format"` - CurrencyFormat CurrencyFormat `json:"currency_format"` + ID string `json:"id"` + Name string `json:"name"` Accounts []*account.Account `json:"accounts"` Payees []*payee.Payee `json:"payees"` @@ -34,6 +32,15 @@ type Budget struct { ScheduledTransactions []*transaction.ScheduledSummary `json:"scheduled_transactions"` ScheduledSubTransactions []*transaction.ScheduledSubTransaction `json:"scheduled_sub_transactions"` + // DateFormat the date format setting for the budget. In some cases + // the format will not be available and will be specified as null. + DateFormat *DateFormat `json:"date_format"` + // CurrencyFormat the currency format setting for the budget. In + // some cases the format will not be available and will be specified + // as null. + CurrencyFormat *CurrencyFormat `json:"currency_format"` + // LastModifiedOn the last time any changes were made to the budget + // from either a web or mobile client. LastModifiedOn *time.Time `json:"last_modified_on"` // FirstMonth undocumented field FirstMonth *api.Date `json:"first_month"` @@ -46,8 +53,13 @@ type Summary struct { ID string `json:"id"` Name string `json:"name"` + // DateFormat the date format setting for the budget. In some cases + // the format will not be available and will be specified as null. + DateFormat *DateFormat `json:"date_format"` + // CurrencyFormat the currency format setting for the budget. In + // some cases the format will not be available and will be specified + // as null. CurrencyFormat *CurrencyFormat `json:"currency_format"` - DateFormat *DateFormat `json:"date_format"` // LastModifiedOn the last time any changes were made to the budget // from either a web or mobile client. LastModifiedOn *time.Time `json:"last_modified_on"` @@ -65,8 +77,13 @@ type Snapshot struct { // Settings represents the settings for a budget type Settings struct { - DateFormat DateFormat `json:"date_format"` - CurrencyFormat CurrencyFormat `json:"currency_format"` + // DateFormat the date format setting for the budget. In some cases + // the format will not be available and will be specified as null. + DateFormat *DateFormat `json:"date_format"` + // CurrencyFormat the currency format setting for the budget. In + // some cases the format will not be available and will be specified + // as null. + CurrencyFormat *CurrencyFormat `json:"currency_format"` } // DateFormat represents date format for a budget diff --git a/api/budget/service_test.go b/api/budget/service_test.go index 6769f24..87d1a23 100644 --- a/api/budget/service_test.go +++ b/api/budget/service_test.go @@ -177,13 +177,14 @@ func TestService_GetBudgets(t *testing.T) { } func TestService_GetBudget(t *testing.T) { - httpmock.Activate() - defer httpmock.DeactivateAndReset() + t.Run(`success`, func(t *testing.T) { + httpmock.Activate() + defer httpmock.DeactivateAndReset() - url := "https://api.youneedabudget.com/v1/budgets/aa248caa-eed7-4575-a990-717386438d2c" - httpmock.RegisterResponder(http.MethodGet, url, - func(req *http.Request) (*http.Response, error) { - res := httpmock.NewStringResponse(200, `{ + url := "https://api.youneedabudget.com/v1/budgets/aa248caa-eed7-4575-a990-717386438d2c" + httpmock.RegisterResponder(http.MethodGet, url, + func(req *http.Request) (*http.Response, error) { + res := httpmock.NewStringResponse(200, `{ "data": { "budget": { "id": "aa248caa-eed7-4575-a990-717386438d2c", @@ -329,14 +330,330 @@ func TestService_GetBudget(t *testing.T) { } } `) - res.Header.Add("X-Rate-Limit", "36/200") - return res, nil + res.Header.Add("X-Rate-Limit", "36/200") + return res, nil + }, + ) + + client := ynab.NewClient("") + _, err := client.Budget().GetBudget("aa248caa-eed7-4575-a990-717386438d2c", nil) + assert.NoError(t, err) + }) + + t.Run(`success when date_format is null`, func(t *testing.T) { + httpmock.Activate() + defer httpmock.DeactivateAndReset() + + url := "https://api.youneedabudget.com/v1/budgets/aa248caa-eed7-4575-a990-717386438d2c" + httpmock.RegisterResponder(http.MethodGet, url, + func(req *http.Request) (*http.Response, error) { + res := httpmock.NewStringResponse(200, `{ + "data": { + "budget": { + "id": "aa248caa-eed7-4575-a990-717386438d2c", + "name": "Test Budget", + "last_modified_on": "2018-03-05T17:24:36+00:00", + "date_format": null, + "currency_format": { + "iso_code": "BRL", + "example_format": "123.456,78", + "decimal_digits": 2, + "decimal_separator": ",", + "symbol_first": true, + "group_separator": ".", + "currency_symbol": "R$", + "display_symbol": true + }, + "first_month": "2017-12-01", + "last_month": "2018-02-01", + "accounts": [ + { + "id": "312bf0ae-9d1a-42d7-84c1-8f1d5e4e7bb0", + "name": "Cash", + "type": "cash", + "on_budget": true, + "closed": false, + "note": null, + "balance": 0, + "cleared_balance": 0, + "uncleared_balance": 0, + "deleted": false + } + ], + "payees": [ + { + "id": "793846ad-f8f5-454e-9ae4-8d938d0d89ca", + "name": "Starting Balance", + "transfer_account_id": null, + "deleted": false + } + ], + "payee_locations": [ + { + "id": "47471638-da3e-4cdd-9288-e373b50fafa7", + "payee_id": "793846ad-f8f5-454e-9ae4-8d938d0d89ca", + "latitude": "20.8988754", + "longitude": "-33.9167891", + "deleted": false + } + ], + "category_groups": [ + { + "id": "840512c5-3b1d-426f-b033-f7c64a16a076", + "name": "Category group", + "hidden": false, + "deleted": false + } + ], + "categories": [ + { + "id": "138c8bcd-6ca3-4c09-82ca-1cde7aa1d6f8", + "category_group_id": "840512c5-3b1d-426f-b033-f7c64a16a076", + "name": "Category", + "hidden": false, + "original_category_group_id": null, + "note": null, + "budgeted": 0, + "activity": 12190, + "balance": 18740, + "deleted": false + } + ], + "months": [ + { + "month": "2018-03-01", + "note": null, + "to_be_budgeted": 0, + "age_of_money": null, + "categories": [ + { + "id": "138c8bcd-6ca3-4c09-82ca-1cde7aa1d6f8", + "category_group_id": "840512c5-3b1d-426f-b033-f7c64a16a076", + "name": "Category", + "hidden": true, + "note": null, + "budgeted": 0, + "activity": 12190, + "balance": 18740, + "deleted": false + } + ] + } + ], + "transactions": [ + { + "id": "e31928db-b236-4c88-9a99-7aa46ff7a6f7", + "date": "2018-01-09", + "amount": -85440, + "memo": null, + "cleared": "cleared", + "approved": true, + "flag_color": null, + "account_id": "312bf0ae-9d1a-42d7-84c1-8f1d5e4e7bb0", + "payee_id": "fa8d442e-0bfc-4386-8e5b-480c4f70733a", + "category_id": "0d3552a4-49da-4191-bac6-e22f80eb2056", + "transfer_account_id": null, + "import_id": null, + "deleted": false + } + ], + "subtransactions": [ + { + "id": "254049fe-cadc-4657-b36e-99baac0bd9ca", + "transaction_id": "891a41b8-bc0f-4c0b-b3a3-97d5d6d61276", + "amount": 0, + "memo": null, + "payee_id": "33fc3c91-8489-4da7-aef5-57ccd19d60dd", + "category_id": "2d9e60f6-0c7e-472f-8064-0465aa1c58d4", + "transfer_account_id": null, + "deleted": false + } + ], + "scheduled_transactions": [ + { + "id": "0971ec91-0961-42be-8598-c6d79c800b28", + "date_first": "2018-11-20", + "date_next": "2018-11-20", + "frequency": "never", + "amount": -17000, + "memo": "Domain bmvs.me", + "flag_color": "yellow", + "account_id": "09eaca5e-2a34-4baa-89c4-828fb90638f2", + "payee_id": "793846ad-f8f5-454e-9ae4-8d938d0d89ca", + "category_id": "138c8bcd-6ca3-4c09-82ca-1cde7aa1d6f8", + "transfer_account_id": null, + "deleted": false + } + ], + "scheduled_subtransactions": [] }, - ) + "server_knowledge": 473 + } +} + `) + res.Header.Add("X-Rate-Limit", "36/200") + return res, nil + }, + ) - client := ynab.NewClient("") - _, err := client.Budget().GetBudget("aa248caa-eed7-4575-a990-717386438d2c", nil) - assert.NoError(t, err) + client := ynab.NewClient("") + _, err := client.Budget().GetBudget("aa248caa-eed7-4575-a990-717386438d2c", nil) + assert.NoError(t, err) + }) + + t.Run(`success when currency_format is null`, func(t *testing.T) { + httpmock.Activate() + defer httpmock.DeactivateAndReset() + + url := "https://api.youneedabudget.com/v1/budgets/aa248caa-eed7-4575-a990-717386438d2c" + httpmock.RegisterResponder(http.MethodGet, url, + func(req *http.Request) (*http.Response, error) { + res := httpmock.NewStringResponse(200, `{ + "data": { + "budget": { + "id": "aa248caa-eed7-4575-a990-717386438d2c", + "name": "Test Budget", + "last_modified_on": "2018-03-05T17:24:36+00:00", + "date_format": { + "format": "DD/MM/YYYY" + }, + "currency_format": null, + "first_month": "2017-12-01", + "last_month": "2018-02-01", + "accounts": [ + { + "id": "312bf0ae-9d1a-42d7-84c1-8f1d5e4e7bb0", + "name": "Cash", + "type": "cash", + "on_budget": true, + "closed": false, + "note": null, + "balance": 0, + "cleared_balance": 0, + "uncleared_balance": 0, + "deleted": false + } + ], + "payees": [ + { + "id": "793846ad-f8f5-454e-9ae4-8d938d0d89ca", + "name": "Starting Balance", + "transfer_account_id": null, + "deleted": false + } + ], + "payee_locations": [ + { + "id": "47471638-da3e-4cdd-9288-e373b50fafa7", + "payee_id": "793846ad-f8f5-454e-9ae4-8d938d0d89ca", + "latitude": "20.8988754", + "longitude": "-33.9167891", + "deleted": false + } + ], + "category_groups": [ + { + "id": "840512c5-3b1d-426f-b033-f7c64a16a076", + "name": "Category group", + "hidden": false, + "deleted": false + } + ], + "categories": [ + { + "id": "138c8bcd-6ca3-4c09-82ca-1cde7aa1d6f8", + "category_group_id": "840512c5-3b1d-426f-b033-f7c64a16a076", + "name": "Category", + "hidden": false, + "original_category_group_id": null, + "note": null, + "budgeted": 0, + "activity": 12190, + "balance": 18740, + "deleted": false + } + ], + "months": [ + { + "month": "2018-03-01", + "note": null, + "to_be_budgeted": 0, + "age_of_money": null, + "categories": [ + { + "id": "138c8bcd-6ca3-4c09-82ca-1cde7aa1d6f8", + "category_group_id": "840512c5-3b1d-426f-b033-f7c64a16a076", + "name": "Category", + "hidden": true, + "note": null, + "budgeted": 0, + "activity": 12190, + "balance": 18740, + "deleted": false + } + ] + } + ], + "transactions": [ + { + "id": "e31928db-b236-4c88-9a99-7aa46ff7a6f7", + "date": "2018-01-09", + "amount": -85440, + "memo": null, + "cleared": "cleared", + "approved": true, + "flag_color": null, + "account_id": "312bf0ae-9d1a-42d7-84c1-8f1d5e4e7bb0", + "payee_id": "fa8d442e-0bfc-4386-8e5b-480c4f70733a", + "category_id": "0d3552a4-49da-4191-bac6-e22f80eb2056", + "transfer_account_id": null, + "import_id": null, + "deleted": false + } + ], + "subtransactions": [ + { + "id": "254049fe-cadc-4657-b36e-99baac0bd9ca", + "transaction_id": "891a41b8-bc0f-4c0b-b3a3-97d5d6d61276", + "amount": 0, + "memo": null, + "payee_id": "33fc3c91-8489-4da7-aef5-57ccd19d60dd", + "category_id": "2d9e60f6-0c7e-472f-8064-0465aa1c58d4", + "transfer_account_id": null, + "deleted": false + } + ], + "scheduled_transactions": [ + { + "id": "0971ec91-0961-42be-8598-c6d79c800b28", + "date_first": "2018-11-20", + "date_next": "2018-11-20", + "frequency": "never", + "amount": -17000, + "memo": "Domain bmvs.me", + "flag_color": "yellow", + "account_id": "09eaca5e-2a34-4baa-89c4-828fb90638f2", + "payee_id": "793846ad-f8f5-454e-9ae4-8d938d0d89ca", + "category_id": "138c8bcd-6ca3-4c09-82ca-1cde7aa1d6f8", + "transfer_account_id": null, + "deleted": false + } + ], + "scheduled_subtransactions": [] + }, + "server_knowledge": 473 + } +} + `) + res.Header.Add("X-Rate-Limit", "36/200") + return res, nil + }, + ) + + client := ynab.NewClient("") + _, err := client.Budget().GetBudget("aa248caa-eed7-4575-a990-717386438d2c", nil) + assert.NoError(t, err) + }) } func TestService_GetLastUsedBudget(t *testing.T) { @@ -503,13 +820,14 @@ func TestService_GetLastUsedBudget(t *testing.T) { } func TestService_GetBudgetSettings(t *testing.T) { - httpmock.Activate() - defer httpmock.DeactivateAndReset() + t.Run(`success`, func(t *testing.T) { + httpmock.Activate() + defer httpmock.DeactivateAndReset() - url := "https://api.youneedabudget.com/v1/budgets/aa248caa-eed7-4575-a990-717386438d2c/settings" - httpmock.RegisterResponder(http.MethodGet, url, - func(req *http.Request) (*http.Response, error) { - res := httpmock.NewStringResponse(200, `{ + url := "https://api.youneedabudget.com/v1/budgets/aa248caa-eed7-4575-a990-717386438d2c/settings" + httpmock.RegisterResponder(http.MethodGet, url, + func(req *http.Request) (*http.Response, error) { + res := httpmock.NewStringResponse(200, `{ "data": { "settings": { "date_format": { @@ -528,32 +846,117 @@ func TestService_GetBudgetSettings(t *testing.T) { } } }`) - res.Header.Add("X-Rate-Limit", "36/200") - return res, nil - }, - ) + res.Header.Add("X-Rate-Limit", "36/200") + return res, nil + }, + ) - client := ynab.NewClient("") - settings, err := client.Budget().GetBudgetSettings("aa248caa-eed7-4575-a990-717386438d2c") - assert.NoError(t, err) + client := ynab.NewClient("") + settings, err := client.Budget().GetBudgetSettings("aa248caa-eed7-4575-a990-717386438d2c") + assert.NoError(t, err) - expected := &budget.Settings{ - DateFormat: budget.DateFormat{ - Format: "DD/MM/YYYY", - }, - CurrencyFormat: budget.CurrencyFormat{ - ISOCode: "BRL", - ExampleFormat: "123.456,78", - DecimalDigits: uint64(2), - DecimalSeparator: ",", - SymbolFirst: true, - GroupSeparator: ".", - CurrencySymbol: "R$", - DisplaySymbol: true, - }, - } + expected := &budget.Settings{ + DateFormat: &budget.DateFormat{ + Format: "DD/MM/YYYY", + }, + CurrencyFormat: &budget.CurrencyFormat{ + ISOCode: "BRL", + ExampleFormat: "123.456,78", + DecimalDigits: uint64(2), + DecimalSeparator: ",", + SymbolFirst: true, + GroupSeparator: ".", + CurrencySymbol: "R$", + DisplaySymbol: true, + }, + } - assert.Equal(t, expected, settings) + assert.Equal(t, expected, settings) + }) + + t.Run(`success when date_format is null`, func(t *testing.T) { + httpmock.Activate() + defer httpmock.DeactivateAndReset() + + url := "https://api.youneedabudget.com/v1/budgets/aa248caa-eed7-4575-a990-717386438d2c/settings" + httpmock.RegisterResponder(http.MethodGet, url, + func(req *http.Request) (*http.Response, error) { + res := httpmock.NewStringResponse(200, `{ + "data": { + "settings": { + "date_format": null, + "currency_format": { + "iso_code": "BRL", + "example_format": "123.456,78", + "decimal_digits": 2, + "decimal_separator": ",", + "symbol_first": true, + "group_separator": ".", + "currency_symbol": "R$", + "display_symbol": true + } + } + } +}`) + res.Header.Add("X-Rate-Limit", "36/200") + return res, nil + }, + ) + + client := ynab.NewClient("") + settings, err := client.Budget().GetBudgetSettings("aa248caa-eed7-4575-a990-717386438d2c") + assert.NoError(t, err) + + expected := &budget.Settings{ + CurrencyFormat: &budget.CurrencyFormat{ + ISOCode: "BRL", + ExampleFormat: "123.456,78", + DecimalDigits: uint64(2), + DecimalSeparator: ",", + SymbolFirst: true, + GroupSeparator: ".", + CurrencySymbol: "R$", + DisplaySymbol: true, + }, + } + + assert.Equal(t, expected, settings) + }) + + t.Run(`success when currency_format is null`, func(t *testing.T) { + httpmock.Activate() + defer httpmock.DeactivateAndReset() + + url := "https://api.youneedabudget.com/v1/budgets/aa248caa-eed7-4575-a990-717386438d2c/settings" + httpmock.RegisterResponder(http.MethodGet, url, + func(req *http.Request) (*http.Response, error) { + res := httpmock.NewStringResponse(200, `{ + "data": { + "settings": { + "date_format": { + "format": "DD/MM/YYYY" + }, + "currency_format": null + } + } +}`) + res.Header.Add("X-Rate-Limit", "36/200") + return res, nil + }, + ) + + client := ynab.NewClient("") + settings, err := client.Budget().GetBudgetSettings("aa248caa-eed7-4575-a990-717386438d2c") + assert.NoError(t, err) + + expected := &budget.Settings{ + DateFormat: &budget.DateFormat{ + Format: "DD/MM/YYYY", + }, + } + + assert.Equal(t, expected, settings) + }) } func TestFilter_ToQuery(t *testing.T) { From af807280007fd4d182444a2cf9d330df51cd0199 Mon Sep 17 00:00:00 2001 From: Bruno M V Souza Date: Thu, 20 Dec 2018 18:30:45 +0100 Subject: [PATCH 2/5] Make api.DateLayout private by creating helper api.DateFormat function --- api/date.go | 16 +++++++++++----- api/date_test.go | 22 ++++++++++++++++++++++ api/month/service.go | 2 +- api/transaction/service.go | 2 +- 4 files changed, 35 insertions(+), 7 deletions(-) diff --git a/api/date.go b/api/date.go index 225764b..9b25505 100644 --- a/api/date.go +++ b/api/date.go @@ -10,8 +10,8 @@ import ( "time" ) -// DateLayout expected layout format for the Date type -const DateLayout = "2006-01-02" +// dateLayout expected layout format for the Date type +const dateLayout = "2006-01-02" // Date represents a budget date type Date struct { @@ -34,14 +34,14 @@ func (d *Date) UnmarshalJSON(b []byte) error { // MarshalJSON parses the expected format for a Date func (d *Date) MarshalJSON() ([]byte, error) { - val := d.Format(DateLayout) + val := d.Format(dateLayout) return []byte(fmt.Sprintf(`"%s"`, val)), nil } // DateFromString creates a new Date from a given string date -// formatted as DateLayout +// formatted as dateLayout func DateFromString(s string) (Date, error) { - t, err := time.Parse(DateLayout, s) + t, err := time.Parse(dateLayout, s) if err != nil { return Date{}, err } @@ -50,3 +50,9 @@ func DateFromString(s string) (Date, error) { } return d, nil } + +// DateFormat creates a new string from a given api.Date +// formatted as dateLayout +func DateFormat(date Date) string { + return date.Format(dateLayout) +} diff --git a/api/date_test.go b/api/date_test.go index 7687348..3b71801 100644 --- a/api/date_test.go +++ b/api/date_test.go @@ -79,3 +79,25 @@ func TestNewDateFromString(t *testing.T) { assert.Equal(t, test.OutputDateToString, date.String()) } } + +func TestDateFormat(t *testing.T) { + apiDate1, err := api.DateFromString("2018-02-01") + assert.NoError(t, err) + + apiDate2, err := api.DateFromString("2018-12-01") + assert.NoError(t, err) + + table := []struct { + InputDate api.Date + OutputFormattedDate string + }{ + {apiDate1, "2018-02-01"}, + {apiDate2, "2018-12-01"}, + {api.Date{}, "0001-01-01"}, + } + + for _, test := range table { + formattedDate := api.DateFormat(test.InputDate) + assert.Equal(t, test.OutputFormattedDate, formattedDate) + } +} diff --git a/api/month/service.go b/api/month/service.go index a970397..e2366af 100644 --- a/api/month/service.go +++ b/api/month/service.go @@ -46,7 +46,7 @@ func (s *Service) GetMonth(budgetID string, month api.Date) (*Month, error) { }{} url := fmt.Sprintf("/budgets/%s/months/%s", budgetID, - month.Format(api.DateLayout)) + api.DateFormat(month)) if err := s.c.GET(url, &resModel); err != nil { return nil, err } diff --git a/api/transaction/service.go b/api/transaction/service.go index 65bb116..82c7ada 100644 --- a/api/transaction/service.go +++ b/api/transaction/service.go @@ -272,7 +272,7 @@ func (f *Filter) ToQuery() string { pairs := make([]string, 0, 2) if f.Since != nil && !f.Since.IsZero() { pairs = append(pairs, fmt.Sprintf("since_date=%s", - f.Since.Format(api.DateLayout))) + api.DateFormat(*f.Since))) } if f.Type != nil { pairs = append(pairs, fmt.Sprintf("type=%s", string(*f.Type))) From 870b4a8f388fa2c4bf04b98d34121c81530e679a Mon Sep 17 00:00:00 2001 From: Bruno M V Souza Date: Thu, 20 Dec 2018 18:30:57 +0100 Subject: [PATCH 3/5] Fix test name --- api/date_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/date_test.go b/api/date_test.go index 3b71801..4e5c43c 100644 --- a/api/date_test.go +++ b/api/date_test.go @@ -63,7 +63,7 @@ func TestDate_MarshalJSON(t *testing.T) { assert.Equal(t, `{"Date":"2020-01-20"}`, string(buf)) } -func TestNewDateFromString(t *testing.T) { +func TestDateFromString(t *testing.T) { table := []struct { InputDate string OutputDateToString string From cbbee00f8bb3029b5f2db5d645fc84596206b97b Mon Sep 17 00:00:00 2001 From: Bruno M V Souza Date: Thu, 20 Dec 2018 18:32:26 +0100 Subject: [PATCH 4/5] Add ability to fetch a category for a given month Implements wrapper for https://api.youneedabudget.com/v1#/Categories/getMonthCategoryById --- api/category/example_test.go | 19 +++++ api/category/service.go | 30 ++++++++ api/category/service_test.go | 140 +++++++++++++++++++++++++++++++++++ 3 files changed, 189 insertions(+) diff --git a/api/category/example_test.go b/api/category/example_test.go index 7412427..ffec067 100644 --- a/api/category/example_test.go +++ b/api/category/example_test.go @@ -9,6 +9,7 @@ import ( "reflect" "go.bmvs.io/ynab" + "go.bmvs.io/ynab/api" ) func ExampleService_GetCategory() { @@ -26,3 +27,21 @@ func ExampleService_GetCategories() { // Output: []*category.GroupWithCategories } + +func ExampleService_GetCategoryForMonth() { + c := ynab.NewClient("") + category, _ := c.Category().GetCategoryForMonth("", + "", api.Date{}) + fmt.Println(reflect.TypeOf(category)) + + // Output: *category.Category +} + +func ExampleService_GetCategoryForCurrentMonth() { + c := ynab.NewClient("") + category, _ := c.Category().GetCategoryForCurrentMonth("", + "") + fmt.Println(reflect.TypeOf(category)) + + // Output: *category.Category +} diff --git a/api/category/service.go b/api/category/service.go index 466569e..87bd87d 100644 --- a/api/category/service.go +++ b/api/category/service.go @@ -10,6 +10,8 @@ import ( "go.bmvs.io/ynab/api" ) +const currentMonthID = "current" + // NewService facilitates the creation of a new category service instance func NewService(c api.ClientReader) *Service { return &Service{c} @@ -51,3 +53,31 @@ func (s *Service) GetCategory(budgetID, categoryID string) (*Category, error) { } return resModel.Data.Category, nil } + +// GetCategoryForMonth fetches a specific category from a budget month +// https://api.youneedabudget.com/v1#/Categories/getMonthCategoryById +func (s *Service) GetCategoryForMonth(budgetID, categoryID string, + month api.Date) (*Category, error) { + + return s.getCategoryForMonth(budgetID, categoryID, api.DateFormat(month)) +} + +// GetCategoryForCurrentMonth fetches a specific category from the current budget month +// https://api.youneedabudget.com/v1#/Categories/getMonthCategoryById +func (s *Service) GetCategoryForCurrentMonth(budgetID, categoryID string) (*Category, error) { + return s.getCategoryForMonth(budgetID, categoryID, currentMonthID) +} + +func (s *Service) getCategoryForMonth(budgetID, categoryID, month string) (*Category, error) { + resModel := struct { + Data struct { + Category *Category `json:"category"` + } `json:"data"` + }{} + + url := fmt.Sprintf("/budgets/%s/months/%s/categories/%s", budgetID, month, categoryID) + if err := s.c.GET(url, &resModel); err != nil { + return nil, err + } + return resModel.Data.Category, nil +} diff --git a/api/category/service_test.go b/api/category/service_test.go index 6befb92..fff1abb 100644 --- a/api/category/service_test.go +++ b/api/category/service_test.go @@ -167,3 +167,143 @@ func TestService_GetCategory(t *testing.T) { } assert.Equal(t, expected, c) } + +func TestService_GetCategoryForMonth(t *testing.T) { + httpmock.Activate() + defer httpmock.DeactivateAndReset() + + url := "https://api.youneedabudget.com/v1/budgets/aa248caa-eed7-4575-a990-717386438d2c/months/2018-01-01/categories/13419c12-78d3-4a26-82ca-1cde7aa1d6f8" + httpmock.RegisterResponder(http.MethodGet, url, + func(req *http.Request) (*http.Response, error) { + res := httpmock.NewStringResponse(200, `{ + "data": { + "category": { + "id": "13419c12-78d3-4a26-82ca-1cde7aa1d6f8", + "category_group_id": "13419c12-78d3-4818-a5dc-601b2b8a6064", + "name": "MasterCard", + "hidden": false, + "original_category_group_id": null, + "note": null, + "budgeted": 0, + "activity": 12190, + "balance": 18740, + "deleted": false, + "goal_type": "TB", + "goal_creation_month": "2018-04-01", + "goal_target": 18740, + "goal_target_month": "2018-05-01", + "goal_percentage_complete": 20 + } + } +} + `) + res.Header.Add("X-Rate-Limit", "36/200") + return res, nil + }, + ) + + date, err := api.DateFromString("2018-01-01") + assert.NoError(t, err) + + client := ynab.NewClient("") + c, err := client.Category().GetCategoryForMonth( + "aa248caa-eed7-4575-a990-717386438d2c", + "13419c12-78d3-4a26-82ca-1cde7aa1d6f8", + date, + ) + assert.NoError(t, err) + + var ( + expectedGoalTarget int64 = 18740 + expectedGoalPercentageComplete uint16 = 20 + ) + expectedGoalCreationMonth, err := api.DateFromString("2018-04-01") + assert.NoError(t, err) + expectedGoalTargetMonth, err := api.DateFromString("2018-05-01") + assert.NoError(t, err) + + expected := &category.Category{ + ID: "13419c12-78d3-4a26-82ca-1cde7aa1d6f8", + CategoryGroupID: "13419c12-78d3-4818-a5dc-601b2b8a6064", + Name: "MasterCard", + Hidden: false, + Budgeted: int64(0), + Activity: int64(12190), + Balance: int64(18740), + Deleted: false, + GoalType: category.GoalTargetCategoryBalance.Pointer(), + GoalCreationMonth: &expectedGoalCreationMonth, + GoalTargetMonth: &expectedGoalTargetMonth, + GoalTarget: &expectedGoalTarget, + GoalPercentageComplete: &expectedGoalPercentageComplete, + } + assert.Equal(t, expected, c) +} + +func TestService_GetCategoryForCurrentMonth(t *testing.T) { + httpmock.Activate() + defer httpmock.DeactivateAndReset() + + url := "https://api.youneedabudget.com/v1/budgets/aa248caa-eed7-4575-a990-717386438d2c/months/current/categories/13419c12-78d3-4a26-82ca-1cde7aa1d6f8" + httpmock.RegisterResponder(http.MethodGet, url, + func(req *http.Request) (*http.Response, error) { + res := httpmock.NewStringResponse(200, `{ + "data": { + "category": { + "id": "13419c12-78d3-4a26-82ca-1cde7aa1d6f8", + "category_group_id": "13419c12-78d3-4818-a5dc-601b2b8a6064", + "name": "MasterCard", + "hidden": false, + "original_category_group_id": null, + "note": null, + "budgeted": 0, + "activity": 12190, + "balance": 18740, + "deleted": false, + "goal_type": "TB", + "goal_creation_month": "2018-04-01", + "goal_target": 18740, + "goal_target_month": "2018-05-01", + "goal_percentage_complete": 20 + } + } +} + `) + res.Header.Add("X-Rate-Limit", "36/200") + return res, nil + }, + ) + + client := ynab.NewClient("") + c, err := client.Category().GetCategoryForCurrentMonth( + "aa248caa-eed7-4575-a990-717386438d2c", + "13419c12-78d3-4a26-82ca-1cde7aa1d6f8", + ) + assert.NoError(t, err) + + var ( + expectedGoalTarget int64 = 18740 + expectedGoalPercentageComplete uint16 = 20 + ) + expectedGoalCreationMonth, err := api.DateFromString("2018-04-01") + assert.NoError(t, err) + expectedGoalTargetMonth, err := api.DateFromString("2018-05-01") + assert.NoError(t, err) + + expected := &category.Category{ + ID: "13419c12-78d3-4a26-82ca-1cde7aa1d6f8", + CategoryGroupID: "13419c12-78d3-4818-a5dc-601b2b8a6064", + Name: "MasterCard", + Hidden: false, + Budgeted: int64(0), + Activity: int64(12190), + Balance: int64(18740), + Deleted: false, + GoalType: category.GoalTargetCategoryBalance.Pointer(), + GoalCreationMonth: &expectedGoalCreationMonth, + GoalTargetMonth: &expectedGoalTargetMonth, + GoalTarget: &expectedGoalTarget, + GoalPercentageComplete: &expectedGoalPercentageComplete, + } + assert.Equal(t, expected, c) +} From e396d419967303b3bb663ec5045c03d24e33d717 Mon Sep 17 00:00:00 2001 From: Bruno M V Souza Date: Thu, 20 Dec 2018 19:20:17 +0100 Subject: [PATCH 5/5] Add ability to update a category for a given month Implements wrappers for https://api.youneedabudget.com/v1#/Categories/updateMonthCategory --- api/category/example_test.go | 48 ++++++++--- api/category/payload.go | 10 +++ api/category/service.go | 50 ++++++++++- api/category/service_test.go | 162 +++++++++++++++++++++++++++++++++++ 4 files changed, 257 insertions(+), 13 deletions(-) create mode 100644 api/category/payload.go diff --git a/api/category/example_test.go b/api/category/example_test.go index ffec067..fbcc529 100644 --- a/api/category/example_test.go +++ b/api/category/example_test.go @@ -6,6 +6,9 @@ package category_test import ( "fmt" + + "go.bmvs.io/ynab/api/category" + "reflect" "go.bmvs.io/ynab" @@ -13,35 +16,58 @@ import ( ) func ExampleService_GetCategory() { - c := ynab.NewClient("") - category, _ := c.Category().GetCategory("", "") - fmt.Println(reflect.TypeOf(category)) + client := ynab.NewClient("") + c, _ := client.Category().GetCategory("", "") + fmt.Println(reflect.TypeOf(c)) // Output: *category.Category } func ExampleService_GetCategories() { - c := ynab.NewClient("") - categories, _ := c.Category().GetCategories("") + client := ynab.NewClient("") + categories, _ := client.Category().GetCategories("") fmt.Println(reflect.TypeOf(categories)) // Output: []*category.GroupWithCategories } func ExampleService_GetCategoryForMonth() { - c := ynab.NewClient("") - category, _ := c.Category().GetCategoryForMonth("", + client := ynab.NewClient("") + c, _ := client.Category().GetCategoryForMonth("", "", api.Date{}) - fmt.Println(reflect.TypeOf(category)) + fmt.Println(reflect.TypeOf(c)) // Output: *category.Category } func ExampleService_GetCategoryForCurrentMonth() { - c := ynab.NewClient("") - category, _ := c.Category().GetCategoryForCurrentMonth("", + client := ynab.NewClient("") + c, _ := client.Category().GetCategoryForCurrentMonth("", "") - fmt.Println(reflect.TypeOf(category)) + fmt.Println(reflect.TypeOf(c)) + + // Output: *category.Category +} + +func ExampleService_UpdateCategoryForMonth() { + validMonth, _ := api.DateFromString("2018-01-01") + validPayload := category.PayloadMonthCategory{Budgeted: 1000} + + client := ynab.NewClient("") + c, _ := client.Category().UpdateCategoryForMonth("", + "", validMonth, validPayload) + fmt.Println(reflect.TypeOf(c)) + + // Output: *category.Category +} + +func ExampleService_UpdateCategoryForCurrentMonth() { + validPayload := category.PayloadMonthCategory{Budgeted: 1000} + + client := ynab.NewClient("") + c, _ := client.Category().UpdateCategoryForCurrentMonth("", + "", validPayload) + fmt.Println(reflect.TypeOf(c)) // Output: *category.Category } diff --git a/api/category/payload.go b/api/category/payload.go new file mode 100644 index 0000000..52a8d49 --- /dev/null +++ b/api/category/payload.go @@ -0,0 +1,10 @@ +// Copyright (c) 2018, Bruno M V Souza . All rights reserved. +// Use of this source code is governed by a BSD-2-Clause license that can be +// found in the LICENSE file. + +package category + +// PayloadMonthCategory is the payload contract for updating a category for a month +type PayloadMonthCategory struct { + Budgeted int64 +} diff --git a/api/category/service.go b/api/category/service.go index 87bd87d..d97b91c 100644 --- a/api/category/service.go +++ b/api/category/service.go @@ -5,6 +5,7 @@ package category import ( + "encoding/json" "fmt" "go.bmvs.io/ynab/api" @@ -13,13 +14,13 @@ import ( const currentMonthID = "current" // NewService facilitates the creation of a new category service instance -func NewService(c api.ClientReader) *Service { +func NewService(c api.ClientReaderWriter) *Service { return &Service{c} } // Service wraps YNAB category API endpoints type Service struct { - c api.ClientReader + c api.ClientReaderWriter } // GetCategories fetches the list of category groups for a budget @@ -81,3 +82,48 @@ func (s *Service) getCategoryForMonth(budgetID, categoryID, month string) (*Cate } return resModel.Data.Category, nil } + +// UpdateCategoryForMonth updates a category for a month +// https://api.youneedabudget.com/v1#/Categories/updateMonthCategory +func (s *Service) UpdateCategoryForMonth(budgetID, categoryID string, month api.Date, + p PayloadMonthCategory) (*Category, error) { + + return s.updateCategoryForMonth(budgetID, categoryID, api.DateFormat(month), p) +} + +// UpdateCategoryForCurrentMonth updates a category for the current month +// https://api.youneedabudget.com/v1#/Categories/updateMonthCategory +func (s *Service) UpdateCategoryForCurrentMonth(budgetID, categoryID string, + p PayloadMonthCategory) (*Category, error) { + + return s.updateCategoryForMonth(budgetID, categoryID, currentMonthID, p) +} + +func (s *Service) updateCategoryForMonth(budgetID, categoryID, month string, + p PayloadMonthCategory) (*Category, error) { + + payload := struct { + MonthCategory *PayloadMonthCategory `json:"month_category"` + }{ + &p, + } + + buf, err := json.Marshal(&payload) + if err != nil { + return nil, err + } + + resModel := struct { + Data struct { + Category *Category `json:"category"` + } `json:"data"` + }{} + + url := fmt.Sprintf("/budgets/%s/months/%s/categories/%s", budgetID, + month, categoryID) + + if err := s.c.PUT(url, &resModel, buf); err != nil { + return nil, err + } + return resModel.Data.Category, nil +} diff --git a/api/category/service_test.go b/api/category/service_test.go index fff1abb..76c49df 100644 --- a/api/category/service_test.go +++ b/api/category/service_test.go @@ -5,6 +5,7 @@ package category_test import ( + "encoding/json" "net/http" "testing" @@ -307,3 +308,164 @@ func TestService_GetCategoryForCurrentMonth(t *testing.T) { } assert.Equal(t, expected, c) } + +func TestService_UpdateCategoryForMonth(t *testing.T) { + httpmock.Activate() + defer httpmock.DeactivateAndReset() + + payload := category.PayloadMonthCategory{ + Budgeted: 1000, + } + + url := "https://api.youneedabudget.com/v1/budgets/aa248caa-eed7-4575-a990-717386438d2c/months/0001-01-01/categories/13419c12-78d3-4a26-82ca-1cde7aa1d6f8" + httpmock.RegisterResponder(http.MethodPut, url, + func(req *http.Request) (*http.Response, error) { + resModel := struct { + MonthCategory *category.PayloadMonthCategory `json:"month_category"` + }{} + err := json.NewDecoder(req.Body).Decode(&resModel) + assert.NoError(t, err) + assert.Equal(t, &payload, resModel.MonthCategory) + + res := httpmock.NewStringResponse(200, `{ + "data": { + "category": { + "id": "13419c12-78d3-4a26-82ca-1cde7aa1d6f8", + "category_group_id": "13419c12-78d3-4818-a5dc-601b2b8a6064", + "name": "MasterCard", + "hidden": false, + "original_category_group_id": null, + "note": null, + "budgeted": 1000, + "activity": 12190, + "balance": 18740, + "deleted": false, + "goal_type": "TB", + "goal_creation_month": "2018-04-01", + "goal_target": 18740, + "goal_target_month": "2018-05-01", + "goal_percentage_complete": 20 + } + } +} + `) + res.Header.Add("X-Rate-Limit", "36/200") + return res, nil + }, + ) + + client := ynab.NewClient("") + c, err := client.Category().UpdateCategoryForMonth( + "aa248caa-eed7-4575-a990-717386438d2c", + "13419c12-78d3-4a26-82ca-1cde7aa1d6f8", + api.Date{}, + payload, + ) + assert.NoError(t, err) + + var ( + expectedGoalTarget int64 = 18740 + expectedGoalPercentageComplete uint16 = 20 + ) + expectedGoalCreationMonth, err := api.DateFromString("2018-04-01") + assert.NoError(t, err) + expectedGoalTargetMonth, err := api.DateFromString("2018-05-01") + assert.NoError(t, err) + + expected := &category.Category{ + ID: "13419c12-78d3-4a26-82ca-1cde7aa1d6f8", + CategoryGroupID: "13419c12-78d3-4818-a5dc-601b2b8a6064", + Name: "MasterCard", + Hidden: false, + Budgeted: int64(1000), + Activity: int64(12190), + Balance: int64(18740), + Deleted: false, + GoalType: category.GoalTargetCategoryBalance.Pointer(), + GoalCreationMonth: &expectedGoalCreationMonth, + GoalTargetMonth: &expectedGoalTargetMonth, + GoalTarget: &expectedGoalTarget, + GoalPercentageComplete: &expectedGoalPercentageComplete, + } + assert.Equal(t, expected, c) +} + +func TestService_UpdateCategoryForCurrentMonth(t *testing.T) { + httpmock.Activate() + defer httpmock.DeactivateAndReset() + + payload := category.PayloadMonthCategory{ + Budgeted: 1000, + } + + url := "https://api.youneedabudget.com/v1/budgets/aa248caa-eed7-4575-a990-717386438d2c/months/current/categories/13419c12-78d3-4a26-82ca-1cde7aa1d6f8" + httpmock.RegisterResponder(http.MethodPut, url, + func(req *http.Request) (*http.Response, error) { + resModel := struct { + MonthCategory *category.PayloadMonthCategory `json:"month_category"` + }{} + err := json.NewDecoder(req.Body).Decode(&resModel) + assert.NoError(t, err) + assert.Equal(t, &payload, resModel.MonthCategory) + + res := httpmock.NewStringResponse(200, `{ + "data": { + "category": { + "id": "13419c12-78d3-4a26-82ca-1cde7aa1d6f8", + "category_group_id": "13419c12-78d3-4818-a5dc-601b2b8a6064", + "name": "MasterCard", + "hidden": false, + "original_category_group_id": null, + "note": null, + "budgeted": 1000, + "activity": 12190, + "balance": 18740, + "deleted": false, + "goal_type": "TB", + "goal_creation_month": "2018-04-01", + "goal_target": 18740, + "goal_target_month": "2018-05-01", + "goal_percentage_complete": 20 + } + } +} + `) + res.Header.Add("X-Rate-Limit", "36/200") + return res, nil + }, + ) + + client := ynab.NewClient("") + c, err := client.Category().UpdateCategoryForCurrentMonth( + "aa248caa-eed7-4575-a990-717386438d2c", + "13419c12-78d3-4a26-82ca-1cde7aa1d6f8", + payload, + ) + assert.NoError(t, err) + + var ( + expectedGoalTarget int64 = 18740 + expectedGoalPercentageComplete uint16 = 20 + ) + expectedGoalCreationMonth, err := api.DateFromString("2018-04-01") + assert.NoError(t, err) + expectedGoalTargetMonth, err := api.DateFromString("2018-05-01") + assert.NoError(t, err) + + expected := &category.Category{ + ID: "13419c12-78d3-4a26-82ca-1cde7aa1d6f8", + CategoryGroupID: "13419c12-78d3-4818-a5dc-601b2b8a6064", + Name: "MasterCard", + Hidden: false, + Budgeted: int64(1000), + Activity: int64(12190), + Balance: int64(18740), + Deleted: false, + GoalType: category.GoalTargetCategoryBalance.Pointer(), + GoalCreationMonth: &expectedGoalCreationMonth, + GoalTargetMonth: &expectedGoalTargetMonth, + GoalTarget: &expectedGoalTarget, + GoalPercentageComplete: &expectedGoalPercentageComplete, + } + assert.Equal(t, expected, c) +}