Skip to content

Commit

Permalink
Add DeleteTransaction API call
Browse files Browse the repository at this point in the history
  • Loading branch information
snarlysodboxer authored and brunomvsouza committed Feb 8, 2025
1 parent a8186a0 commit 64610ec
Show file tree
Hide file tree
Showing 5 changed files with 166 additions and 1 deletion.
1 change: 1 addition & 0 deletions api/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ type ClientWriter interface {
POST(url string, responseModel interface{}, requestBody []byte) error
PUT(url string, responseModel interface{}, requestBody []byte) error
PATCH(url string, responseModel interface{}, requestBody []byte) error
DELETE(url string, responseModel interface{}) error
}

// ClientReaderWriter contract for a read-write client
Expand Down
19 changes: 18 additions & 1 deletion api/transaction/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,23 @@ func (s *Service) UpdateTransactions(budgetID string,
return resModel.Data, nil
}

// DeleteTransaction deletes a transaction from a budget
// https://api.youneedabudget.com/v1#/Transactions/deleteTransaction
func (s *Service) DeleteTransaction(budgetID, transactionID string) (*Transaction, error) {
resModel := struct {
Data struct {
Transaction *Transaction `json:"transaction"`
} `json:"data"`
}{}

url := fmt.Sprintf("/budgets/%s/transactions/%s", budgetID, transactionID)
err := s.c.DELETE(url, &resModel)
if err != nil {
return nil, err
}
return resModel.Data.Transaction, nil
}

