diff --git a/delivery/http_server/benefactor/address/delete.go b/delivery/http_server/benefactor/address/delete.go new file mode 100644 index 0000000..95111bd --- /dev/null +++ b/delivery/http_server/benefactor/address/delete.go @@ -0,0 +1,46 @@ +package benefactoraddresshandler + +import ( + "net/http" + + param "git.gocasts.ir/ebhomengo/niki/param/benefactor/address" + "git.gocasts.ir/ebhomengo/niki/pkg/claim" + httpmsg "git.gocasts.ir/ebhomengo/niki/pkg/http_msg" + "github.com/labstack/echo/v4" +) + +// DeleteAddress godoc +// @Summary Delete address by benefactor +// @Description This endpoint is used to delete an address by benefactor +// @Tags Address +// @Param id path int true "Address ID" +// @Success 204 +// @Failure 400 {string} "Bad request" +// @Security AuthBearerBenefactor +// @Router /address/{id} [delete] +func (h Handler) DeleteAddress(c echo.Context) error { + var req param.DeleteAddressRequest + + if bErr := echo.PathParamsBinder(c).Uint("id", &req.AddressID).BindError(); bErr != nil { + + return echo.NewHTTPError(http.StatusBadRequest) + } + claims := claim.GetClaimsFromEchoContext(c) + req.BenefactorID = claims.UserID + if fieldErrors, err := h.addressVld.ValidateDeleteRequest(req); err != nil { + msg, code := httpmsg.Error(err) + + return c.JSON(code, echo.Map{ + "message": msg, + "errors": fieldErrors, + }) + } + dErr := h.addressSvc.Delete(c.Request().Context(), req) + if dErr != nil { + msg, code := httpmsg.Error(dErr) + + return echo.NewHTTPError(code, msg) + } + + return c.JSON(http.StatusNoContent, nil) +} diff --git a/delivery/http_server/benefactor/address/route.go b/delivery/http_server/benefactor/address/route.go index ad891f7..fbe9c95 100644 --- a/delivery/http_server/benefactor/address/route.go +++ b/delivery/http_server/benefactor/address/route.go @@ -17,4 +17,6 @@ func (h Handler) SetRoutes(e *echo.Echo) { middleware.BenefactorAuthorization(entity.UserBenefactorRole)) r.GET("/", h.GetAddresses, middleware.Auth(h.authSvc, h.authConfig), middleware.BenefactorAuthorization(entity.UserBenefactorRole)) + r.DELETE("/:id", h.DeleteAddress, middleware.Auth(h.authSvc, h.authConfig), + middleware.BenefactorAuthorization(entity.UserBenefactorRole)) } diff --git a/docs/docs.go b/docs/docs.go index edb1537..548a97b 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -187,6 +187,38 @@ const docTemplate = `{ } } } + }, + "delete": { + "security": [ + { + "AuthBearerBenefactor": [] + } + ], + "description": "This endpoint is used to delete an address by benefactor", + "tags": [ + "Address" + ], + "summary": "Delete address by benefactor", + "parameters": [ + { + "type": "integer", + "description": "Address ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "204": { + "description": "No Content" + }, + "400": { + "description": "Bad request", + "schema": { + "type": "string" + } + } + } } }, "/admin/kindboxreqs": { diff --git a/docs/swagger.json b/docs/swagger.json index 892ef8c..0b76ae4 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -176,6 +176,38 @@ } } } + }, + "delete": { + "security": [ + { + "AuthBearerBenefactor": [] + } + ], + "description": "This endpoint is used to delete an address by benefactor", + "tags": [ + "Address" + ], + "summary": "Delete address by benefactor", + "parameters": [ + { + "type": "integer", + "description": "Address ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "204": { + "description": "No Content" + }, + "400": { + "description": "Bad request", + "schema": { + "type": "string" + } + } + } } }, "/admin/kindboxreqs": { diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 5949748..cf511af 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -553,6 +553,26 @@ paths: tags: - Address /address/{id}: + delete: + description: This endpoint is used to delete an address by benefactor + parameters: + - description: Address ID + in: path + name: id + required: true + type: integer + responses: + "204": + description: No Content + "400": + description: Bad request + schema: + type: string + security: + - AuthBearerBenefactor: [] + summary: Delete address by benefactor + tags: + - Address get: consumes: - application/json diff --git a/param/benefactor/address/delete.go b/param/benefactor/address/delete.go new file mode 100644 index 0000000..0687730 --- /dev/null +++ b/param/benefactor/address/delete.go @@ -0,0 +1,6 @@ +package addressparam + +type DeleteAddressRequest struct { + AddressID uint + BenefactorID uint +} diff --git a/pkg/err_msg/message.go b/pkg/err_msg/message.go index 5e01722..818d54d 100644 --- a/pkg/err_msg/message.go +++ b/pkg/err_msg/message.go @@ -25,4 +25,5 @@ const ( ErrorMsgReferTimeNotFound = "refer time not found" ErrorMsgReferTimeIsNotActive = "refer time is not active" ErrorMsgKindBoxReqDoesntBelongToBenefactor = "kind box req doesnt belong to benefactor" + ErrorMsgCantDeleteAddress = "can't delete address" ) diff --git a/repository/mysql/address/delete.go b/repository/mysql/address/delete.go new file mode 100644 index 0000000..3da9712 --- /dev/null +++ b/repository/mysql/address/delete.go @@ -0,0 +1,20 @@ +package mysqladdress + +import ( + "context" + + errmsg "git.gocasts.ir/ebhomengo/niki/pkg/err_msg" + richerror "git.gocasts.ir/ebhomengo/niki/pkg/rich_error" +) + +func (d *DB) DeleteBenefactorAddress(ctx context.Context, addressID uint, benefactorID uint) error { + const op = "mysqladdress.DeleteBenefactorAddress" + + _, err := d.conn.Conn().ExecContext(ctx, `UPDATE addresses SET deleted_at = CURRENT_TIMESTAMP WHERE id = ? AND benefactor_id = ? AND deleted_at IS NULL`, addressID, benefactorID) + if err != nil { + + return richerror.New(op).WithErr(err).WithKind(richerror.KindUnexpected).WithMessage(errmsg.ErrorMsgCantDeleteAddress) + } + + return nil +} diff --git a/repository/mysql/address/exist_address.go b/repository/mysql/address/exist_address.go new file mode 100644 index 0000000..dc8eebb --- /dev/null +++ b/repository/mysql/address/exist_address.go @@ -0,0 +1,32 @@ +package mysqladdress + +import ( + "context" + "database/sql" + "errors" + + errmsg "git.gocasts.ir/ebhomengo/niki/pkg/err_msg" + richerror "git.gocasts.ir/ebhomengo/niki/pkg/rich_error" +) + +func (d *DB) IsExistAddressByID(ctx context.Context, addressID uint, benefactorID uint) (bool, error) { + const op = "mysqladdress.IsExistAddressByID" + + row := d.conn.Conn().QueryRowContext(ctx, `select * from addresses where id = ? and benefactor_id = ? and deleted_at is null `, addressID, benefactorID) + + _, err := scanAddress(row) + if err != nil { + sErr := sql.ErrNoRows + //TODO-errorsas: second argument to errors.As should not be *error + //nolint + if errors.As(err, &sErr) { + return false, nil + } + + // TODO - log unexpected error for better observability + return false, richerror.New(op).WithErr(err). + WithMessage(errmsg.ErrorMsgCantScanQueryResult).WithKind(richerror.KindUnexpected) + } + + return true, nil +} diff --git a/repository/mysql/address/get.go b/repository/mysql/address/get.go index 9186c25..cd0ff1f 100644 --- a/repository/mysql/address/get.go +++ b/repository/mysql/address/get.go @@ -16,7 +16,7 @@ import ( func (d *DB) GetAddressByID(ctx context.Context, id uint) (*entity.Address, error) { const op = "mysqladdress.IsExistAddressByID" - row := d.conn.Conn().QueryRowContext(ctx, `select * from addresses where id = ?`, id) + row := d.conn.Conn().QueryRowContext(ctx, `select * from addresses where id = ? and deleted_at is null`, id) address, err := scanAddress(row) if err != nil { @@ -38,7 +38,7 @@ func (d *DB) GetAddressByID(ctx context.Context, id uint) (*entity.Address, erro func (d *DB) GetAddress(ctx context.Context, addressID uint, benefactorID uint) (entity.Address, error) { const op = "mysqladdress.GetAddress" - row := d.conn.Conn().QueryRowContext(ctx, `select * from addresses where id = ? and benefactor_id = ?`, addressID, benefactorID) + row := d.conn.Conn().QueryRowContext(ctx, `select * from addresses where id = ? and benefactor_id = ? and deleted_at is null`, addressID, benefactorID) address, err := scanAddress(row) if err != nil { @@ -60,11 +60,12 @@ func (d *DB) GetAddress(ctx context.Context, addressID uint, benefactorID uint) func scanAddress(scanner mysql.Scanner) (entity.Address, error) { var createdAt, updatedAt time.Time + var deletedAt sql.NullTime var address entity.Address err := scanner.Scan(&address.ID, &address.PostalCode, &address.Address, &address.Lat, &address.Lon, &address.Name, &address.CityID, &address.ProvinceID, &address.BenefactorID, - &createdAt, &updatedAt) + &createdAt, &updatedAt, &deletedAt) return address, err } diff --git a/repository/mysql/address/get_all.go b/repository/mysql/address/get_all.go index 3f0eb9d..71cd6b1 100644 --- a/repository/mysql/address/get_all.go +++ b/repository/mysql/address/get_all.go @@ -11,7 +11,7 @@ import ( func (d *DB) GetAddresses(ctx context.Context, benefactorID uint) ([]entity.Address, error) { const op = "mysqladdress.GetAddresses" - rows, err := d.conn.Conn().QueryContext(ctx, `select * from addresses where benefactor_id = ?`, benefactorID) + rows, err := d.conn.Conn().QueryContext(ctx, `select * from addresses where benefactor_id = ? and deleted_at is null`, benefactorID) if err != nil { return nil, richerror.New(op).WithErr(err). WithMessage(errmsg.ErrorMsgSomethingWentWrong).WithKind(richerror.KindUnexpected) diff --git a/repository/mysql/migration/1705399370_create_addresses_table.sql b/repository/mysql/migration/1705399370_create_addresses_table.sql index 8ec48ea..d7c39f1 100644 --- a/repository/mysql/migration/1705399370_create_addresses_table.sql +++ b/repository/mysql/migration/1705399370_create_addresses_table.sql @@ -12,6 +12,7 @@ CREATE TABLE `addresses` ( `created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, `updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + `deleted_at` TIMESTAMP, FOREIGN KEY (`province_id`) REFERENCES `provinces` (`id`), FOREIGN KEY (`city_id`) REFERENCES `cities` (`id`), FOREIGN KEY (`benefactor_id`) REFERENCES `benefactors` (`id`) diff --git a/service/benefactor/address/delete.go b/service/benefactor/address/delete.go new file mode 100644 index 0000000..eacb892 --- /dev/null +++ b/service/benefactor/address/delete.go @@ -0,0 +1,19 @@ +package benefactoraddressservice + +import ( + "context" + + param "git.gocasts.ir/ebhomengo/niki/param/benefactor/address" + richerror "git.gocasts.ir/ebhomengo/niki/pkg/rich_error" +) + +func (s Service) Delete(ctx context.Context, req param.DeleteAddressRequest) error { + const op = "benefactoraddressservice.Delete" + + err := s.repo.DeleteBenefactorAddress(ctx, req.AddressID, req.BenefactorID) + if err != nil { + return richerror.New(op).WithErr(err) + } + + return nil +} diff --git a/service/benefactor/address/service.go b/service/benefactor/address/service.go index 1061b6f..3117899 100644 --- a/service/benefactor/address/service.go +++ b/service/benefactor/address/service.go @@ -2,7 +2,6 @@ package benefactoraddressservice import ( "context" - "git.gocasts.ir/ebhomengo/niki/entity" ) @@ -13,6 +12,7 @@ type Repository interface { GetAllCities(ctx context.Context) ([]entity.City, error) GetAddress(ctx context.Context, addressID uint, benefactorID uint) (entity.Address, error) GetAddresses(ctx context.Context, benefactorID uint) ([]entity.Address, error) + DeleteBenefactorAddress(ctx context.Context, addressID uint, benefactorID uint) error } type Service struct { diff --git a/validator/benefactor/address/delete.go b/validator/benefactor/address/delete.go new file mode 100644 index 0000000..cd7f3be --- /dev/null +++ b/validator/benefactor/address/delete.go @@ -0,0 +1,42 @@ +package benefactoraddressvalidator + +import ( + "errors" + + param "git.gocasts.ir/ebhomengo/niki/param/benefactor/address" + errmsg "git.gocasts.ir/ebhomengo/niki/pkg/err_msg" + richerror "git.gocasts.ir/ebhomengo/niki/pkg/rich_error" + validation "github.com/go-ozzo/ozzo-validation/v4" +) + +func (v Validator) ValidateDeleteRequest(req param.DeleteAddressRequest) (map[string]string, error) { + const op = "benefactoraddressvalidator.ValidateDeleteRequest" + + if err := validation.ValidateStruct(&req, + validation.Field(&req.BenefactorID, + validation.Required, + validation.By(v.doesBenefactorExist)), + validation.Field(&req.AddressID, + validation.Required, + validation.By(v.doesAddressExist(req.BenefactorID))), + ); err != nil { + fieldErrors := make(map[string]string) + + var errV validation.Errors + if errors.As(err, &errV) { + for key, value := range errV { + if value != nil { + fieldErrors[key] = value.Error() + } + } + } + + return fieldErrors, richerror.New(op). + WithMessage(errmsg.ErrorMsgInvalidInput). + WithKind(richerror.KindInvalid). + WithMeta(map[string]interface{}{"req": req}). + WithErr(err) + } + + return map[string]string{}, nil +} diff --git a/validator/benefactor/address/validator.go b/validator/benefactor/address/validator.go index 520a73f..7c78923 100644 --- a/validator/benefactor/address/validator.go +++ b/validator/benefactor/address/validator.go @@ -6,6 +6,7 @@ import ( param "git.gocasts.ir/ebhomengo/niki/param/benefactor/benefactore" errmsg "git.gocasts.ir/ebhomengo/niki/pkg/err_msg" + validation "github.com/go-ozzo/ozzo-validation/v4" ) type BenefactorSvc interface { @@ -13,6 +14,7 @@ type BenefactorSvc interface { } type Repository interface { IsExistCityByID(ctx context.Context, id uint) (bool, error) + IsExistAddressByID(ctx context.Context, addressID uint, benefactorID uint) (bool, error) } type Validator struct { benefactorSvc BenefactorSvc @@ -51,3 +53,22 @@ func (v Validator) doesCityExist(value interface{}) error { return nil } + +func (v Validator) doesAddressExist(benefactorID uint) validation.RuleFunc { + + return func(value interface{}) error { + addressID, ok := value.(uint) + if !ok { + return fmt.Errorf(errmsg.ErrorMsgSomethingWentWrong) + } + isExisted, err := v.repository.IsExistAddressByID(context.Background(), addressID, benefactorID) + if err != nil { + return fmt.Errorf(errmsg.ErrorMsgNotFound) + } + if !isExisted { + return fmt.Errorf(errmsg.ErrorMsgNotFound) + } + + return nil + } +}