diff --git a/api/docs.go b/api/docs.go index c7069ebc..dc11ada8 100644 --- a/api/docs.go +++ b/api/docs.go @@ -1603,6 +1603,7 @@ const docTemplate = `{ "Envelopes" ], "summary": "Get envelopes", + "deprecated": true, "parameters": [ { "type": "string", @@ -1665,6 +1666,7 @@ const docTemplate = `{ "Envelopes" ], "summary": "Create envelope", + "deprecated": true, "parameters": [ { "description": "Envelope", @@ -1709,6 +1711,7 @@ const docTemplate = `{ "Envelopes" ], "summary": "Allowed HTTP verbs", + "deprecated": true, "responses": { "204": { "description": "No Content" @@ -1726,6 +1729,7 @@ const docTemplate = `{ "Envelopes" ], "summary": "Get envelope", + "deprecated": true, "parameters": [ { "type": "string", @@ -1768,6 +1772,7 @@ const docTemplate = `{ "Envelopes" ], "summary": "Delete envelope", + "deprecated": true, "parameters": [ { "type": "string", @@ -1807,6 +1812,7 @@ const docTemplate = `{ "Envelopes" ], "summary": "Allowed HTTP verbs", + "deprecated": true, "parameters": [ { "type": "string", @@ -1852,6 +1858,7 @@ const docTemplate = `{ "Envelopes" ], "summary": "Update envelope", + "deprecated": true, "parameters": [ { "type": "string", @@ -4519,6 +4526,326 @@ const docTemplate = `{ } } }, + "/v3/envelopes": { + "get": { + "description": "Returns a list of envelopes", + "produces": [ + "application/json" + ], + "tags": [ + "Envelopes" + ], + "summary": "Get envelopes", + "parameters": [ + { + "type": "string", + "description": "Filter by name", + "name": "name", + "in": "query" + }, + { + "type": "string", + "description": "Filter by note", + "name": "note", + "in": "query" + }, + { + "type": "string", + "description": "Filter by category ID", + "name": "category", + "in": "query" + }, + { + "type": "boolean", + "description": "Is the envelope archived?", + "name": "archived", + "in": "query" + }, + { + "type": "string", + "description": "Search for this text in name and note", + "name": "search", + "in": "query" + }, + { + "type": "integer", + "description": "The offset of the first Transaction returned. Defaults to 0.", + "name": "offset", + "in": "query" + }, + { + "type": "integer", + "description": "Maximum number of transactions to return. Defaults to 50.", + "name": "limit", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/controllers.EnvelopeListResponseV3" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/controllers.EnvelopeListResponseV3" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/controllers.EnvelopeListResponseV3" + } + } + } + }, + "post": { + "description": "Creates a new envelope", + "produces": [ + "application/json" + ], + "tags": [ + "Envelopes" + ], + "summary": "Create envelope", + "parameters": [ + { + "description": "Envelopes", + "name": "envelope", + "in": "body", + "required": true, + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/models.EnvelopeCreate" + } + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/controllers.EnvelopeCreateResponseV3" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/controllers.EnvelopeCreateResponseV3" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/controllers.EnvelopeCreateResponseV3" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/controllers.EnvelopeCreateResponseV3" + } + } + } + }, + "options": { + "description": "Returns an empty response with the HTTP Header \"allow\" set to the allowed HTTP verbs", + "tags": [ + "Envelopes" + ], + "summary": "Allowed HTTP verbs", + "responses": { + "204": { + "description": "No Content" + } + } + } + }, + "/v3/envelopes/{id}": { + "get": { + "description": "Returns a specific Envelope", + "produces": [ + "application/json" + ], + "tags": [ + "Envelopes" + ], + "summary": "Get Envelope", + "parameters": [ + { + "type": "string", + "description": "ID formatted as string", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/controllers.EnvelopeResponseV3" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/controllers.EnvelopeResponseV3" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/controllers.EnvelopeResponseV3" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/controllers.EnvelopeResponseV3" + } + } + } + }, + "delete": { + "description": "Deletes an envelope", + "tags": [ + "Envelopes" + ], + "summary": "Delete envelope", + "parameters": [ + { + "type": "string", + "description": "ID formatted as string", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "204": { + "description": "No Content" + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/httperrors.HTTPError" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/httperrors.HTTPError" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/httperrors.HTTPError" + } + } + } + }, + "options": { + "description": "Returns an empty response with the HTTP Header \"allow\" set to the allowed HTTP verbs", + "tags": [ + "Envelopes" + ], + "summary": "Allowed HTTP verbs", + "parameters": [ + { + "type": "string", + "description": "ID formatted as string", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "204": { + "description": "No Content" + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/httperrors.HTTPError" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/httperrors.HTTPError" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/httperrors.HTTPError" + } + } + } + }, + "patch": { + "description": "Updates an existing envelope. Only values to be updated need to be specified.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Envelopes" + ], + "summary": "Update envelope", + "parameters": [ + { + "type": "string", + "description": "ID formatted as string", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Envelope", + "name": "envelope", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/models.EnvelopeCreate" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/controllers.EnvelopeResponseV3" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/controllers.EnvelopeResponseV3" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/controllers.EnvelopeResponseV3" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/controllers.EnvelopeResponseV3" + } + } + } + } + }, "/v3/import": { "get": { "description": "Returns general information about the v3 API", @@ -6219,6 +6546,23 @@ const docTemplate = `{ } } }, + "controllers.EnvelopeCreateResponseV3": { + "type": "object", + "properties": { + "data": { + "description": "Data for the Envelope", + "type": "array", + "items": { + "$ref": "#/definitions/controllers.EnvelopeResponseV3" + } + }, + "error": { + "description": "The error, if any occurred", + "type": "string", + "example": "the specified resource ID is not a valid UUID" + } + } + }, "controllers.EnvelopeListResponse": { "type": "object", "properties": { @@ -6231,6 +6575,31 @@ const docTemplate = `{ } } }, + "controllers.EnvelopeListResponseV3": { + "type": "object", + "properties": { + "data": { + "description": "List of Envelopes", + "type": "array", + "items": { + "$ref": "#/definitions/controllers.EnvelopeV3" + } + }, + "error": { + "description": "The error, if any occurred", + "type": "string", + "example": "the specified resource ID is not a valid UUID" + }, + "pagination": { + "description": "Pagination information", + "allOf": [ + { + "$ref": "#/definitions/controllers.Pagination" + } + ] + } + } + }, "controllers.EnvelopeMonthResponse": { "type": "object", "properties": { @@ -6257,6 +6626,86 @@ const docTemplate = `{ } } }, + "controllers.EnvelopeResponseV3": { + "type": "object", + "properties": { + "data": { + "description": "Data for the Envelope", + "allOf": [ + { + "$ref": "#/definitions/controllers.EnvelopeV3" + } + ] + }, + "error": { + "description": "The error, if any occurred", + "type": "string", + "example": "the specified resource ID is not a valid UUID" + } + } + }, + "controllers.EnvelopeV3": { + "type": "object", + "properties": { + "categoryId": { + "description": "ID of the category the envelope belongs to", + "type": "string", + "example": "878c831f-af99-4a71-b3ca-80deb7d793c1" + }, + "createdAt": { + "description": "Time the resource was created", + "type": "string", + "example": "2022-04-02T19:28:44.491514Z" + }, + "deletedAt": { + "description": "Time the resource was marked as deleted", + "type": "string", + "example": "2022-04-22T21:01:05.058161Z" + }, + "hidden": { + "description": "Is the envelope hidden?", + "type": "boolean", + "default": false, + "example": true + }, + "id": { + "description": "UUID for the resource", + "type": "string", + "example": "65392deb-5e92-4268-b114-297faad6cdce" + }, + "links": { + "description": "Links to related resources", + "type": "object", + "properties": { + "self": { + "description": "The envelope itself", + "type": "string", + "example": "https://example.com/api/v3/envelopes/45b6b5b9-f746-4ae9-b77b-7688b91f8166" + }, + "transactions": { + "description": "The envelope's transactions", + "type": "string", + "example": "https://example.com/api/v3/transactions?envelope=45b6b5b9-f746-4ae9-b77b-7688b91f8166" + } + } + }, + "name": { + "description": "Name of the envelope", + "type": "string", + "example": "Groceries" + }, + "note": { + "description": "Notes about the envelope", + "type": "string", + "example": "For stuff bought at supermarkets and drugstores" + }, + "updatedAt": { + "description": "Last time the resource was updated", + "type": "string", + "example": "2022-04-17T20:14:01.048145Z" + } + } + }, "controllers.ImportPreviewList": { "type": "object", "properties": { @@ -7891,6 +8340,11 @@ const docTemplate = `{ "type": "string", "example": "https://example.com/api/v3/budgets" }, + "envelopes": { + "description": "URL of Envelope collection endpoint", + "type": "string", + "example": "https://example.com/api/v3/envelopes" + }, "import": { "description": "URL of import list endpoint", "type": "string", diff --git a/api/swagger.json b/api/swagger.json index 3a99003d..bc51c353 100644 --- a/api/swagger.json +++ b/api/swagger.json @@ -1592,6 +1592,7 @@ "Envelopes" ], "summary": "Get envelopes", + "deprecated": true, "parameters": [ { "type": "string", @@ -1654,6 +1655,7 @@ "Envelopes" ], "summary": "Create envelope", + "deprecated": true, "parameters": [ { "description": "Envelope", @@ -1698,6 +1700,7 @@ "Envelopes" ], "summary": "Allowed HTTP verbs", + "deprecated": true, "responses": { "204": { "description": "No Content" @@ -1715,6 +1718,7 @@ "Envelopes" ], "summary": "Get envelope", + "deprecated": true, "parameters": [ { "type": "string", @@ -1757,6 +1761,7 @@ "Envelopes" ], "summary": "Delete envelope", + "deprecated": true, "parameters": [ { "type": "string", @@ -1796,6 +1801,7 @@ "Envelopes" ], "summary": "Allowed HTTP verbs", + "deprecated": true, "parameters": [ { "type": "string", @@ -1841,6 +1847,7 @@ "Envelopes" ], "summary": "Update envelope", + "deprecated": true, "parameters": [ { "type": "string", @@ -4508,6 +4515,326 @@ } } }, + "/v3/envelopes": { + "get": { + "description": "Returns a list of envelopes", + "produces": [ + "application/json" + ], + "tags": [ + "Envelopes" + ], + "summary": "Get envelopes", + "parameters": [ + { + "type": "string", + "description": "Filter by name", + "name": "name", + "in": "query" + }, + { + "type": "string", + "description": "Filter by note", + "name": "note", + "in": "query" + }, + { + "type": "string", + "description": "Filter by category ID", + "name": "category", + "in": "query" + }, + { + "type": "boolean", + "description": "Is the envelope archived?", + "name": "archived", + "in": "query" + }, + { + "type": "string", + "description": "Search for this text in name and note", + "name": "search", + "in": "query" + }, + { + "type": "integer", + "description": "The offset of the first Transaction returned. Defaults to 0.", + "name": "offset", + "in": "query" + }, + { + "type": "integer", + "description": "Maximum number of transactions to return. Defaults to 50.", + "name": "limit", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/controllers.EnvelopeListResponseV3" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/controllers.EnvelopeListResponseV3" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/controllers.EnvelopeListResponseV3" + } + } + } + }, + "post": { + "description": "Creates a new envelope", + "produces": [ + "application/json" + ], + "tags": [ + "Envelopes" + ], + "summary": "Create envelope", + "parameters": [ + { + "description": "Envelopes", + "name": "envelope", + "in": "body", + "required": true, + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/models.EnvelopeCreate" + } + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/controllers.EnvelopeCreateResponseV3" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/controllers.EnvelopeCreateResponseV3" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/controllers.EnvelopeCreateResponseV3" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/controllers.EnvelopeCreateResponseV3" + } + } + } + }, + "options": { + "description": "Returns an empty response with the HTTP Header \"allow\" set to the allowed HTTP verbs", + "tags": [ + "Envelopes" + ], + "summary": "Allowed HTTP verbs", + "responses": { + "204": { + "description": "No Content" + } + } + } + }, + "/v3/envelopes/{id}": { + "get": { + "description": "Returns a specific Envelope", + "produces": [ + "application/json" + ], + "tags": [ + "Envelopes" + ], + "summary": "Get Envelope", + "parameters": [ + { + "type": "string", + "description": "ID formatted as string", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/controllers.EnvelopeResponseV3" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/controllers.EnvelopeResponseV3" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/controllers.EnvelopeResponseV3" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/controllers.EnvelopeResponseV3" + } + } + } + }, + "delete": { + "description": "Deletes an envelope", + "tags": [ + "Envelopes" + ], + "summary": "Delete envelope", + "parameters": [ + { + "type": "string", + "description": "ID formatted as string", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "204": { + "description": "No Content" + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/httperrors.HTTPError" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/httperrors.HTTPError" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/httperrors.HTTPError" + } + } + } + }, + "options": { + "description": "Returns an empty response with the HTTP Header \"allow\" set to the allowed HTTP verbs", + "tags": [ + "Envelopes" + ], + "summary": "Allowed HTTP verbs", + "parameters": [ + { + "type": "string", + "description": "ID formatted as string", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "204": { + "description": "No Content" + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/httperrors.HTTPError" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/httperrors.HTTPError" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/httperrors.HTTPError" + } + } + } + }, + "patch": { + "description": "Updates an existing envelope. Only values to be updated need to be specified.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Envelopes" + ], + "summary": "Update envelope", + "parameters": [ + { + "type": "string", + "description": "ID formatted as string", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Envelope", + "name": "envelope", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/models.EnvelopeCreate" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/controllers.EnvelopeResponseV3" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/controllers.EnvelopeResponseV3" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/controllers.EnvelopeResponseV3" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/controllers.EnvelopeResponseV3" + } + } + } + } + }, "/v3/import": { "get": { "description": "Returns general information about the v3 API", @@ -6208,6 +6535,23 @@ } } }, + "controllers.EnvelopeCreateResponseV3": { + "type": "object", + "properties": { + "data": { + "description": "Data for the Envelope", + "type": "array", + "items": { + "$ref": "#/definitions/controllers.EnvelopeResponseV3" + } + }, + "error": { + "description": "The error, if any occurred", + "type": "string", + "example": "the specified resource ID is not a valid UUID" + } + } + }, "controllers.EnvelopeListResponse": { "type": "object", "properties": { @@ -6220,6 +6564,31 @@ } } }, + "controllers.EnvelopeListResponseV3": { + "type": "object", + "properties": { + "data": { + "description": "List of Envelopes", + "type": "array", + "items": { + "$ref": "#/definitions/controllers.EnvelopeV3" + } + }, + "error": { + "description": "The error, if any occurred", + "type": "string", + "example": "the specified resource ID is not a valid UUID" + }, + "pagination": { + "description": "Pagination information", + "allOf": [ + { + "$ref": "#/definitions/controllers.Pagination" + } + ] + } + } + }, "controllers.EnvelopeMonthResponse": { "type": "object", "properties": { @@ -6246,6 +6615,86 @@ } } }, + "controllers.EnvelopeResponseV3": { + "type": "object", + "properties": { + "data": { + "description": "Data for the Envelope", + "allOf": [ + { + "$ref": "#/definitions/controllers.EnvelopeV3" + } + ] + }, + "error": { + "description": "The error, if any occurred", + "type": "string", + "example": "the specified resource ID is not a valid UUID" + } + } + }, + "controllers.EnvelopeV3": { + "type": "object", + "properties": { + "categoryId": { + "description": "ID of the category the envelope belongs to", + "type": "string", + "example": "878c831f-af99-4a71-b3ca-80deb7d793c1" + }, + "createdAt": { + "description": "Time the resource was created", + "type": "string", + "example": "2022-04-02T19:28:44.491514Z" + }, + "deletedAt": { + "description": "Time the resource was marked as deleted", + "type": "string", + "example": "2022-04-22T21:01:05.058161Z" + }, + "hidden": { + "description": "Is the envelope hidden?", + "type": "boolean", + "default": false, + "example": true + }, + "id": { + "description": "UUID for the resource", + "type": "string", + "example": "65392deb-5e92-4268-b114-297faad6cdce" + }, + "links": { + "description": "Links to related resources", + "type": "object", + "properties": { + "self": { + "description": "The envelope itself", + "type": "string", + "example": "https://example.com/api/v3/envelopes/45b6b5b9-f746-4ae9-b77b-7688b91f8166" + }, + "transactions": { + "description": "The envelope's transactions", + "type": "string", + "example": "https://example.com/api/v3/transactions?envelope=45b6b5b9-f746-4ae9-b77b-7688b91f8166" + } + } + }, + "name": { + "description": "Name of the envelope", + "type": "string", + "example": "Groceries" + }, + "note": { + "description": "Notes about the envelope", + "type": "string", + "example": "For stuff bought at supermarkets and drugstores" + }, + "updatedAt": { + "description": "Last time the resource was updated", + "type": "string", + "example": "2022-04-17T20:14:01.048145Z" + } + } + }, "controllers.ImportPreviewList": { "type": "object", "properties": { @@ -7880,6 +8329,11 @@ "type": "string", "example": "https://example.com/api/v3/budgets" }, + "envelopes": { + "description": "URL of Envelope collection endpoint", + "type": "string", + "example": "https://example.com/api/v3/envelopes" + }, "import": { "description": "URL of import list endpoint", "type": "string", diff --git a/api/swagger.yaml b/api/swagger.yaml index 53037bd2..c0b692fb 100644 --- a/api/swagger.yaml +++ b/api/swagger.yaml @@ -619,6 +619,18 @@ definitions: example: "2022-04-17T20:14:01.048145Z" type: string type: object + controllers.EnvelopeCreateResponseV3: + properties: + data: + description: Data for the Envelope + items: + $ref: '#/definitions/controllers.EnvelopeResponseV3' + type: array + error: + description: The error, if any occurred + example: the specified resource ID is not a valid UUID + type: string + type: object controllers.EnvelopeListResponse: properties: data: @@ -627,6 +639,22 @@ definitions: $ref: '#/definitions/controllers.Envelope' type: array type: object + controllers.EnvelopeListResponseV3: + properties: + data: + description: List of Envelopes + items: + $ref: '#/definitions/controllers.EnvelopeV3' + type: array + error: + description: The error, if any occurred + example: the specified resource ID is not a valid UUID + type: string + pagination: + allOf: + - $ref: '#/definitions/controllers.Pagination' + description: Pagination information + type: object controllers.EnvelopeMonthResponse: properties: data: @@ -641,6 +669,65 @@ definitions: - $ref: '#/definitions/controllers.Envelope' description: Data for the Envelope type: object + controllers.EnvelopeResponseV3: + properties: + data: + allOf: + - $ref: '#/definitions/controllers.EnvelopeV3' + description: Data for the Envelope + error: + description: The error, if any occurred + example: the specified resource ID is not a valid UUID + type: string + type: object + controllers.EnvelopeV3: + properties: + categoryId: + description: ID of the category the envelope belongs to + example: 878c831f-af99-4a71-b3ca-80deb7d793c1 + type: string + createdAt: + description: Time the resource was created + example: "2022-04-02T19:28:44.491514Z" + type: string + deletedAt: + description: Time the resource was marked as deleted + example: "2022-04-22T21:01:05.058161Z" + type: string + hidden: + default: false + description: Is the envelope hidden? + example: true + type: boolean + id: + description: UUID for the resource + example: 65392deb-5e92-4268-b114-297faad6cdce + type: string + links: + description: Links to related resources + properties: + self: + description: The envelope itself + example: https://example.com/api/v3/envelopes/45b6b5b9-f746-4ae9-b77b-7688b91f8166 + type: string + transactions: + description: The envelope's transactions + example: https://example.com/api/v3/transactions?envelope=45b6b5b9-f746-4ae9-b77b-7688b91f8166 + type: string + type: object + name: + description: Name of the envelope + example: Groceries + type: string + note: + description: Notes about the envelope + example: For stuff bought at supermarkets and drugstores + type: string + updatedAt: + description: Last time the resource was updated + example: "2022-04-17T20:14:01.048145Z" + type: string + type: object controllers.ImportPreviewList: properties: data: @@ -1887,6 +1974,10 @@ definitions: description: URL of Budget collection endpoint example: https://example.com/api/v3/budgets type: string + envelopes: + description: URL of Envelope collection endpoint + example: https://example.com/api/v3/envelopes + type: string import: description: URL of import list endpoint example: https://example.com/api/v3/import @@ -3001,6 +3092,7 @@ paths: - Categories /v1/envelopes: get: + deprecated: true description: Returns a list of envelopes parameters: - description: Filter by name @@ -3042,6 +3134,7 @@ paths: tags: - Envelopes options: + deprecated: true description: Returns an empty response with the HTTP Header "allow" set to the allowed HTTP verbs responses: @@ -3051,6 +3144,7 @@ paths: tags: - Envelopes post: + deprecated: true description: Creates a new envelope parameters: - description: Envelope @@ -3083,6 +3177,7 @@ paths: - Envelopes /v1/envelopes/{id}: delete: + deprecated: true description: Deletes an envelope parameters: - description: ID formatted as string @@ -3109,6 +3204,7 @@ paths: tags: - Envelopes get: + deprecated: true description: Returns a specific envelope parameters: - description: ID formatted as string @@ -3139,6 +3235,7 @@ paths: tags: - Envelopes options: + deprecated: true description: Returns an empty response with the HTTP Header "allow" set to the allowed HTTP verbs parameters: @@ -3168,6 +3265,7 @@ paths: patch: consumes: - application/json + deprecated: true description: Updates an existing envelope. Only values to be updated need to be specified. parameters: @@ -4994,6 +5092,221 @@ paths: summary: Update budget tags: - Budgets + /v3/envelopes: + get: + description: Returns a list of envelopes + parameters: + - description: Filter by name + in: query + name: name + type: string + - description: Filter by note + in: query + name: note + type: string + - description: Filter by category ID + in: query + name: category + type: string + - description: Is the envelope archived? + in: query + name: archived + type: boolean + - description: Search for this text in name and note + in: query + name: search + type: string + - description: The offset of the first Transaction returned. Defaults to 0. + in: query + name: offset + type: integer + - description: Maximum number of transactions to return. Defaults to 50. + in: query + name: limit + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/controllers.EnvelopeListResponseV3' + "400": + description: Bad Request + schema: + $ref: '#/definitions/controllers.EnvelopeListResponseV3' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/controllers.EnvelopeListResponseV3' + summary: Get envelopes + tags: + - Envelopes + options: + description: Returns an empty response with the HTTP Header "allow" set to the + allowed HTTP verbs + responses: + "204": + description: No Content + summary: Allowed HTTP verbs + tags: + - Envelopes + post: + description: Creates a new envelope + parameters: + - description: Envelopes + in: body + name: envelope + required: true + schema: + items: + $ref: '#/definitions/models.EnvelopeCreate' + type: array + produces: + - application/json + responses: + "201": + description: Created + schema: + $ref: '#/definitions/controllers.EnvelopeCreateResponseV3' + "400": + description: Bad Request + schema: + $ref: '#/definitions/controllers.EnvelopeCreateResponseV3' + "404": + description: Not Found + schema: + $ref: '#/definitions/controllers.EnvelopeCreateResponseV3' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/controllers.EnvelopeCreateResponseV3' + summary: Create envelope + tags: + - Envelopes + /v3/envelopes/{id}: + delete: + description: Deletes an envelope + parameters: + - description: ID formatted as string + in: path + name: id + required: true + type: string + responses: + "204": + description: No Content + "400": + description: Bad Request + schema: + $ref: '#/definitions/httperrors.HTTPError' + "404": + description: Not Found + schema: + $ref: '#/definitions/httperrors.HTTPError' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/httperrors.HTTPError' + summary: Delete envelope + tags: + - Envelopes + get: + description: Returns a specific Envelope + parameters: + - description: ID formatted as string + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/controllers.EnvelopeResponseV3' + "400": + description: Bad Request + schema: + $ref: '#/definitions/controllers.EnvelopeResponseV3' + "404": + description: Not Found + schema: + $ref: '#/definitions/controllers.EnvelopeResponseV3' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/controllers.EnvelopeResponseV3' + summary: Get Envelope + tags: + - Envelopes + options: + description: Returns an empty response with the HTTP Header "allow" set to the + allowed HTTP verbs + parameters: + - description: ID formatted as string + in: path + name: id + required: true + type: string + responses: + "204": + description: No Content + "400": + description: Bad Request + schema: + $ref: '#/definitions/httperrors.HTTPError' + "404": + description: Not Found + schema: + $ref: '#/definitions/httperrors.HTTPError' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/httperrors.HTTPError' + summary: Allowed HTTP verbs + tags: + - Envelopes + patch: + consumes: + - application/json + description: Updates an existing envelope. Only values to be updated need to + be specified. + parameters: + - description: ID formatted as string + in: path + name: id + required: true + type: string + - description: Envelope + in: body + name: envelope + required: true + schema: + $ref: '#/definitions/models.EnvelopeCreate' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/controllers.EnvelopeResponseV3' + "400": + description: Bad Request + schema: + $ref: '#/definitions/controllers.EnvelopeResponseV3' + "404": + description: Not Found + schema: + $ref: '#/definitions/controllers.EnvelopeResponseV3' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/controllers.EnvelopeResponseV3' + summary: Update envelope + tags: + - Envelopes /v3/import: get: description: Returns general information about the v3 API diff --git a/pkg/controllers/account_v3.go b/pkg/controllers/account_v3.go index 7ae80e8a..75a9aeac 100644 --- a/pkg/controllers/account_v3.go +++ b/pkg/controllers/account_v3.go @@ -229,7 +229,6 @@ func (co Controller) CreateAccountsV3(c *gin.Context) { continue } - // Append the transaction aObject, err := co.getAccountV3(c, a.ID) if !err.Nil() { e := err.Error() diff --git a/pkg/controllers/account_v3_test.go b/pkg/controllers/account_v3_test.go index 0cce178d..b1ee5960 100644 --- a/pkg/controllers/account_v3_test.go +++ b/pkg/controllers/account_v3_test.go @@ -90,7 +90,7 @@ func (suite *TestSuiteStandard) TestAccountsV3Options() { for _, tt := range tests { suite.T().Run(tt.name, func(t *testing.T) { path := fmt.Sprintf("%s/%s", "http://example.com/v3/accounts", tt.id) - r := test.Request(suite.controller, suite.T(), http.MethodOptions, path, "") + r := test.Request(suite.controller, t, http.MethodOptions, path, "") assertHTTPStatus(t, &r, tt.status) if tt.status == http.StatusNoContent { @@ -129,8 +129,8 @@ func (suite *TestSuiteStandard) TestAccountsV3GetSingle() { suite.T().Run(tt.name, func(t *testing.T) { r := test.Request(suite.controller, t, tt.method, fmt.Sprintf("http://example.com/v3/accounts/%s", tt.id), "") - var budget controllers.AccountResponseV3 - suite.decodeResponse(&r, &budget) + var account controllers.AccountResponseV3 + suite.decodeResponse(&r, &account) assertHTTPStatus(t, &r, tt.status) }) } @@ -287,13 +287,12 @@ func (suite *TestSuiteStandard) TestAccountsV3UpdateFails() { var recorder httptest.ResponseRecorder if tt.id == "" { - // Create test Account - budget := suite.createTestAccountV3(suite.T(), models.AccountCreate{ + account := suite.createTestAccountV3(suite.T(), models.AccountCreate{ Name: "New Budget", Note: "More tests something something", }) - tt.id = budget.Data.ID.String() + tt.id = account.Data.ID.String() } // Update Account diff --git a/pkg/controllers/budget_v3_test.go b/pkg/controllers/budget_v3_test.go index e4c26d46..0ad7ebea 100644 --- a/pkg/controllers/budget_v3_test.go +++ b/pkg/controllers/budget_v3_test.go @@ -83,7 +83,7 @@ func (suite *TestSuiteStandard) TestBudgetV3Options() { for _, tt := range tests { suite.T().Run(tt.name, func(t *testing.T) { path := fmt.Sprintf("%s/%s", "http://example.com/v3/budgets", tt.id) - r := test.Request(suite.controller, suite.T(), http.MethodOptions, path, "") + r := test.Request(suite.controller, t, http.MethodOptions, path, "") assertHTTPStatus(t, &r, tt.status) if tt.status == http.StatusNoContent { diff --git a/pkg/controllers/cleanup_v3_test.go b/pkg/controllers/cleanup_v3_test.go index d834620e..9b9581a4 100644 --- a/pkg/controllers/cleanup_v3_test.go +++ b/pkg/controllers/cleanup_v3_test.go @@ -17,7 +17,7 @@ func (suite *TestSuiteStandard) TestCleanupV3() { _ = suite.createTestBudget(models.BudgetCreate{}) account := suite.createTestAccountV3(suite.T(), models.AccountCreate{Name: "TestCleanup"}) _ = suite.createTestCategory(models.CategoryCreate{}) - envelope := suite.createTestEnvelope(models.EnvelopeCreate{}) + envelope := suite.createTestEnvelopeV3(suite.T(), models.EnvelopeCreate{}) _ = suite.createTestAllocation(models.AllocationCreate{}) _ = suite.createTestTransaction(models.TransactionCreate{Amount: decimal.NewFromFloat(17.32)}) _ = suite.createTestMonthConfig(envelope.Data.ID, types.NewMonth(time.Now().Year(), time.Now().Month()), models.MonthConfigCreate{}) diff --git a/pkg/controllers/envelope.go b/pkg/controllers/envelope_v1.go similarity index 66% rename from pkg/controllers/envelope.go rename to pkg/controllers/envelope_v1.go index 6e102ede..5c262b15 100644 --- a/pkg/controllers/envelope.go +++ b/pkg/controllers/envelope_v1.go @@ -99,28 +99,26 @@ func (co Controller) RegisterEnvelopeRoutes(r *gin.RouterGroup) { } } -// OptionsEnvelopeList returns the allowed HTTP methods -// -// @Summary Allowed HTTP verbs -// @Description Returns an empty response with the HTTP Header "allow" set to the allowed HTTP verbs -// @Tags Envelopes -// @Success 204 -// @Router /v1/envelopes [options] +// @Summary Allowed HTTP verbs +// @Description Returns an empty response with the HTTP Header "allow" set to the allowed HTTP verbs +// @Tags Envelopes +// @Success 204 +// @Router /v1/envelopes [options] +// @Deprecated true func (co Controller) OptionsEnvelopeList(c *gin.Context) { httputil.OptionsGetPost(c) } -// OptionsEnvelopeDetail returns the allowed HTTP methods -// -// @Summary Allowed HTTP verbs -// @Description Returns an empty response with the HTTP Header "allow" set to the allowed HTTP verbs -// @Tags Envelopes -// @Success 204 -// @Failure 400 {object} httperrors.HTTPError -// @Failure 404 {object} httperrors.HTTPError -// @Failure 500 {object} httperrors.HTTPError -// @Param id path string true "ID formatted as string" -// @Router /v1/envelopes/{id} [options] +// @Summary Allowed HTTP verbs +// @Description Returns an empty response with the HTTP Header "allow" set to the allowed HTTP verbs +// @Tags Envelopes +// @Success 204 +// @Failure 400 {object} httperrors.HTTPError +// @Failure 404 {object} httperrors.HTTPError +// @Failure 500 {object} httperrors.HTTPError +// @Param id path string true "ID formatted as string" +// @Router /v1/envelopes/{id} [options] +// @Deprecated true func (co Controller) OptionsEnvelopeDetail(c *gin.Context) { id, err := uuid.Parse(c.Param("id")) if err != nil { @@ -136,18 +134,17 @@ func (co Controller) OptionsEnvelopeDetail(c *gin.Context) { httputil.OptionsGetPatchDelete(c) } -// CreateEnvelope creates a new envelope -// -// @Summary Create envelope -// @Description Creates a new envelope -// @Tags Envelopes -// @Produce json -// @Success 201 {object} EnvelopeResponse -// @Failure 400 {object} httperrors.HTTPError -// @Failure 404 {object} httperrors.HTTPError -// @Failure 500 {object} httperrors.HTTPError -// @Param envelope body models.EnvelopeCreate true "Envelope" -// @Router /v1/envelopes [post] +// @Summary Create envelope +// @Description Creates a new envelope +// @Tags Envelopes +// @Produce json +// @Success 201 {object} EnvelopeResponse +// @Failure 400 {object} httperrors.HTTPError +// @Failure 404 {object} httperrors.HTTPError +// @Failure 500 {object} httperrors.HTTPError +// @Param envelope body models.EnvelopeCreate true "Envelope" +// @Router /v1/envelopes [post] +// @Deprecated true func (co Controller) CreateEnvelope(c *gin.Context) { var create models.EnvelopeCreate @@ -177,21 +174,20 @@ func (co Controller) CreateEnvelope(c *gin.Context) { c.JSON(http.StatusCreated, EnvelopeResponse{Data: r}) } -// GetEnvelopes returns a list of envelopes filtered by the query parameters -// -// @Summary Get envelopes -// @Description Returns a list of envelopes -// @Tags Envelopes -// @Produce json -// @Success 200 {object} EnvelopeListResponse -// @Failure 400 {object} httperrors.HTTPError -// @Failure 500 {object} httperrors.HTTPError -// @Router /v1/envelopes [get] -// @Param name query string false "Filter by name" -// @Param note query string false "Filter by note" -// @Param category query string false "Filter by category ID" -// @Param hidden query bool false "Is the envelope hidden?" -// @Param search query string false "Search for this text in name and note" +// @Summary Get envelopes +// @Description Returns a list of envelopes +// @Tags Envelopes +// @Produce json +// @Success 200 {object} EnvelopeListResponse +// @Failure 400 {object} httperrors.HTTPError +// @Failure 500 {object} httperrors.HTTPError +// @Router /v1/envelopes [get] +// @Param name query string false "Filter by name" +// @Param note query string false "Filter by note" +// @Param category query string false "Filter by category ID" +// @Param hidden query bool false "Is the envelope hidden?" +// @Param search query string false "Search for this text in name and note" +// @Deprecated true func (co Controller) GetEnvelopes(c *gin.Context) { var filter EnvelopeQueryFilter @@ -230,18 +226,17 @@ func (co Controller) GetEnvelopes(c *gin.Context) { c.JSON(http.StatusOK, EnvelopeListResponse{Data: r}) } -// GetEnvelope returns data about a specific envelope -// -// @Summary Get envelope -// @Description Returns a specific envelope -// @Tags Envelopes -// @Produce json -// @Success 200 {object} EnvelopeResponse -// @Failure 400 {object} httperrors.HTTPError -// @Failure 404 {object} httperrors.HTTPError -// @Failure 500 {object} httperrors.HTTPError -// @Param id path string true "ID formatted as string" -// @Router /v1/envelopes/{id} [get] +// @Summary Get envelope +// @Description Returns a specific envelope +// @Tags Envelopes +// @Produce json +// @Success 200 {object} EnvelopeResponse +// @Failure 400 {object} httperrors.HTTPError +// @Failure 404 {object} httperrors.HTTPError +// @Failure 500 {object} httperrors.HTTPError +// @Param id path string true "ID formatted as string" +// @Router /v1/envelopes/{id} [get] +// @Deprecated true func (co Controller) GetEnvelope(c *gin.Context) { id, err := uuid.Parse(c.Param("id")) if err != nil { @@ -262,20 +257,18 @@ func (co Controller) GetEnvelope(c *gin.Context) { c.JSON(http.StatusOK, EnvelopeResponse{Data: r}) } -// GetEnvelopeMonth returns month data for a specific envelope -// -// @Summary Get Envelope month data -// @Description Returns data about an envelope for a for a specific month. **Use GET /month endpoint with month and budgetId query parameters instead.** -// @Tags Envelopes -// @Produce json -// @Success 200 {object} EnvelopeMonthResponse -// @Failure 400 {object} httperrors.HTTPError -// @Failure 404 {object} httperrors.HTTPError -// @Failure 500 {object} httperrors.HTTPError -// @Param id path string true "ID formatted as string" -// @Param month path string true "The month in YYYY-MM format" -// @Router /v1/envelopes/{id}/{month} [get] -// @Deprecated true +// @Summary Get Envelope month data +// @Description Returns data about an envelope for a for a specific month. **Use GET /month endpoint with month and budgetId query parameters instead.** +// @Tags Envelopes +// @Produce json +// @Success 200 {object} EnvelopeMonthResponse +// @Failure 400 {object} httperrors.HTTPError +// @Failure 404 {object} httperrors.HTTPError +// @Failure 500 {object} httperrors.HTTPError +// @Param id path string true "ID formatted as string" +// @Param month path string true "The month in YYYY-MM format" +// @Router /v1/envelopes/{id}/{month} [get] +// @Deprecated true func (co Controller) GetEnvelopeMonth(c *gin.Context) { id, err := uuid.Parse(c.Param("id")) if err != nil { @@ -308,20 +301,19 @@ func (co Controller) GetEnvelopeMonth(c *gin.Context) { c.JSON(http.StatusOK, EnvelopeMonthResponse{Data: envelopeMonth}) } -// UpdateEnvelope updates data for an envelope -// -// @Summary Update envelope -// @Description Updates an existing envelope. Only values to be updated need to be specified. -// @Tags Envelopes -// @Accept json -// @Produce json -// @Success 200 {object} EnvelopeResponse -// @Failure 400 {object} httperrors.HTTPError -// @Failure 404 {object} httperrors.HTTPError -// @Failure 500 {object} httperrors.HTTPError -// @Param id path string true "ID formatted as string" -// @Param envelope body models.EnvelopeCreate true "Envelope" -// @Router /v1/envelopes/{id} [patch] +// @Summary Update envelope +// @Description Updates an existing envelope. Only values to be updated need to be specified. +// @Tags Envelopes +// @Accept json +// @Produce json +// @Success 200 {object} EnvelopeResponse +// @Failure 400 {object} httperrors.HTTPError +// @Failure 404 {object} httperrors.HTTPError +// @Failure 500 {object} httperrors.HTTPError +// @Param id path string true "ID formatted as string" +// @Param envelope body models.EnvelopeCreate true "Envelope" +// @Router /v1/envelopes/{id} [patch] +// @Deprecated true func (co Controller) UpdateEnvelope(c *gin.Context) { id, err := uuid.Parse(c.Param("id")) if err != nil { @@ -356,17 +348,16 @@ func (co Controller) UpdateEnvelope(c *gin.Context) { c.JSON(http.StatusOK, EnvelopeResponse{Data: r}) } -// DeleteEnvelope deletes an envelope -// -// @Summary Delete envelope -// @Description Deletes an envelope -// @Tags Envelopes -// @Success 204 -// @Failure 400 {object} httperrors.HTTPError -// @Failure 404 {object} httperrors.HTTPError -// @Failure 500 {object} httperrors.HTTPError -// @Param id path string true "ID formatted as string" -// @Router /v1/envelopes/{id} [delete] +// @Summary Delete envelope +// @Description Deletes an envelope +// @Tags Envelopes +// @Success 204 +// @Failure 400 {object} httperrors.HTTPError +// @Failure 404 {object} httperrors.HTTPError +// @Failure 500 {object} httperrors.HTTPError +// @Param id path string true "ID formatted as string" +// @Router /v1/envelopes/{id} [delete] +// @Deprecated true func (co Controller) DeleteEnvelope(c *gin.Context) { id, err := uuid.Parse(c.Param("id")) if err != nil { diff --git a/pkg/controllers/envelope_test.go b/pkg/controllers/envelope_v1_test.go similarity index 100% rename from pkg/controllers/envelope_test.go rename to pkg/controllers/envelope_v1_test.go diff --git a/pkg/controllers/envelope_v3.go b/pkg/controllers/envelope_v3.go new file mode 100644 index 00000000..8593d410 --- /dev/null +++ b/pkg/controllers/envelope_v3.go @@ -0,0 +1,466 @@ +package controllers + +import ( + "fmt" + "net/http" + + "github.com/envelope-zero/backend/v3/pkg/database" + "github.com/envelope-zero/backend/v3/pkg/httperrors" + "github.com/envelope-zero/backend/v3/pkg/httputil" + "github.com/envelope-zero/backend/v3/pkg/models" + "github.com/gin-gonic/gin" + "github.com/google/uuid" + "golang.org/x/exp/slices" +) + +type EnvelopeV3 struct { + models.Envelope + Links struct { + Self string `json:"self" example:"https://example.com/api/v3/envelopes/45b6b5b9-f746-4ae9-b77b-7688b91f8166"` // The envelope itself + Transactions string `json:"transactions" example:"https://example.com/api/v3/transactions?envelope=45b6b5b9-f746-4ae9-b77b-7688b91f8166"` // The envelope's transactions + } `json:"links"` // Links to related resources +} + +func (e *EnvelopeV3) links(c *gin.Context) { + url := c.GetString(string(database.ContextURL)) + self := fmt.Sprintf("%s/v3/envelopes/%s", url, e.ID) + + e.Links.Self = self + e.Links.Transactions = fmt.Sprintf("%s/v3/transactions?envelope=%s", url, e.ID) +} + +func (co Controller) getEnvelopeV3(c *gin.Context, id uuid.UUID) (EnvelopeV3, httperrors.Error) { + m, err := getResourceByID[models.Envelope](c, co, id) + if !err.Nil() { + return EnvelopeV3{}, httperrors.Error{} + } + + r := EnvelopeV3{ + Envelope: m, + } + + r.links(c) + return r, httperrors.Error{} +} + +type EnvelopeListResponseV3 struct { + Data []EnvelopeV3 `json:"data"` // List of Envelopes + Error *string `json:"error" example:"the specified resource ID is not a valid UUID"` // The error, if any occurred + Pagination *Pagination `json:"pagination"` // Pagination information +} + +type EnvelopeCreateResponseV3 struct { + Data []EnvelopeResponseV3 `json:"data"` // Data for the Envelope + Error *string `json:"error" example:"the specified resource ID is not a valid UUID"` // The error, if any occurred +} + +type EnvelopeResponseV3 struct { + Data *EnvelopeV3 `json:"data"` // Data for the Envelope + Error *string `json:"error" example:"the specified resource ID is not a valid UUID"` // The error, if any occurred +} + +type EnvelopeMonthResponseV3 struct { + Data *models.EnvelopeMonth `json:"data"` // Data for the month for the envelope + Error *string `json:"error" example:"the specified resource ID is not a valid UUID"` // The error, if any occurred +} + +type EnvelopeQueryFilterV3 struct { + Name string `form:"name" filterField:"false"` // By name + CategoryID string `form:"category"` // By the ID of the category + Note string `form:"note" filterField:"false"` // By the note + Archived bool `form:"archived" filterField:"false"` // Is the envelope archived? + Search string `form:"search" filterField:"false"` // By string in name or note + Offset uint `form:"offset" filterField:"false"` // The offset of the first Transaction returned. Defaults to 0. + Limit int `form:"limit" filterField:"false"` // Maximum number of transactions to return. Defaults to 50. +} + +func (f EnvelopeQueryFilterV3) ToCreate() (models.EnvelopeCreate, httperrors.Error) { + categoryID, err := httputil.UUIDFromString(f.CategoryID) + if !err.Nil() { + return models.EnvelopeCreate{}, err + } + + return models.EnvelopeCreate{ + CategoryID: categoryID, + Hidden: f.Archived, + }, httperrors.Error{} +} + +// RegisterEnvelopeRoutes registers the routes for envelopes with +// the RouterGroup that is passed. +func (co Controller) RegisterEnvelopeRoutesV3(r *gin.RouterGroup) { + // Root group + { + r.OPTIONS("", co.OptionsEnvelopeListV3) + r.GET("", co.GetEnvelopesV3) + r.POST("", co.CreateEnvelopesV3) + } + + // Envelope with ID + { + r.OPTIONS("/:id", co.OptionsEnvelopeDetailV3) + r.GET("/:id", co.GetEnvelopeV3) + r.PATCH("/:id", co.UpdateEnvelopeV3) + r.DELETE("/:id", co.DeleteEnvelopeV3) + } +} + +// @Summary Allowed HTTP verbs +// @Description Returns an empty response with the HTTP Header "allow" set to the allowed HTTP verbs +// @Tags Envelopes +// @Success 204 +// @Router /v3/envelopes [options] +func (co Controller) OptionsEnvelopeListV3(c *gin.Context) { + httputil.OptionsGetPost(c) +} + +// @Summary Allowed HTTP verbs +// @Description Returns an empty response with the HTTP Header "allow" set to the allowed HTTP verbs +// @Tags Envelopes +// @Success 204 +// @Failure 400 {object} httperrors.HTTPError +// @Failure 404 {object} httperrors.HTTPError +// @Failure 500 {object} httperrors.HTTPError +// @Param id path string true "ID formatted as string" +// @Router /v3/envelopes/{id} [options] +func (co Controller) OptionsEnvelopeDetailV3(c *gin.Context) { + id, err := httputil.UUIDFromString(c.Param("id")) + if !err.Nil() { + c.JSON(err.Status, httperrors.HTTPError{ + Error: err.Error(), + }) + return + } + + _, err = getResourceByID[models.Envelope](c, co, id) + if !err.Nil() { + c.JSON(err.Status, httperrors.HTTPError{ + Error: err.Error(), + }) + return + } + + httputil.OptionsGetPatchDelete(c) +} + +// @Summary Create envelope +// @Description Creates a new envelope +// @Tags Envelopes +// @Produce json +// @Success 201 {object} EnvelopeCreateResponseV3 +// @Failure 400 {object} EnvelopeCreateResponseV3 +// @Failure 404 {object} EnvelopeCreateResponseV3 +// @Failure 500 {object} EnvelopeCreateResponseV3 +// @Param envelope body []models.EnvelopeCreate true "Envelopes" +// @Router /v3/envelopes [post] +func (co Controller) CreateEnvelopesV3(c *gin.Context) { + var envelopes []models.EnvelopeCreate + + // Bind data and return error if not possible + err := httputil.BindData(c, &envelopes) + if !err.Nil() { + e := err.Error() + c.JSON(err.Status, EnvelopeCreateResponseV3{ + Error: &e, + }) + return + } + + // The final http status. Will be modified when errors occur + status := http.StatusCreated + r := EnvelopeCreateResponseV3{} + + for _, create := range envelopes { + e := models.Envelope{ + EnvelopeCreate: create, + } + + dbErr := co.DB.Create(&e).Error + if dbErr != nil { + err := httperrors.GenericDBError[models.Envelope](e, c, dbErr) + s := err.Error() + c.JSON(err.Status, EnvelopeCreateResponseV3{ + Error: &s, + }) + return + } + + // Append the error + if !err.Nil() { + e := err.Error() + r.Data = append(r.Data, EnvelopeResponseV3{Error: &e}) + + // The final status code is the highest HTTP status code number since this also + // represents the priority we + if err.Status > status { + status = err.Status + } + continue + } + + eObject, err := co.getEnvelopeV3(c, e.ID) + if !err.Nil() { + e := err.Error() + c.JSON(err.Status, EnvelopeCreateResponseV3{ + Error: &e, + }) + return + } + r.Data = append(r.Data, EnvelopeResponseV3{Data: &eObject}) + } + + c.JSON(status, r) +} + +// @Summary Get envelopes +// @Description Returns a list of envelopes +// @Tags Envelopes +// @Produce json +// @Success 200 {object} EnvelopeListResponseV3 +// @Failure 400 {object} EnvelopeListResponseV3 +// @Failure 500 {object} EnvelopeListResponseV3 +// @Router /v3/envelopes [get] +// @Param name query string false "Filter by name" +// @Param note query string false "Filter by note" +// @Param category query string false "Filter by category ID" +// @Param archived query bool false "Is the envelope archived?" +// @Param search query string false "Search for this text in name and note" +// @Param offset query uint false "The offset of the first Transaction returned. Defaults to 0." +// @Param limit query int false "Maximum number of transactions to return. Defaults to 50." +func (co Controller) GetEnvelopesV3(c *gin.Context) { + var filter EnvelopeQueryFilterV3 + + // The filters contain only strings, so this will always succeed + _ = c.Bind(&filter) + + queryFields, setFields := httputil.GetURLFields(c.Request.URL, filter) + + // If the archived parameter is set, add "Hidden" to the query fields + // This is done since in v3, we're using the name "Archived", but the + // field is not yet updated in the database, which will happen later + if slices.Contains(setFields, "Archived") { + queryFields = append(queryFields, "Hidden") + } + + // Convert the QueryFilter to a Create struct + create, err := filter.ToCreate() + if !err.Nil() { + s := err.Error() + c.JSON(err.Status, EnvelopeListResponseV3{ + Error: &s, + }) + return + } + + q := co.DB. + Order("name ASC"). + Where(&models.Envelope{ + EnvelopeCreate: create, + }, queryFields...) + + q = stringFilters(co.DB, q, setFields, filter.Name, filter.Note, filter.Search) + + // Set the offset. Does not need checking since the default is 0 + q = q.Offset(int(filter.Offset)) + + // Default to 50 Accounts and set the limit + limit := 50 + if slices.Contains(setFields, "Limit") { + limit = filter.Limit + } + q = q.Limit(limit) + + var envelopes []models.Envelope + err = query(c, q.Find(&envelopes)) + + if !err.Nil() { + s := err.Error() + c.JSON(err.Status, EnvelopeListResponseV3{ + Error: &s, + }) + return + } + + var count int64 + err = query(c, q.Limit(-1).Offset(-1).Count(&count)) + if !err.Nil() { + e := err.Error() + c.JSON(err.Status, EnvelopeListResponseV3{ + Error: &e, + }) + return + } + + r := make([]EnvelopeV3, 0, len(envelopes)) + for _, e := range envelopes { + o, err := co.getEnvelopeV3(c, e.ID) + if !err.Nil() { + s := err.Error() + c.JSON(err.Status, EnvelopeListResponseV3{ + Error: &s, + }) + return + } + + r = append(r, o) + } + + c.JSON(http.StatusOK, EnvelopeListResponseV3{ + Data: r, + Pagination: &Pagination{ + Count: len(r), + Total: count, + Offset: filter.Offset, + Limit: limit, + }, + }) +} + +// @Summary Get Envelope +// @Description Returns a specific Envelope +// @Tags Envelopes +// @Produce json +// @Success 200 {object} EnvelopeResponseV3 +// @Failure 400 {object} EnvelopeResponseV3 +// @Failure 404 {object} EnvelopeResponseV3 +// @Failure 500 {object} EnvelopeResponseV3 +// @Param id path string true "ID formatted as string" +// @Router /v3/envelopes/{id} [get] +func (co Controller) GetEnvelopeV3(c *gin.Context) { + id, err := httputil.UUIDFromString(c.Param("id")) + if !err.Nil() { + s := err.Error() + c.JSON(err.Status, EnvelopeResponseV3{ + Error: &s, + }) + return + } + + m, err := getResourceByID[models.Envelope](c, co, id) + if !err.Nil() { + s := err.Error() + c.JSON(err.Status, EnvelopeResponseV3{ + Error: &s, + }) + return + } + + r, err := co.getEnvelopeV3(c, m.ID) + if !err.Nil() { + s := err.Error() + c.JSON(err.Status, EnvelopeResponseV3{ + Error: &s, + }) + return + } + + c.JSON(http.StatusOK, EnvelopeResponseV3{Data: &r}) +} + +// @Summary Update envelope +// @Description Updates an existing envelope. Only values to be updated need to be specified. +// @Tags Envelopes +// @Accept json +// @Produce json +// @Success 200 {object} EnvelopeResponseV3 +// @Failure 400 {object} EnvelopeResponseV3 +// @Failure 404 {object} EnvelopeResponseV3 +// @Failure 500 {object} EnvelopeResponseV3 +// @Param id path string true "ID formatted as string" +// @Param envelope body models.EnvelopeCreate true "Envelope" +// @Router /v3/envelopes/{id} [patch] +func (co Controller) UpdateEnvelopeV3(c *gin.Context) { + id, err := httputil.UUIDFromString(c.Param("id")) + if !err.Nil() { + s := err.Error() + c.JSON(err.Status, EnvelopeResponseV3{ + Error: &s, + }) + return + } + + envelope, err := getResourceByID[models.Envelope](c, co, id) + if !err.Nil() { + s := err.Error() + c.JSON(err.Status, EnvelopeResponseV3{ + Error: &s, + }) + return + } + + updateFields, err := httputil.GetBodyFields(c, models.EnvelopeCreate{}) + if !err.Nil() { + s := err.Error() + c.JSON(err.Status, EnvelopeResponseV3{ + Error: &s, + }) + return + } + + var data models.Envelope + err = httputil.BindData(c, &data.EnvelopeCreate) + if !err.Nil() { + s := err.Error() + c.JSON(err.Status, EnvelopeResponseV3{ + Error: &s, + }) + return + } + + err = query(c, co.DB.Model(&envelope).Select("", updateFields...).Updates(data)) + if !err.Nil() { + s := err.Error() + c.JSON(err.Status, EnvelopeResponseV3{ + Error: &s, + }) + return + } + + r, err := co.getEnvelopeV3(c, envelope.ID) + if !err.Nil() { + s := err.Error() + c.JSON(err.Status, EnvelopeResponseV3{ + Error: &s, + }) + return + } + + c.JSON(http.StatusOK, EnvelopeResponseV3{Data: &r}) +} + +// @Summary Delete envelope +// @Description Deletes an envelope +// @Tags Envelopes +// @Success 204 +// @Failure 400 {object} httperrors.HTTPError +// @Failure 404 {object} httperrors.HTTPError +// @Failure 500 {object} httperrors.HTTPError +// @Param id path string true "ID formatted as string" +// @Router /v3/envelopes/{id} [delete] +func (co Controller) DeleteEnvelopeV3(c *gin.Context) { + id, err := httputil.UUIDFromString(c.Param("id")) + if !err.Nil() { + c.JSON(err.Status, httperrors.HTTPError{ + Error: err.Error(), + }) + return + } + + envelope, err := getResourceByID[models.Envelope](c, co, id) + if !err.Nil() { + c.JSON(err.Status, httperrors.HTTPError{ + Error: err.Error(), + }) + return + } + + err = query(c, co.DB.Delete(&envelope)) + if !err.Nil() { + c.JSON(err.Status, httperrors.HTTPError{ + Error: err.Error(), + }) + return + } + + c.JSON(http.StatusNoContent, nil) +} diff --git a/pkg/controllers/envelope_v3_test.go b/pkg/controllers/envelope_v3_test.go new file mode 100644 index 00000000..c4d97ada --- /dev/null +++ b/pkg/controllers/envelope_v3_test.go @@ -0,0 +1,383 @@ +package controllers_test + +import ( + "fmt" + "net/http" + "net/http/httptest" + "testing" + + "github.com/envelope-zero/backend/v3/pkg/controllers" + "github.com/envelope-zero/backend/v3/pkg/models" + "github.com/envelope-zero/backend/v3/test" + "github.com/google/uuid" + "github.com/stretchr/testify/assert" +) + +func (suite *TestSuiteStandard) createTestEnvelopeV3(t *testing.T, c models.EnvelopeCreate, expectedStatus ...int) controllers.EnvelopeResponseV3 { + if c.CategoryID == uuid.Nil { + c.CategoryID = suite.createTestCategory(models.CategoryCreate{}).Data.ID + } + + if c.Name == "" { + c.Name = uuid.NewString() + } + + // Default to 200 OK as expected status + if len(expectedStatus) == 0 { + expectedStatus = append(expectedStatus, http.StatusCreated) + } + + body := []models.EnvelopeCreate{c} + + r := test.Request(suite.controller, t, http.MethodPost, "http://example.com/v3/envelopes", body) + assertHTTPStatus(t, &r, expectedStatus...) + + var e controllers.EnvelopeCreateResponseV3 + suite.decodeResponse(&r, &e) + + if r.Code == http.StatusCreated { + return e.Data[0] + } + + return controllers.EnvelopeResponseV3{} +} + +// TestEnvelopesV3DBClosed verifies that errors are processed correctly when +// the database is closed. +func (suite *TestSuiteStandard) TestEnvelopesV3DBClosed() { + b := suite.createTestCategory(models.CategoryCreate{}) + + tests := []struct { + name string // Name of the test + test func(t *testing.T) // Code to run + }{ + { + "Creation fails", + func(t *testing.T) { + suite.createTestEnvelopeV3(t, models.EnvelopeCreate{CategoryID: b.Data.ID}, http.StatusInternalServerError) + }, + }, + { + "GET fails", + func(t *testing.T) { + recorder := test.Request(suite.controller, t, http.MethodGet, "http://example.com/v3/envelopes", "") + assertHTTPStatus(t, &recorder, http.StatusInternalServerError) + assert.Contains(t, test.DecodeError(t, recorder.Body.Bytes()), "there is a problem with the database connection") + }, + }, + } + + for _, tt := range tests { + suite.T().Run(tt.name, func(t *testing.T) { + suite.CloseDB() + + tt.test(t) + }) + } +} + +// TestEnvelopesV3Options verifies that OPTIONS requests are handled correctly. +func (suite *TestSuiteStandard) TestEnvelopesV3Options() { + tests := []struct { + name string + id string // path at the Accounts endpoint to test + status int // Expected HTTP status code + }{ + {"No Envelope with this ID", uuid.New().String(), http.StatusNotFound}, + {"Not a valid UUID", "NotParseableAsUUID", http.StatusBadRequest}, + {"Envelope exists", suite.createTestEnvelopeV3(suite.T(), models.EnvelopeCreate{}).Data.ID.String(), http.StatusNoContent}, + } + + for _, tt := range tests { + suite.T().Run(tt.name, func(t *testing.T) { + path := fmt.Sprintf("%s/%s", "http://example.com/v3/envelopes", tt.id) + r := test.Request(suite.controller, t, http.MethodOptions, path, "") + assertHTTPStatus(t, &r, tt.status) + + if tt.status == http.StatusNoContent { + assert.Equal(t, "OPTIONS, GET, PATCH, DELETE", r.Header().Get("allow")) + } + }) + } +} + +// TestEnvelopesV3GetSingle verifies that requests for the resource endpoints are +// handled correctly. +func (suite *TestSuiteStandard) TestEnvelopesV3GetSingle() { + e := suite.createTestEnvelopeV3(suite.T(), models.EnvelopeCreate{}) + + tests := []struct { + name string + id string + status int + method string + }{ + {"GET Existing Envelope", e.Data.ID.String(), http.StatusOK, http.MethodGet}, + {"GET ID nil", uuid.Nil.String(), http.StatusBadRequest, http.MethodGet}, + {"GET No Envelope with this ID", uuid.New().String(), http.StatusNotFound, http.MethodGet}, + {"GET Invalid ID (negative number)", "-56", http.StatusBadRequest, http.MethodGet}, + {"GET Invalid ID (positive number)", "23", http.StatusBadRequest, http.MethodGet}, + {"GET Invalid ID (string)", "notaUUID", http.StatusBadRequest, http.MethodGet}, + {"PATCH Invalid ID (negative number)", "-56", http.StatusBadRequest, http.MethodPatch}, + {"PATCH Invalid ID (positive number)", "23", http.StatusBadRequest, http.MethodPatch}, + {"PATCH Invalid ID (string)", "notaUUID", http.StatusBadRequest, http.MethodPatch}, + {"DELETE Invalid ID (negative number)", "-56", http.StatusBadRequest, http.MethodDelete}, + {"DELETE Invalid ID (positive number)", "23", http.StatusBadRequest, http.MethodDelete}, + {"DELETE Invalid ID (string)", "notaUUID", http.StatusBadRequest, http.MethodDelete}, + } + + for _, tt := range tests { + suite.T().Run(tt.name, func(t *testing.T) { + r := test.Request(suite.controller, t, tt.method, fmt.Sprintf("http://example.com/v3/envelopes/%s", tt.id), "") + + var envelope controllers.EnvelopeResponseV3 + suite.decodeResponse(&r, &envelope) + assertHTTPStatus(t, &r, tt.status) + }) + } +} + +func (suite *TestSuiteStandard) TestEnvelopesV3GetFilter() { + c1 := suite.createTestCategory(models.CategoryCreate{}) + c2 := suite.createTestCategory(models.CategoryCreate{}) + + _ = suite.createTestEnvelopeV3(suite.T(), models.EnvelopeCreate{ + Name: "Groceries", + Note: "For the stuff bought in supermarkets", + CategoryID: c1.Data.ID, + }) + + _ = suite.createTestEnvelopeV3(suite.T(), models.EnvelopeCreate{ + Name: "Hairdresser", + Note: "Becauseā€¦ Hair!", + CategoryID: c2.Data.ID, + Hidden: true, + }) + + _ = suite.createTestEnvelopeV3(suite.T(), models.EnvelopeCreate{ + Name: "Stamps", + Note: "Because each stamp needs to go on an envelope. Hopefully it's not hairy", + CategoryID: c2.Data.ID, + }) + + tests := []struct { + name string + query string + len int + }{ + {"Category 2", fmt.Sprintf("category=%s", c2.Data.ID), 2}, + {"Category Not Existing", "category=e0f9ff7a-9f07-463c-bbd2-0d72d09d3cc6", 0}, + {"Empty Note", "note=", 0}, + {"Empty Name", "name=", 0}, + {"Name & Note", "name=Groceries¬e=For the stuff bought in supermarkets", 1}, + {"Fuzzy name", "name=es", 2}, + {"Fuzzy note", "note=Because", 2}, + {"Not archived", "archived=false", 2}, + {"Archived", "archived=true", 1}, + {"Search for 'hair'", "search=hair", 2}, + {"Search for 'st'", "search=st", 2}, + {"Search for 'STUFF'", "search=STUFF", 1}, + {"Offset 2", "offset=2", 1}, + {"Offset 0, limit 2", "offset=0&limit=2", 2}, + {"Limit 4", "limit=4", 3}, + {"Limit 0", "limit=0", 0}, + {"Limit -1", "limit=-1", 3}, + } + + for _, tt := range tests { + suite.T().Run(tt.name, func(t *testing.T) { + var re controllers.EnvelopeListResponse + r := test.Request(suite.controller, t, http.MethodGet, fmt.Sprintf("/v3/envelopes?%s", tt.query), "") + assertHTTPStatus(suite.T(), &r, http.StatusOK) + suite.decodeResponse(&r, &re) + + assert.Equal(t, tt.len, len(re.Data), "Request ID: %s", r.Result().Header.Get("x-request-id")) + }) + } +} + +func (suite *TestSuiteStandard) TestEnvelopesV3CreateFails() { + // Test envelope for uniqueness + e := suite.createTestEnvelopeV3(suite.T(), models.EnvelopeCreate{ + Name: "Unique Envelope Name for Category", + }) + + tests := []struct { + name string + body any + status int // expected HTTP status + }{ + {"Broken Body", `[{ "note": 2 }]`, http.StatusBadRequest}, + {"No body", "", http.StatusBadRequest}, + { + "No Category", + `[{ "note": "Some text" }]`, + http.StatusBadRequest, + }, + { + "Non-existing Category", + `[{ "categoryId": "ea85ad1a-3679-4ced-b83b-89566c12ece9" }]`, + http.StatusBadRequest, + }, + { + "Duplicate name in Category", + models.EnvelopeCreate{ + CategoryID: e.Data.CategoryID, + Name: e.Data.Name, + }, + http.StatusBadRequest, + }, + } + + for _, tt := range tests { + suite.T().Run(tt.name, func(t *testing.T) { + recorder := test.Request(suite.controller, t, http.MethodPost, "http://example.com/v3/envelopes", tt.body) + assertHTTPStatus(t, &recorder, tt.status) + }) + } +} + +func (suite *TestSuiteStandard) TestEnvelopesV3Update() { + envelope := suite.createTestEnvelopeV3(suite.T(), models.EnvelopeCreate{Name: "New envelope", Note: "Keks is a cuddly cat"}) + + recorder := test.Request(suite.controller, suite.T(), http.MethodPatch, envelope.Data.Links.Self, map[string]any{ + "name": "Updated new envelope for testing", + "note": "", + }) + assertHTTPStatus(suite.T(), &recorder, http.StatusOK) + + var updatedEnvelope controllers.EnvelopeResponse + suite.decodeResponse(&recorder, &updatedEnvelope) + + assert.Equal(suite.T(), "", updatedEnvelope.Data.Note) + assert.Equal(suite.T(), "Updated new envelope for testing", updatedEnvelope.Data.Name) +} + +func (suite *TestSuiteStandard) TestEnvelopesV3UpdateFails() { + tests := []struct { + name string + id string + body any + status int // expected response status + }{ + {"Invalid type", "", `{"name": 2}`, http.StatusBadRequest}, + {"Broken JSON", "", `{ "name": 2" }`, http.StatusBadRequest}, + {"Non-existing account", uuid.New().String(), `{"name": 2}`, http.StatusNotFound}, + {"Set Category to uuid.Nil", "", models.EnvelopeCreate{}, http.StatusBadRequest}, + } + + for _, tt := range tests { + suite.T().Run(tt.name, func(t *testing.T) { + var recorder httptest.ResponseRecorder + + if tt.id == "" { + envelope := suite.createTestEnvelopeV3(suite.T(), models.EnvelopeCreate{ + Name: "New Envelope", + Note: "Auto-created for test", + }) + + tt.id = envelope.Data.ID.String() + } + + // Update Account + recorder = test.Request(suite.controller, t, http.MethodPatch, fmt.Sprintf("http://example.com/v3/envelopes/%s", tt.id), tt.body) + assertHTTPStatus(t, &recorder, tt.status) + }) + } +} + +// TestEnvelopesV3Delete verifies all cases for Account deletions. +func (suite *TestSuiteStandard) TestEnvelopesV3Delete() { + tests := []struct { + name string + id string + status int // expected response status + }{ + {"Success", "", http.StatusNoContent}, + {"Non-existing Envelope", uuid.New().String(), http.StatusNotFound}, + } + + for _, tt := range tests { + suite.T().Run(tt.name, func(t *testing.T) { + var recorder httptest.ResponseRecorder + + if tt.id == "" { + // Create test Account + e := suite.createTestEnvelopeV3(t, models.EnvelopeCreate{}) + tt.id = e.Data.ID.String() + } + + // Delete Account + recorder = test.Request(suite.controller, t, http.MethodDelete, fmt.Sprintf("http://example.com/v3/envelopes/%s", tt.id), "") + assertHTTPStatus(t, &recorder, tt.status) + }) + } +} + +// TestEnvelopesV3GetSorted verifies that Accounts are sorted by name. +func (suite *TestSuiteStandard) TestEnvelopesV3GetSorted() { + e1 := suite.createTestEnvelopeV3(suite.T(), models.EnvelopeCreate{ + Name: "Alphabetically first", + }) + + e2 := suite.createTestEnvelopeV3(suite.T(), models.EnvelopeCreate{ + Name: "Second in creation, third in list", + }) + + e3 := suite.createTestEnvelopeV3(suite.T(), models.EnvelopeCreate{ + Name: "First is alphabetically second", + }) + + e4 := suite.createTestEnvelopeV3(suite.T(), models.EnvelopeCreate{ + Name: "Zulu is the last one", + }) + + r := test.Request(suite.controller, suite.T(), http.MethodGet, "http://example.com/v3/envelopes", "") + assertHTTPStatus(suite.T(), &r, http.StatusOK) + + var envelopes controllers.EnvelopeListResponseV3 + suite.decodeResponse(&r, &envelopes) + + if !assert.Len(suite.T(), envelopes.Data, 4) { + assert.FailNow(suite.T(), "Envelope list has wrong length") + } + + assert.Equal(suite.T(), e1.Data.Name, envelopes.Data[0].Name) + assert.Equal(suite.T(), e2.Data.Name, envelopes.Data[2].Name) + assert.Equal(suite.T(), e3.Data.Name, envelopes.Data[1].Name) + assert.Equal(suite.T(), e4.Data.Name, envelopes.Data[3].Name) +} + +func (suite *TestSuiteStandard) TestEnvelopesV3Pagination() { + for i := 0; i < 10; i++ { + suite.createTestEnvelopeV3(suite.T(), models.EnvelopeCreate{Name: fmt.Sprint(i)}) + } + + tests := []struct { + name string + offset uint + limit int + expectedCount int + expectedTotal int64 + }{ + {"All", 0, -1, 10, 10}, + {"First 5", 0, 5, 5, 10}, + {"Last 5", 5, -1, 5, 10}, + {"Offset 3", 3, -1, 7, 10}, + } + + for _, tt := range tests { + suite.T().Run(tt.name, func(t *testing.T) { + r := test.Request(suite.controller, suite.T(), http.MethodGet, fmt.Sprintf("http://example.com/v3/envelopes?offset=%d&limit=%d", tt.offset, tt.limit), "") + assertHTTPStatus(suite.T(), &r, http.StatusOK) + + var envelopes controllers.EnvelopeListResponseV3 + suite.decodeResponse(&r, &envelopes) + + assert.Equal(suite.T(), tt.offset, envelopes.Pagination.Offset) + assert.Equal(suite.T(), tt.limit, envelopes.Pagination.Limit) + assert.Equal(suite.T(), tt.expectedCount, envelopes.Pagination.Count) + assert.Equal(suite.T(), tt.expectedTotal, envelopes.Pagination.Total) + }) + } +} diff --git a/pkg/controllers/import_v3_test.go b/pkg/controllers/import_v3_test.go index d7c7af4c..db3ca0c2 100644 --- a/pkg/controllers/import_v3_test.go +++ b/pkg/controllers/import_v3_test.go @@ -192,7 +192,7 @@ func (suite *TestSuiteStandard) TestImportYnabImportPreviewV3FindAccounts() { internalAccount := suite.createTestAccountV3(suite.T(), models.AccountCreate{BudgetID: budget.Data.ID, Name: "Envelope Zero Account"}) // Test envelope and test transaction to the Edeka account with an envelope to test the envelope prefill - envelope := suite.createTestEnvelope(models.EnvelopeCreate{CategoryID: suite.createTestCategory(models.CategoryCreate{BudgetID: budget.Data.ID}).Data.ID}) + envelope := suite.createTestEnvelopeV3(suite.T(), models.EnvelopeCreate{CategoryID: suite.createTestCategory(models.CategoryCreate{BudgetID: budget.Data.ID}).Data.ID}) envelopeID := envelope.Data.ID _ = suite.createTestTransactionV3(suite.T(), models.TransactionCreate{BudgetID: budget.Data.ID, SourceAccountID: internalAccount.Data.ID, DestinationAccountID: edeka.Data.ID, EnvelopeID: &envelopeID, Amount: decimal.NewFromFloat(12.00)}) @@ -249,7 +249,7 @@ func (suite *TestSuiteStandard) TestImportYnabImportPreviewV3Match() { internalAccount := suite.createTestAccountV3(suite.T(), models.AccountCreate{BudgetID: budget.Data.ID, Name: "Envelope Zero Account"}) // Test envelope and test transaction to the Edeka account with an envelope to test the envelope prefill - envelope := suite.createTestEnvelope(models.EnvelopeCreate{CategoryID: suite.createTestCategory(models.CategoryCreate{BudgetID: budget.Data.ID}).Data.ID}) + envelope := suite.createTestEnvelopeV3(suite.T(), models.EnvelopeCreate{CategoryID: suite.createTestCategory(models.CategoryCreate{BudgetID: budget.Data.ID}).Data.ID}) envelopeID := envelope.Data.ID _ = suite.createTestTransactionV3(suite.T(), models.TransactionCreate{BudgetID: budget.Data.ID, SourceAccountID: internalAccount.Data.ID, DestinationAccountID: edeka.Data.ID, EnvelopeID: &envelopeID, Amount: decimal.NewFromFloat(12.00)}) diff --git a/pkg/controllers/test_options_test.go b/pkg/controllers/test_options_test.go index 3f8b348b..7ba4463a 100644 --- a/pkg/controllers/test_options_test.go +++ b/pkg/controllers/test_options_test.go @@ -14,26 +14,27 @@ func (suite *TestSuiteStandard) TestOptionsHeaderResources() { response string }{ {"http://example.com/healthz", "OPTIONS, GET"}, - {"http://example.com/v1/budgets", "OPTIONS, GET, POST"}, {"http://example.com/v1/accounts", "OPTIONS, GET, POST"}, + {"http://example.com/v1/allocations", "OPTIONS, GET, POST"}, + {"http://example.com/v1/budgets", "OPTIONS, GET, POST"}, {"http://example.com/v1/categories", "OPTIONS, GET, POST"}, {"http://example.com/v1/envelopes", "OPTIONS, GET, POST"}, - {"http://example.com/v1/allocations", "OPTIONS, GET, POST"}, - {"http://example.com/v1/transactions", "OPTIONS, GET, POST"}, - {"http://example.com/v1/month-configs", "OPTIONS, GET"}, {"http://example.com/v1/import", "OPTIONS, POST"}, - {"http://example.com/v1/import/ynab4", "OPTIONS, POST"}, {"http://example.com/v1/import/ynab-import-preview", "OPTIONS, POST"}, - {"http://example.com/v2/transactions", "OPTIONS, POST"}, - {"http://example.com/v2/match-rules", "OPTIONS, GET, POST"}, + {"http://example.com/v1/import/ynab4", "OPTIONS, POST"}, + {"http://example.com/v1/month-configs", "OPTIONS, GET"}, + {"http://example.com/v1/transactions", "OPTIONS, GET, POST"}, {"http://example.com/v2/accounts", "OPTIONS, GET"}, + {"http://example.com/v2/match-rules", "OPTIONS, GET, POST"}, + {"http://example.com/v2/transactions", "OPTIONS, POST"}, {"http://example.com/v3/accounts", "OPTIONS, GET, POST"}, {"http://example.com/v3/budgets", "OPTIONS, GET, POST"}, - {"http://example.com/v3/transactions", "OPTIONS, GET, POST"}, - {"http://example.com/v3/match-rules", "OPTIONS, GET, POST"}, + {"http://example.com/v3/envelopes", "OPTIONS, GET, POST"}, {"http://example.com/v3/import", "OPTIONS, GET"}, {"http://example.com/v3/import/ynab-import-preview", "OPTIONS, POST"}, {"http://example.com/v3/import/ynab4", "OPTIONS, POST"}, + {"http://example.com/v3/match-rules", "OPTIONS, GET, POST"}, + {"http://example.com/v3/transactions", "OPTIONS, GET, POST"}, } for _, tt := range optionsHeaderTests { diff --git a/pkg/controllers/transaction_v3_test.go b/pkg/controllers/transaction_v3_test.go index 3f94cc9c..0440468d 100644 --- a/pkg/controllers/transaction_v3_test.go +++ b/pkg/controllers/transaction_v3_test.go @@ -162,8 +162,8 @@ func (suite *TestSuiteStandard) TestTransactionsV3GetFilter() { c := suite.createTestCategory(models.CategoryCreate{BudgetID: b.Data.ID}) - e1 := suite.createTestEnvelope(models.EnvelopeCreate{CategoryID: c.Data.ID}) - e2 := suite.createTestEnvelope(models.EnvelopeCreate{CategoryID: c.Data.ID}) + e1 := suite.createTestEnvelopeV3(suite.T(), models.EnvelopeCreate{CategoryID: c.Data.ID}) + e2 := suite.createTestEnvelopeV3(suite.T(), models.EnvelopeCreate{CategoryID: c.Data.ID}) e1ID := &e1.Data.ID e2ID := &e2.Data.ID @@ -533,7 +533,7 @@ func (suite *TestSuiteStandard) TestUpdateNonExistingTransactionV3() { // TestTransactionsV3Update verifies that transaction updates are successful. func (suite *TestSuiteStandard) TestTransactionsV3Update() { - envelope := suite.createTestEnvelope(models.EnvelopeCreate{}) + envelope := suite.createTestEnvelopeV3(suite.T(), models.EnvelopeCreate{}) transaction := suite.createTestTransactionV3(suite.T(), models.TransactionCreate{ Amount: decimal.NewFromFloat(23.14), Note: "Test note for transaction", diff --git a/pkg/router/router.go b/pkg/router/router.go index 016ebdd1..67c8e97f 100644 --- a/pkg/router/router.go +++ b/pkg/router/router.go @@ -161,9 +161,10 @@ func AttachRoutes(co controllers.Controller, group *gin.RouterGroup) { co.RegisterAccountRoutesV3(v3.Group("/accounts")) co.RegisterBudgetRoutesV3(v3.Group("/budgets")) - co.RegisterTransactionRoutesV3(v3.Group("/transactions")) - co.RegisterMatchRuleRoutesV3(v3.Group("/match-rules")) + co.RegisterEnvelopeRoutesV3(v3.Group("/envelopes")) co.RegisterImportRoutesV3(v3.Group("/import")) + co.RegisterMatchRuleRoutesV3(v3.Group("/match-rules")) + co.RegisterTransactionRoutesV3(v3.Group("/transactions")) } type RootResponse struct { @@ -342,9 +343,10 @@ type V3Response struct { type V3Links struct { Accounts string `json:"accounts" example:"https://example.com/api/v3/accounts"` // URL of Account collection endpoint Budgets string `json:"budgets" example:"https://example.com/api/v3/budgets"` // URL of Budget collection endpoint - Transactions string `json:"transactions" example:"https://example.com/api/v3/transactions"` // URL of Transaction collection endpoint - MatchRules string `json:"matchRules" example:"https://example.com/api/v3/match-rules"` // URL of Match Rule collection endpoint + Envelopes string `json:"envelopes" example:"https://example.com/api/v3/envelopes"` // URL of Envelope collection endpoint Import string `json:"import" example:"https://example.com/api/v3/import"` // URL of import list endpoint + MatchRules string `json:"matchRules" example:"https://example.com/api/v3/match-rules"` // URL of Match Rule collection endpoint + Transactions string `json:"transactions" example:"https://example.com/api/v3/transactions"` // URL of Transaction collection endpoint } // GetV3 returns the link list for v3 @@ -361,9 +363,10 @@ func GetV3(c *gin.Context) { Links: V3Links{ Accounts: url + "/v3/accounts", Budgets: url + "/v3/budgets", - Transactions: url + "/v3/transactions", - MatchRules: url + "/v3/match-rules", + Envelopes: url + "/v3/envelopes", Import: url + "/v3/import", + MatchRules: url + "/v3/match-rules", + Transactions: url + "/v3/transactions", }, }) } diff --git a/pkg/router/router_test.go b/pkg/router/router_test.go index efb89b28..5c54f305 100644 --- a/pkg/router/router_test.go +++ b/pkg/router/router_test.go @@ -191,9 +191,10 @@ func TestGetV3(t *testing.T) { Links: router.V3Links{ Accounts: "/v3/accounts", Budgets: "/v3/budgets", - Transactions: "/v3/transactions", - MatchRules: "/v3/match-rules", + Envelopes: "/v3/envelopes", Import: "/v3/import", + MatchRules: "/v3/match-rules", + Transactions: "/v3/transactions", }, }