// GetTransactionsByAccount fetches the list of transactions of a specific account
// from a budget with filtering capabilities
// https://api.youneedabudget.com/v1#/Transactions/getTransactionsByAccount
Expand Down Expand Up @@ -257,7 +274,7 @@ func (s *Service) GetTransactionsByPayee(budgetID, payeeID string,

// GetScheduledTransactions fetches the list of scheduled transactions from
// a budget
//https://api.youneedabudget.com/v1#/Scheduled_Transactions/getScheduledTransactions
// https://api.youneedabudget.com/v1#/Scheduled_Transactions/getScheduledTransactions
func (s *Service) GetScheduledTransactions(budgetID string) ([]*Scheduled, error) {
resModel := struct {
Data struct {
Expand Down
33 changes: 33 additions & 0 deletions api/transaction/service_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1178,6 +1178,39 @@ func TestService_UpdateTransaction(t *testing.T) {
assert.Equal(t, expectedTransaction, tx)
}

func TestService_DeleteTransaction(t *testing.T) {
httpmock.Activate()
defer httpmock.DeactivateAndReset()

url := "https://api.youneedabudget.com/v1/budgets/aa248caa-eed7-4575-a990-717386438d2c/transactions/e6ad88f5-6f16-4480-9515-5377012750dd"
httpmock.RegisterResponder(http.MethodDelete, url,
func(req *http.Request) (*http.Response, error) {
res := httpmock.NewStringResponse(200, `{
"data": {
"transaction": {
"id": "e6ad88f5-6f16-4480-9515-5377012750dd"
}
}
}
`)
res.Header.Add("X-Rate-Limit", "36/200")
return res, nil
},
)

client := ynab.NewClient("")
tx, err := client.Transaction().DeleteTransaction(
"aa248caa-eed7-4575-a990-717386438d2c",
"e6ad88f5-6f16-4480-9515-5377012750dd",
)
assert.NoError(t, err)

expected := &transaction.Transaction{
ID: "e6ad88f5-6f16-4480-9515-5377012750dd",
}
assert.Equal(t, expected, tx)
}

func TestFilter_ToQuery(t *testing.T) {
sinceDate, err := api.DateFromString("2020-02-02")
assert.NoError(t, err)
Expand Down
5 changes: 5 additions & 0 deletions client.go
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,11 @@ func (c *client) PATCH(url string, responseModel interface{}, requestBody []byte
return c.do(http.MethodPatch, url, responseModel, requestBody)
}

// DELETE sends a DELETE request to the YNAB API
func (c *client) DELETE(url string, responseModel interface{}) error {
return c.do(http.MethodDelete, url, responseModel, nil)
}

// do sends a request to the YNAB API
func (c *client) do(method, url string, responseModel interface{}, requestBody []byte) error {
fullURL := fmt.Sprintf("%s%s", apiEndpoint, url)
Expand Down
109 changes: 109 additions & 0 deletions client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -478,3 +478,112 @@ func TestClient_PATCH(t *testing.T) {
}{}, response)
})
}

func TestClient_DELETE(t *testing.T) {
t.Run("success", func(t *testing.T) {
httpmock.Activate()
defer httpmock.DeactivateAndReset()

httpmock.RegisterResponder(http.MethodDelete, fmt.Sprintf("%s%s", apiEndpoint, "/foo"),
func(req *http.Request) (*http.Response, error) {
assert.Equal(t, "application/json", req.Header.Get("Accept"))
assert.Equal(t, "Bearer 6zL9vh8]B9H3BEecwL%Vzh^VwKR3C2CNZ3Bv%=fFxm$z)duY[U+2=3CydZrkQFnA", req.Header.Get("Authorization"))

res := httpmock.NewStringResponse(http.StatusOK, `{
"data": {
"transaction": {
"id": "some_id"
}
}
}`)
res.Header.Add("X-Rate-Limit", "36/200")
return res, nil
},
)

response := struct {
Data struct {
Transaction struct {
ID string `json:"id"`
} `json:"transaction"`
} `json:"data"`
}{}

c := NewClient("6zL9vh8]B9H3BEecwL%Vzh^VwKR3C2CNZ3Bv%=fFxm$z)duY[U+2=3CydZrkQFnA")
err := c.(*client).DELETE("/foo", &response)
assert.NoError(t, err)
assert.Equal(t, "some_id", response.Data.Transaction.ID)
})

t.Run("failure with with expected API error", func(t *testing.T) {
httpmock.Activate()
defer httpmock.DeactivateAndReset()

httpmock.RegisterResponder(http.MethodDelete, fmt.Sprintf("%s%s", apiEndpoint, "/foo"),
func(req *http.Request) (*http.Response, error) {
res := httpmock.NewStringResponse(http.StatusBadRequest, `{
"error": {
"id": "400",
"name": "error_name",
"detail": "Error detail"
}
}`)
res.Header.Add("X-Rate-Limit", "36/200")
return res, nil
},
)

response := struct {
Foo string `json:"foo"`
}{}

c := NewClient("")
err := c.(*client).DELETE("/foo", &response)
expectedErrStr := "api: error id=400 name=error_name detail=Error detail"
assert.EqualError(t, err, expectedErrStr)
})

t.Run("failure with with unexpected API error", func(t *testing.T) {
httpmock.Activate()
defer httpmock.DeactivateAndReset()

httpmock.RegisterResponder(http.MethodDelete, fmt.Sprintf("%s%s", apiEndpoint, "/foo"),
func(req *http.Request) (*http.Response, error) {
return httpmock.NewStringResponse(http.StatusInternalServerError, "Internal Server Error"), nil
},
)

response := struct {
Foo string `json:"foo"`
}{}

c := NewClient("")
err := c.(*client).DELETE("/foo", &response)
expectedErrStr := "api: error id=500 name=unknown_api_error detail=Unknown API error"
assert.EqualError(t, err, expectedErrStr)
})

t.Run("silent failure due to invalid response model", func(t *testing.T) {
httpmock.Activate()
defer httpmock.DeactivateAndReset()

httpmock.RegisterResponder(http.MethodDelete, fmt.Sprintf("%s%s", apiEndpoint, "/foo"),
func(req *http.Request) (*http.Response, error) {
res := httpmock.NewStringResponse(http.StatusOK, `{"bar":"foo"}`)
res.Header.Add("X-Rate-Limit", "36/200")
return res, nil
},
)

response := struct {
Foo string `json:"foo"`
}{}

c := NewClient("")
err := c.(*client).DELETE("/foo", &response)
assert.NoError(t, err)
assert.Equal(t, struct {
Foo string `json:"foo"`
}{}, response)
})
}

0 comments on commit 64610ec

Please sign in to comment.