From 8c36ef4b3651475f9b60d53c0b0315ab7eed6f49 Mon Sep 17 00:00:00 2001 From: nikhilnarayanan623 Date: Tue, 10 Oct 2023 18:58:03 +0530 Subject: [PATCH] feature brand full CURD operations added product can add brand --- cmd/api/docs/docs.go | 256 ++++++++++++++++++++++++++++ cmd/api/docs/swagger.json | 256 ++++++++++++++++++++++++++++ cmd/api/docs/swagger.yaml | 165 ++++++++++++++++++ pkg/api/handler/brand.go | 191 +++++++++++++++++++++ pkg/api/handler/interfaces/brand.go | 11 ++ pkg/api/handler/product.go | 20 ++- pkg/api/handler/request/product.go | 5 + pkg/api/handler/response/product.go | 4 + pkg/api/routes/admin.go | 12 +- pkg/api/server.go | 5 +- pkg/di/wire.go | 3 + pkg/di/wire_gen.go | 5 +- pkg/domain/product.go | 7 + pkg/repository/brand.go | 59 +++++++ pkg/repository/interfaces/brand.go | 15 ++ pkg/repository/product.go | 18 +- pkg/usecase/brand.go | 79 +++++++++ pkg/usecase/errors.go | 3 + pkg/usecase/interfaces/brand.go | 14 ++ pkg/usecase/product.go | 1 + 20 files changed, 1115 insertions(+), 14 deletions(-) create mode 100644 pkg/api/handler/brand.go create mode 100644 pkg/api/handler/interfaces/brand.go create mode 100644 pkg/repository/brand.go create mode 100644 pkg/repository/interfaces/brand.go create mode 100644 pkg/usecase/brand.go create mode 100644 pkg/usecase/interfaces/brand.go diff --git a/cmd/api/docs/docs.go b/cmd/api/docs/docs.go index b7611d7..e23dc30 100644 --- a/cmd/api/docs/docs.go +++ b/cmd/api/docs/docs.go @@ -551,6 +551,242 @@ const docTemplate = `{ } } }, + "/admin/brands": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "API for admin to find all brands", + "tags": [ + "Admin Brand" + ], + "summary": "Find All Brand", + "operationId": "FindAllBrands", + "parameters": [ + { + "type": "integer", + "description": "Page number", + "name": "page_number", + "in": "query" + }, + { + "type": "integer", + "description": "Count", + "name": "count", + "in": "query" + } + ], + "responses": { + "200": { + "description": "successfully found all brands", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "204": { + "description": "there is no brands to show", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "500": { + "description": "failed to find brand", + "schema": { + "$ref": "#/definitions/response.Response" + } + } + } + }, + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "API for admin to save new brand", + "tags": [ + "Admin Brand" + ], + "summary": "Save Brand", + "operationId": "SaveBrand", + "parameters": [ + { + "description": "Input Field", + "name": "inputs", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.Brand" + } + } + ], + "responses": { + "200": { + "description": "successfully brand created", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "400": { + "description": "invalid input", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "409": { + "description": "brand name already exist", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "500": { + "description": "failed to create brand", + "schema": { + "$ref": "#/definitions/response.Response" + } + } + } + } + }, + "/admin/brands/{brand_id}": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "API for admin to find one brand", + "tags": [ + "Admin Brand" + ], + "summary": "Find One Brand", + "operationId": "FindOneBrand", + "parameters": [ + { + "type": "integer", + "description": "Brand ID", + "name": "brand_id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "successfully brand found", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "400": { + "description": "invalid input", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "500": { + "description": "failed to find brand", + "schema": { + "$ref": "#/definitions/response.Response" + } + } + } + }, + "put": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "API for admin to update brand", + "tags": [ + "Admin Brand" + ], + "summary": "Save Brand", + "operationId": "UpdateBrand", + "parameters": [ + { + "type": "integer", + "description": "Brand ID", + "name": "brand_id", + "in": "path", + "required": true + }, + { + "description": "Input Field", + "name": "inputs", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.Brand" + } + } + ], + "responses": { + "200": { + "description": "successfully brand updated", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "400": { + "description": "invalid input", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "500": { + "description": "failed to update brand", + "schema": { + "$ref": "#/definitions/response.Response" + } + } + } + }, + "delete": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "API for admin to delete brand", + "tags": [ + "Admin Brand" + ], + "summary": "Save Brand", + "operationId": "DeleteBrand", + "parameters": [ + { + "type": "integer", + "description": "Brand ID", + "name": "brand_id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "successfully brand deleted", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "400": { + "description": "invalid input", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "500": { + "description": "failed to delete brand", + "schema": { + "$ref": "#/definitions/response.Response" + } + } + } + } + }, "/admin/categories": { "get": { "security": [ @@ -1942,6 +2178,13 @@ const docTemplate = `{ "in": "formData", "required": true }, + { + "type": "integer", + "description": "Brand Id", + "name": "brand_id", + "in": "formData", + "required": true + }, { "type": "integer", "description": "Product Price", @@ -3675,6 +3918,19 @@ const docTemplate = `{ } } }, + "request.Brand": { + "type": "object", + "required": [ + "category_name" + ], + "properties": { + "category_name": { + "type": "string", + "maxLength": 25, + "minLength": 3 + } + } + }, "request.Category": { "type": "object", "required": [ diff --git a/cmd/api/docs/swagger.json b/cmd/api/docs/swagger.json index d0bca59..a78c70b 100644 --- a/cmd/api/docs/swagger.json +++ b/cmd/api/docs/swagger.json @@ -543,6 +543,242 @@ } } }, + "/admin/brands": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "API for admin to find all brands", + "tags": [ + "Admin Brand" + ], + "summary": "Find All Brand", + "operationId": "FindAllBrands", + "parameters": [ + { + "type": "integer", + "description": "Page number", + "name": "page_number", + "in": "query" + }, + { + "type": "integer", + "description": "Count", + "name": "count", + "in": "query" + } + ], + "responses": { + "200": { + "description": "successfully found all brands", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "204": { + "description": "there is no brands to show", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "500": { + "description": "failed to find brand", + "schema": { + "$ref": "#/definitions/response.Response" + } + } + } + }, + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "API for admin to save new brand", + "tags": [ + "Admin Brand" + ], + "summary": "Save Brand", + "operationId": "SaveBrand", + "parameters": [ + { + "description": "Input Field", + "name": "inputs", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.Brand" + } + } + ], + "responses": { + "200": { + "description": "successfully brand created", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "400": { + "description": "invalid input", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "409": { + "description": "brand name already exist", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "500": { + "description": "failed to create brand", + "schema": { + "$ref": "#/definitions/response.Response" + } + } + } + } + }, + "/admin/brands/{brand_id}": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "API for admin to find one brand", + "tags": [ + "Admin Brand" + ], + "summary": "Find One Brand", + "operationId": "FindOneBrand", + "parameters": [ + { + "type": "integer", + "description": "Brand ID", + "name": "brand_id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "successfully brand found", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "400": { + "description": "invalid input", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "500": { + "description": "failed to find brand", + "schema": { + "$ref": "#/definitions/response.Response" + } + } + } + }, + "put": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "API for admin to update brand", + "tags": [ + "Admin Brand" + ], + "summary": "Save Brand", + "operationId": "UpdateBrand", + "parameters": [ + { + "type": "integer", + "description": "Brand ID", + "name": "brand_id", + "in": "path", + "required": true + }, + { + "description": "Input Field", + "name": "inputs", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.Brand" + } + } + ], + "responses": { + "200": { + "description": "successfully brand updated", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "400": { + "description": "invalid input", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "500": { + "description": "failed to update brand", + "schema": { + "$ref": "#/definitions/response.Response" + } + } + } + }, + "delete": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "API for admin to delete brand", + "tags": [ + "Admin Brand" + ], + "summary": "Save Brand", + "operationId": "DeleteBrand", + "parameters": [ + { + "type": "integer", + "description": "Brand ID", + "name": "brand_id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "successfully brand deleted", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "400": { + "description": "invalid input", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "500": { + "description": "failed to delete brand", + "schema": { + "$ref": "#/definitions/response.Response" + } + } + } + } + }, "/admin/categories": { "get": { "security": [ @@ -1934,6 +2170,13 @@ "in": "formData", "required": true }, + { + "type": "integer", + "description": "Brand Id", + "name": "brand_id", + "in": "formData", + "required": true + }, { "type": "integer", "description": "Product Price", @@ -3667,6 +3910,19 @@ } } }, + "request.Brand": { + "type": "object", + "required": [ + "category_name" + ], + "properties": { + "category_name": { + "type": "string", + "maxLength": 25, + "minLength": 3 + } + } + }, "request.Category": { "type": "object", "required": [ diff --git a/cmd/api/docs/swagger.yaml b/cmd/api/docs/swagger.yaml index 813bed8..6b13cff 100644 --- a/cmd/api/docs/swagger.yaml +++ b/cmd/api/docs/swagger.yaml @@ -45,6 +45,15 @@ definitions: required: - user_id type: object + request.Brand: + properties: + category_name: + maxLength: 25 + minLength: 3 + type: string + required: + - category_name + type: object request.Category: properties: category_name: @@ -803,6 +812,157 @@ paths: summary: Login with password (Admin) tags: - Admin Authentication + /admin/brands: + get: + description: API for admin to find all brands + operationId: FindAllBrands + parameters: + - description: Page number + in: query + name: page_number + type: integer + - description: Count + in: query + name: count + type: integer + responses: + "200": + description: successfully found all brands + schema: + $ref: '#/definitions/response.Response' + "204": + description: there is no brands to show + schema: + $ref: '#/definitions/response.Response' + "500": + description: failed to find brand + schema: + $ref: '#/definitions/response.Response' + security: + - BearerAuth: [] + summary: Find All Brand + tags: + - Admin Brand + post: + description: API for admin to save new brand + operationId: SaveBrand + parameters: + - description: Input Field + in: body + name: inputs + required: true + schema: + $ref: '#/definitions/request.Brand' + responses: + "200": + description: successfully brand created + schema: + $ref: '#/definitions/response.Response' + "400": + description: invalid input + schema: + $ref: '#/definitions/response.Response' + "409": + description: brand name already exist + schema: + $ref: '#/definitions/response.Response' + "500": + description: failed to create brand + schema: + $ref: '#/definitions/response.Response' + security: + - BearerAuth: [] + summary: Save Brand + tags: + - Admin Brand + /admin/brands/{brand_id}: + delete: + description: API for admin to delete brand + operationId: DeleteBrand + parameters: + - description: Brand ID + in: path + name: brand_id + required: true + type: integer + responses: + "200": + description: successfully brand deleted + schema: + $ref: '#/definitions/response.Response' + "400": + description: invalid input + schema: + $ref: '#/definitions/response.Response' + "500": + description: failed to delete brand + schema: + $ref: '#/definitions/response.Response' + security: + - BearerAuth: [] + summary: Save Brand + tags: + - Admin Brand + get: + description: API for admin to find one brand + operationId: FindOneBrand + parameters: + - description: Brand ID + in: path + name: brand_id + required: true + type: integer + responses: + "200": + description: successfully brand found + schema: + $ref: '#/definitions/response.Response' + "400": + description: invalid input + schema: + $ref: '#/definitions/response.Response' + "500": + description: failed to find brand + schema: + $ref: '#/definitions/response.Response' + security: + - BearerAuth: [] + summary: Find One Brand + tags: + - Admin Brand + put: + description: API for admin to update brand + operationId: UpdateBrand + parameters: + - description: Brand ID + in: path + name: brand_id + required: true + type: integer + - description: Input Field + in: body + name: inputs + required: true + schema: + $ref: '#/definitions/request.Brand' + responses: + "200": + description: successfully brand updated + schema: + $ref: '#/definitions/response.Response' + "400": + description: invalid input + schema: + $ref: '#/definitions/response.Response' + "500": + description: failed to update brand + schema: + $ref: '#/definitions/response.Response' + security: + - BearerAuth: [] + summary: Save Brand + tags: + - Admin Brand /admin/categories: get: consumes: @@ -1646,6 +1806,11 @@ paths: name: category_id required: true type: integer + - description: Brand Id + in: formData + name: brand_id + required: true + type: integer - description: Product Price in: formData name: price diff --git a/pkg/api/handler/brand.go b/pkg/api/handler/brand.go new file mode 100644 index 0000000..004e598 --- /dev/null +++ b/pkg/api/handler/brand.go @@ -0,0 +1,191 @@ +package handler + +import ( + "net/http" + + "github.com/gin-gonic/gin" + "github.com/nikhilnarayanan623/ecommerce-gin-clean-arch/pkg/api/handler/interfaces" + "github.com/nikhilnarayanan623/ecommerce-gin-clean-arch/pkg/api/handler/request" + "github.com/nikhilnarayanan623/ecommerce-gin-clean-arch/pkg/api/handler/response" + "github.com/nikhilnarayanan623/ecommerce-gin-clean-arch/pkg/domain" + "github.com/nikhilnarayanan623/ecommerce-gin-clean-arch/pkg/usecase" + usecaseInterface "github.com/nikhilnarayanan623/ecommerce-gin-clean-arch/pkg/usecase/interfaces" +) + +type brandHandler struct { + brandUseCase usecaseInterface.BrandUseCase +} + +func NewBrandHandler(brandUseCase usecaseInterface.BrandUseCase) interfaces.BrandHandler { + return &brandHandler{ + brandUseCase: brandUseCase, + } +} + +// @Summary Save Brand +// @Description API for admin to save new brand +// @Security BearerAuth +// @Tags Admin Brand +// @Id SaveBrand +// @Param inputs body request.Brand{} true "Input Field" +// @Router /admin/brands [post] +// @Success 200 {object} response.Response{domain.Brand{}} "successfully brand created" +// @Failure 400 {object} response.Response{} "invalid input" +// @Failure 409 {object} response.Response{} "brand name already exist" +// @Failure 500 {object} response.Response{} "failed to create brand" +func (b *brandHandler) Save(ctx *gin.Context) { + + var body request.Brand + + if err := ctx.ShouldBindJSON(&body); err != nil { + response.ErrorResponse(ctx, http.StatusBadRequest, BindJsonFailMessage, err, body) + return + } + + brand := domain.Brand{ + Name: body.Name, + } + + brand, err := b.brandUseCase.Save(brand) + + if err != nil { + var ( + statusCode = http.StatusInternalServerError + message = "failed to save brand" + ) + if err == usecase.ErrBrandAlreadyExist { + statusCode = http.StatusConflict + message = "brand name already exist different other name" + } + response.ErrorResponse(ctx, statusCode, message, err, nil) + return + } + + response.SuccessResponse(ctx, http.StatusCreated, "successfully brand created", brand) +} + +// @Summary Find One Brand +// @Description API for admin to find one brand +// @Security BearerAuth +// @Tags Admin Brand +// @Id FindOneBrand +// @Param brand_id path int true "Brand ID" +// @Router /admin/brands/{brand_id} [get] +// @Success 200 {object} response.Response{domain.Brand{}} "successfully brand found" +// @Failure 400 {object} response.Response{} "invalid input" +// @Failure 500 {object} response.Response{} "failed to find brand" +func (b *brandHandler) FindOne(ctx *gin.Context) { + + brandID, err := request.GetParamAsUint(ctx, "brand_id") + if err != nil { + response.ErrorResponse(ctx, http.StatusBadRequest, BindParamFailMessage, err, nil) + return + } + + brand, err := b.brandUseCase.FindOne(brandID) + + if err != nil { + response.ErrorResponse(ctx, http.StatusInternalServerError, "failed to find brand", err, nil) + return + } + + response.SuccessResponse(ctx, http.StatusOK, "successfully found brand", brand) +} + +// @Summary Find All Brand +// @Description API for admin to find all brands +// @Security BearerAuth +// @Tags Admin Brand +// @Id FindAllBrands +// @Param page_number query int false "Page number" +// @Param count query int false "Count" +// @Router /admin/brands [get] +// @Success 200 {object} response.Response{[]domain.Brand{}} "successfully found all brands" +// @Success 204 {object} response.Response{[]domain.Brand{}} "there is no brands to show" +// @Failure 500 {object} response.Response{} "failed to find brand" +func (b *brandHandler) FindAll(ctx *gin.Context) { + + pagination := request.GetPagination(ctx) + + brands, err := b.brandUseCase.FindAll(pagination) + + if err != nil { + response.ErrorResponse(ctx, http.StatusInternalServerError, "failed to find all brands", err, nil) + return + } + + if len(brands) == 0 { + response.SuccessResponse(ctx, http.StatusNoContent, "there is no brands available to show") + } + + response.SuccessResponse(ctx, http.StatusOK, "successfully found all brands", brands) +} + +// @Summary Save Brand +// @Description API for admin to update brand +// @Security BearerAuth +// @Tags Admin Brand +// @Id UpdateBrand +// @Param brand_id path int true "Brand ID" +// @Param inputs body request.Brand{} true "Input Field" +// @Router /admin/brands/{brand_id} [put] +// @Success 200 {object} response.Response{domain.Brand{}} "successfully brand updated" +// @Failure 400 {object} response.Response{} "invalid input" +// @Failure 500 {object} response.Response{} "failed to update brand" +func (b *brandHandler) Update(ctx *gin.Context) { + + brandID, err := request.GetParamAsUint(ctx, "brand_id") + if err != nil { + response.ErrorResponse(ctx, http.StatusBadRequest, BindParamFailMessage, err, nil) + return + } + + var body request.Brand + + if err := ctx.ShouldBindJSON(&body); err != nil { + response.ErrorResponse(ctx, http.StatusBadRequest, BindJsonFailMessage, err, body) + return + } + + brand := domain.Brand{ + ID: brandID, + Name: body.Name, + } + + err = b.brandUseCase.Update(brand) + + if err != nil { + response.ErrorResponse(ctx, http.StatusInternalServerError, "failed to update brand", err, nil) + return + } + + response.SuccessResponse(ctx, http.StatusOK, "successfully updated brand") +} + +// @Summary Save Brand +// @Description API for admin to delete brand +// @Security BearerAuth +// @Tags Admin Brand +// @Id DeleteBrand +// @Param brand_id path int true "Brand ID" +// @Router /admin/brands/{brand_id} [delete] +// @Success 200 {object} response.Response{domain.Brand{}} "successfully brand deleted" +// @Failure 400 {object} response.Response{} "invalid input" +// @Failure 500 {object} response.Response{} "failed to delete brand" +func (b *brandHandler) Delete(ctx *gin.Context) { + + brandID, err := request.GetParamAsUint(ctx, "brand_id") + if err != nil { + response.ErrorResponse(ctx, http.StatusBadRequest, BindParamFailMessage, err, nil) + return + } + + err = b.brandUseCase.Delete(brandID) + + if err != nil { + response.ErrorResponse(ctx, http.StatusInternalServerError, "failed to deleted brand", err, nil) + return + } + + response.SuccessResponse(ctx, http.StatusOK, "successfully deleted brand") +} diff --git a/pkg/api/handler/interfaces/brand.go b/pkg/api/handler/interfaces/brand.go new file mode 100644 index 0000000..7e96273 --- /dev/null +++ b/pkg/api/handler/interfaces/brand.go @@ -0,0 +1,11 @@ +package interfaces + +import "github.com/gin-gonic/gin" + +type BrandHandler interface { + Save(ctx *gin.Context) + FindOne(ctx *gin.Context) + FindAll(ctx *gin.Context) + Update(ctx *gin.Context) + Delete(ctx *gin.Context) +} diff --git a/pkg/api/handler/product.go b/pkg/api/handler/product.go index 1764adc..c7aae72 100644 --- a/pkg/api/handler/product.go +++ b/pkg/api/handler/product.go @@ -26,6 +26,7 @@ func NewProductHandler(productUsecase usecaseInterface.ProductUseCase) interface } // GetAllCategories godoc +// // @Summary Get all categories (Admin) // @Security BearerAuth // @Description API for admin to get all categories and their subcategories @@ -58,6 +59,7 @@ func (p *ProductHandler) GetAllCategories(ctx *gin.Context) { } // SaveCategory godoc +// // @Summary Add a new category (Admin) // @Security BearerAuth // @Description API for Admin to save new category @@ -97,6 +99,7 @@ func (p *ProductHandler) SaveCategory(ctx *gin.Context) { } // SaveSubCategory godoc +// // @Summary Add a new subcategory (Admin) // @Security BearerAuth // @Description API for admin to add a new sub category for a existing category @@ -135,6 +138,7 @@ func (p *ProductHandler) SaveSubCategory(ctx *gin.Context) { } // SaveVariation godoc +// // @Summary Add new variations (Admin) // @Security BearerAuth // @Description API for admin to add new variations for a category @@ -178,6 +182,7 @@ func (p *ProductHandler) SaveVariation(ctx *gin.Context) { } // SaveVariationOption godoc +// // @Summary Add new variation options (Admin) // @Security BearerAuth // @Description API for admin to add variation options for a variation @@ -220,6 +225,7 @@ func (p *ProductHandler) SaveVariationOption(ctx *gin.Context) { } // GetAllVariations godoc +// // @Summary Get all variations (Admin) // @Security BearerAuth // @Description API for admin to get all variation and its values of a category @@ -255,6 +261,7 @@ func (c *ProductHandler) GetAllVariations(ctx *gin.Context) { } // SaveProduct godoc +// // @Summary Add a new product (Admin) // @Security BearerAuth // @Description API for admin to add a new product @@ -264,6 +271,7 @@ func (c *ProductHandler) GetAllVariations(ctx *gin.Context) { // @Param name formData string true "Product Name" // @Param description formData string true "Product Description" // @Param category_id formData int true "Category Id" +// @Param brand_id formData int true "Brand Id" // @Param price formData int true "Product Price" // @Param image formData file true "Product Description" // @Success 200 {object} response.Response{} "successfully product added" @@ -276,10 +284,11 @@ func (p *ProductHandler) SaveProduct(ctx *gin.Context) { description, err2 := request.GetFormValuesAsString(ctx, "description") categoryID, err3 := request.GetFormValuesAsUint(ctx, "category_id") price, err4 := request.GetFormValuesAsUint(ctx, "price") + brandID, err5 := request.GetFormValuesAsUint(ctx, "brand_id") - fileHeader, err5 := ctx.FormFile("image") + fileHeader, err6 := ctx.FormFile("image") - err := errors.Join(err1, err2, err3, err4, err5) + err := errors.Join(err1, err2, err3, err4, err5, err6) if err != nil { response.ErrorResponse(ctx, http.StatusBadRequest, BindFormValueMessage, err, nil) @@ -290,9 +299,11 @@ func (p *ProductHandler) SaveProduct(ctx *gin.Context) { Name: name, Description: description, CategoryID: categoryID, + BrandID: brandID, Price: price, ImageFileHeader: fileHeader, } + err = p.productUseCase.SaveProduct(ctx, product) if err != nil { @@ -308,6 +319,7 @@ func (p *ProductHandler) SaveProduct(ctx *gin.Context) { } // GetAllProductsAdmin godoc +// // @Summary Get all products (Admin) // @Security BearerAuth // @Description API for admin to get all products @@ -323,6 +335,7 @@ func (p *ProductHandler) GetAllProductsAdmin() func(ctx *gin.Context) { } // GetAllProductsUser godoc +// // @Summary Get all products (User) // @Security BearerAuth // @Description API for user to get all products @@ -362,6 +375,7 @@ func (p *ProductHandler) getAllProducts() func(ctx *gin.Context) { } // UpdateProduct godoc +// // @Summary Update a product (Admin) // @Security BearerAuth // @Description API for admin to update a product @@ -469,6 +483,7 @@ func (p *ProductHandler) SaveProductItem(ctx *gin.Context) { } // GetAllProductItemsAdmin godoc +// // @Summary Get all product items (Admin) // @Security BearerAuth // @Description API for admin to get all product items for a specific product @@ -486,6 +501,7 @@ func (p *ProductHandler) GetAllProductItemsAdmin() func(ctx *gin.Context) { } // GetAllProductItemsUser godoc +// // @Summary Get all product items (User) // @Security BearerAuth // @Description API for user to get all product items for a specific product diff --git a/pkg/api/handler/request/product.go b/pkg/api/handler/request/product.go index 348b8ee..e4ce0ac 100644 --- a/pkg/api/handler/request/product.go +++ b/pkg/api/handler/request/product.go @@ -7,6 +7,7 @@ type Product struct { Name string `json:"product_name" binding:"required,min=3,max=50"` Description string `json:"description" binding:"required,min=10,max=100"` CategoryID uint `json:"category_id" binding:"required"` + BrandID uint `json:"brand_id" binding:"required"` Price uint `json:"price" binding:"required,numeric"` ImageFileHeader *multipart.FileHeader } @@ -44,3 +45,7 @@ type SubCategory struct { CategoryID uint `json:"category_id" binding:"required"` Name string `json:"category_name" binding:"required"` } + +type Brand struct { + Name string `json:"category_name" binding:"required,min=3,max=25"` +} diff --git a/pkg/api/handler/response/product.go b/pkg/api/handler/response/product.go index 3214337..e955d64 100644 --- a/pkg/api/handler/response/product.go +++ b/pkg/api/handler/response/product.go @@ -14,6 +14,8 @@ type Product struct { Description string `json:"description" ` CategoryName string `json:"category_name"` MainCategoryName string `json:"main_category_name"` + BrandID uint `json:"brand_id"` + BrandName string `json:"brand_name"` Image string `json:"image"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` @@ -55,6 +57,8 @@ type ProductItems struct { QtyInStock uint `json:"qty_in_stock"` CategoryName string `json:"category_name"` MainCategoryName string `json:"main_category_name"` + BrandID uint `json:"brand_id"` + BrandName string `json:"brand_name"` VariationValues []ProductVariationValue `json:"variation_values" gorm:"-"` Images []string `json:"images" gorm:"-"` } diff --git a/pkg/api/routes/admin.go b/pkg/api/routes/admin.go index 5764fef..136c02a 100644 --- a/pkg/api/routes/admin.go +++ b/pkg/api/routes/admin.go @@ -10,7 +10,7 @@ func AdminRoutes(api *gin.RouterGroup, authHandler handlerInterface.AuthHandler, adminHandler handlerInterface.AdminHandler, productHandler handlerInterface.ProductHandler, paymentHandler handlerInterface.PaymentHandler, orderHandler handlerInterface.OrderHandler, couponHandler handlerInterface.CouponHandler, offerHandler handlerInterface.OfferHandler, - stockHandler handlerInterface.StockHandler, + stockHandler handlerInterface.StockHandler, branHandler handlerInterface.BrandHandler, ) { @@ -57,6 +57,16 @@ func AdminRoutes(api *gin.RouterGroup, authHandler handlerInterface.AuthHandler, } } + // brand + brand := api.Group("/brands") + { + brand.POST("", branHandler.Save) + brand.GET("", branHandler.FindAll) + brand.GET("/:brand_id", branHandler.FindOne) + brand.PUT("/:brand_id", branHandler.Update) + brand.DELETE("/:brand_id", branHandler.Delete) + } + // product product := api.Group("/products") { diff --git a/pkg/api/server.go b/pkg/api/server.go index bda95b1..95ce5c4 100644 --- a/pkg/api/server.go +++ b/pkg/api/server.go @@ -36,7 +36,8 @@ func NewServerHTTP(authHandler handlerInterface.AuthHandler, middleware middlewa cartHandler handlerInterface.CartHandler, paymentHandler handlerInterface.PaymentHandler, productHandler handlerInterface.ProductHandler, orderHandler handlerInterface.OrderHandler, couponHandler handlerInterface.CouponHandler, offerHandler handlerInterface.OfferHandler, - stockHandler handlerInterface.StockHandler) *ServerHTTP { + stockHandler handlerInterface.StockHandler, branHandler handlerInterface.BrandHandler, +) *ServerHTTP { engine := gin.New() @@ -51,7 +52,7 @@ func NewServerHTTP(authHandler handlerInterface.AuthHandler, middleware middlewa routes.UserRoutes(engine.Group("/api"), authHandler, middleware, userHandler, cartHandler, productHandler, paymentHandler, orderHandler, couponHandler) routes.AdminRoutes(engine.Group("/api/admin"), authHandler, middleware, adminHandler, - productHandler, paymentHandler, orderHandler, couponHandler, offerHandler, stockHandler) + productHandler, paymentHandler, orderHandler, couponHandler, offerHandler, stockHandler, branHandler) // no handler engine.NoRoute(func(ctx *gin.Context) { diff --git a/pkg/di/wire.go b/pkg/di/wire.go index e74e205..768eaf5 100644 --- a/pkg/di/wire.go +++ b/pkg/di/wire.go @@ -38,6 +38,7 @@ func InitializeApi(cfg config.Config) (*http.ServerHTTP, error) { repository.NewCouponRepository, repository.NewOfferRepository, repository.NewStockRepository, + repository.NewBrandDatabaseRepository, //usecase usecase.NewAuthUseCase, @@ -50,6 +51,7 @@ func InitializeApi(cfg config.Config) (*http.ServerHTTP, error) { usecase.NewCouponUseCase, usecase.NewOfferUseCase, usecase.NewStockUseCase, + usecase.NewBrandUseCase, // handler handler.NewAuthHandler, handler.NewAdminHandler, @@ -61,6 +63,7 @@ func InitializeApi(cfg config.Config) (*http.ServerHTTP, error) { handler.NewCouponHandler, handler.NewOfferHandler, handler.NewStockHandler, + handler.NewBrandHandler, http.NewServerHTTP, ) diff --git a/pkg/di/wire_gen.go b/pkg/di/wire_gen.go index e90a90c..50392d3 100644 --- a/pkg/di/wire_gen.go +++ b/pkg/di/wire_gen.go @@ -63,6 +63,9 @@ func InitializeApi(cfg config.Config) (*http.ServerHTTP, error) { stockRepository := repository.NewStockRepository(gormDB) stockUseCase := usecase.NewStockUseCase(stockRepository) stockHandler := handler.NewStockHandler(stockUseCase) - serverHTTP := http.NewServerHTTP(authHandler, middlewareMiddleware, adminHandler, userHandler, cartHandler, paymentHandler, productHandler, orderHandler, couponHandler, offerHandler, stockHandler) + brandRepository := repository.NewBrandDatabaseRepository(gormDB) + brandUseCase := usecase.NewBrandUseCase(brandRepository) + brandHandler := handler.NewBrandHandler(brandUseCase) + serverHTTP := http.NewServerHTTP(authHandler, middlewareMiddleware, adminHandler, userHandler, cartHandler, paymentHandler, productHandler, orderHandler, couponHandler, offerHandler, stockHandler, brandHandler) return serverHTTP, nil } diff --git a/pkg/domain/product.go b/pkg/domain/product.go index 9313455..f00ee16 100644 --- a/pkg/domain/product.go +++ b/pkg/domain/product.go @@ -9,6 +9,8 @@ type Product struct { Description string `json:"description" gorm:"not null" binding:"required,min=10,max=100"` CategoryID uint `json:"category_id" binding:"omitempty,numeric"` Category Category `json:"-"` + BrandID uint `gorm:"not null"` + Brand Brand `json:"-"` Price uint `json:"price" gorm:"not null" binding:"required,numeric"` DiscountPrice uint `json:"discount_price"` Image string `json:"image" gorm:"not null"` @@ -37,6 +39,11 @@ type Category struct { Name string `json:"category_name" gorm:"not null" binding:"required,min=1,max=30"` } +type Brand struct { + ID uint `json:"id" gorm:"primaryKey;not null"` + Name string `json:"brand_name" gorm:"unique;not null"` +} + // variation means size color etc.. type Variation struct { ID uint `json:"-" gorm:"primaryKey;not null"` diff --git a/pkg/repository/brand.go b/pkg/repository/brand.go new file mode 100644 index 0000000..6eaed41 --- /dev/null +++ b/pkg/repository/brand.go @@ -0,0 +1,59 @@ +package repository + +import ( + "github.com/nikhilnarayanan623/ecommerce-gin-clean-arch/pkg/api/handler/request" + "github.com/nikhilnarayanan623/ecommerce-gin-clean-arch/pkg/domain" + "github.com/nikhilnarayanan623/ecommerce-gin-clean-arch/pkg/repository/interfaces" + "gorm.io/gorm" +) + +type brandDatabase struct { + DB *gorm.DB +} + +func NewBrandDatabaseRepository(db *gorm.DB) interfaces.BrandRepository { + return &brandDatabase{ + DB: db, + } +} + +func (c *brandDatabase) IsExist(brand domain.Brand) (bool, error) { + + res := c.DB.Where("name = ?", brand.Name).Find(&brand) + if res.Error != nil { + return false, res.Error + } + + return res.RowsAffected != 0, nil +} + +func (c *brandDatabase) Save(brand domain.Brand) (domain.Brand, error) { + + err := c.DB.Create(&brand).Error + + return brand, err +} + +func (c *brandDatabase) Update(brand domain.Brand) error { + + return c.DB.Where("id = ?", brand.ID).Updates(&brand).Error +} + +func (c *brandDatabase) FindAll(pagination request.Pagination) (brands []domain.Brand, err error) { + + err = c.DB.Limit(int(pagination.Count)).Offset(int(pagination.PageNumber) - 1).Find(&brands).Error + + return +} + +func (c *brandDatabase) FindOne(brandID uint) (brand domain.Brand, err error) { + + err = c.DB.Where("id = ?", brandID).First(&brand).Error + + return +} + +func (c *brandDatabase) Delete(brandID uint) error { + + return c.DB.Where("id = ?", brandID).Delete(&domain.Brand{}).Error +} diff --git a/pkg/repository/interfaces/brand.go b/pkg/repository/interfaces/brand.go new file mode 100644 index 0000000..2c928c7 --- /dev/null +++ b/pkg/repository/interfaces/brand.go @@ -0,0 +1,15 @@ +package interfaces + +import ( + "github.com/nikhilnarayanan623/ecommerce-gin-clean-arch/pkg/api/handler/request" + "github.com/nikhilnarayanan623/ecommerce-gin-clean-arch/pkg/domain" +) + +type BrandRepository interface { + IsExist(brand domain.Brand) (bool, error) + Save(brand domain.Brand) (domain.Brand, error) + Update(brand domain.Brand) error + FindAll(pagination request.Pagination) ([]domain.Brand, error) + FindOne(brandID uint) (domain.Brand, error) + Delete(brandID uint) error +} diff --git a/pkg/repository/product.go b/pkg/repository/product.go index d775d92..58483d1 100644 --- a/pkg/repository/product.go +++ b/pkg/repository/product.go @@ -186,11 +186,11 @@ func (c *productDatabase) IsProductNameExist(ctx context.Context, productName st // to add a new product in database func (c *productDatabase) SaveProduct(ctx context.Context, product domain.Product) error { - query := `INSERT INTO products (name, description, category_id, price, image, created_at) - VALUES($1, $2, $3, $4, $5, $6)` + query := `INSERT INTO products (name, description, category_id, brand_id, price, image, created_at) + VALUES($1, $2, $3, $4, $5, $6, $7)` createdAt := time.Now() - err := c.DB.Exec(query, product.Name, product.Description, product.CategoryID, + err := c.DB.Exec(query, product.Name, product.Description, product.CategoryID, product.BrandID, product.Price, product.Image, createdAt).Error return err @@ -200,13 +200,13 @@ func (c *productDatabase) SaveProduct(ctx context.Context, product domain.Produc func (c *productDatabase) UpdateProduct(ctx context.Context, product domain.Product) error { query := `UPDATE products SET name = $1, description = $2, category_id = $3, - price = $4, image = $5, updated_at = $6 - WHERE id = $7` + price = $4, image = $5, brand_id = $6 updated_at = $7 + WHERE id = $8` updatedAt := time.Now() err := c.DB.Exec(query, product.Name, product.Description, product.CategoryID, - product.Price, product.Image, updatedAt, product.ID).Error + product.Price, product.Image, product.BrandID, updatedAt, product.ID).Error return err } @@ -219,11 +219,12 @@ func (c *productDatabase) FindAllProducts(ctx context.Context, pagination reques query := `SELECT p.id, p.name, p.description, p.price, p.discount_price, p.image, p.image, p.category_id, sc.name AS category_name, - mc.name AS main_category_name, + mc.name AS main_category_name, p.brand_id, b.name AS brand_name, p.created_at, p.updated_at FROM products p INNER JOIN categories sc ON p.category_id = sc.id INNER JOIN categories mc ON sc.category_id = mc.id + INNER JOIN brands b ON b.id = p.brand_id ORDER BY created_at DESC LIMIT $1 OFFSET $2` err = c.DB.Raw(query, limit, offset).Scan(&products).Error @@ -292,11 +293,12 @@ func (c *productDatabase) FindAllProductItems(ctx context.Context, query := `SELECT p.name, pi.id, pi.product_id, pi.price, pi.discount_price, pi.qty_in_stock, pi.sku, p.category_id, sc.name AS category_name, - mc.name AS main_category_name + mc.name AS main_category_name p.brand_id, b.name AS brand_name FROM product_items pi INNER JOIN products p ON p.id = pi.product_id INNER JOIN categories sc ON p.category_id = sc.id INNER JOIN categories mc ON sc.category_id = mc.id + INNER JOIN brands b ON b.id = p.brand_id AND pi.product_id = $1` err = c.DB.Raw(query, productID).Scan(&productItems).Error diff --git a/pkg/usecase/brand.go b/pkg/usecase/brand.go new file mode 100644 index 0000000..00383ff --- /dev/null +++ b/pkg/usecase/brand.go @@ -0,0 +1,79 @@ +package usecase + +import ( + "github.com/nikhilnarayanan623/ecommerce-gin-clean-arch/pkg/api/handler/request" + "github.com/nikhilnarayanan623/ecommerce-gin-clean-arch/pkg/domain" + repoInterface "github.com/nikhilnarayanan623/ecommerce-gin-clean-arch/pkg/repository/interfaces" + "github.com/nikhilnarayanan623/ecommerce-gin-clean-arch/pkg/usecase/interfaces" + "github.com/nikhilnarayanan623/ecommerce-gin-clean-arch/pkg/utils" +) + +type brandUseCase struct { + brandRepo repoInterface.BrandRepository +} + +func NewBrandUseCase(brandRepo repoInterface.BrandRepository) interfaces.BrandUseCase { + return &brandUseCase{ + brandRepo: brandRepo, + } +} + +func (b *brandUseCase) Save(brand domain.Brand) (domain.Brand, error) { + + alreadyExist, err := b.brandRepo.IsExist(brand) + if err != nil { + return domain.Brand{}, utils.PrependMessageToError(err, "failed to check brand name already exist") + } + + if alreadyExist { + return domain.Brand{}, ErrBrandAlreadyExist + } + + brand, err = b.brandRepo.Save(brand) + if err != nil { + return domain.Brand{}, utils.PrependMessageToError(err, "failed to save brand on db") + } + + return brand, nil +} + +func (b *brandUseCase) Update(brand domain.Brand) error { + + err := b.brandRepo.Update(brand) + if err != nil { + return utils.PrependMessageToError(err, "failed to update brand on db") + } + + return nil +} + +func (b *brandUseCase) FindAll(pagination request.Pagination) ([]domain.Brand, error) { + + brands, err := b.brandRepo.FindAll(pagination) + + if err != nil { + return nil, utils.PrependMessageToError(err, "failed to find all brands from db") + } + + return brands, nil +} + +func (b *brandUseCase) FindOne(brandID uint) (domain.Brand, error) { + + brand, err := b.brandRepo.FindOne(brandID) + if err != nil { + return domain.Brand{}, utils.PrependMessageToError(err, "failed to find brand from db") + } + + return brand, nil +} + +func (b *brandUseCase) Delete(brandID uint) error { + + err := b.brandRepo.Delete(brandID) + if err != nil { + return utils.PrependMessageToError(err, "failed to delete brands from db") + } + + return nil +} diff --git a/pkg/usecase/errors.go b/pkg/usecase/errors.go index 5d3dab6..d2c8844 100644 --- a/pkg/usecase/errors.go +++ b/pkg/usecase/errors.go @@ -66,4 +66,7 @@ var ( ErrBlockedPayment = errors.New("selected payment is blocked by admin") ErrPaymentAmountReachedMax = errors.New("order total price reached payment method maximum amount") ErrPaymentNotApproved = errors.New("payment not approved") + + // brand + ErrBrandAlreadyExist = errors.New("brand name already exist") ) diff --git a/pkg/usecase/interfaces/brand.go b/pkg/usecase/interfaces/brand.go new file mode 100644 index 0000000..0592414 --- /dev/null +++ b/pkg/usecase/interfaces/brand.go @@ -0,0 +1,14 @@ +package interfaces + +import ( + "github.com/nikhilnarayanan623/ecommerce-gin-clean-arch/pkg/api/handler/request" + "github.com/nikhilnarayanan623/ecommerce-gin-clean-arch/pkg/domain" +) + +type BrandUseCase interface { + Save(brand domain.Brand) (domain.Brand, error) + Update(brand domain.Brand) error + FindAll(pagination request.Pagination) ([]domain.Brand, error) + FindOne(brandID uint) (domain.Brand, error) + Delete(brandID uint) error +} diff --git a/pkg/usecase/product.go b/pkg/usecase/product.go index 5e5b7a7..4a89664 100644 --- a/pkg/usecase/product.go +++ b/pkg/usecase/product.go @@ -192,6 +192,7 @@ func (c *productUseCase) SaveProduct(ctx context.Context, product request.Produc Name: product.Name, Description: product.Description, CategoryID: product.CategoryID, + BrandID: product.BrandID, Price: product.Price, Image: uploadID, })