diff --git a/controllers/pkg/database/cockroach/accountv2.go b/controllers/pkg/database/cockroach/accountv2.go index dd5f98eca8e..a124ab56970 100644 --- a/controllers/pkg/database/cockroach/accountv2.go +++ b/controllers/pkg/database/cockroach/accountv2.go @@ -1054,3 +1054,48 @@ func (c *Cockroach) Close() error { } return db.Close() } + +func (c *Cockroach) GetGiftCodeWithCode(code string) (*types.GiftCode, error) { + var giftCode types.GiftCode + if err := c.DB.Where(&types.GiftCode{Code: code}).First(&giftCode).Error; err != nil { + return nil, fmt.Errorf("failed to get gift code: %w", err) + } + return &giftCode, nil +} + +func (c *Cockroach) UseGiftCode(giftCode *types.GiftCode, userID string) error { + return c.DB.Transaction(func(tx *gorm.DB) error { + ops := &types.UserQueryOpts{ID: userID} + // Update the user's balance + if err := c.updateBalance(tx, ops, giftCode.CreditAmount, false, true); err != nil { + return fmt.Errorf("failed to update user balance: %w", err) + } + + message := "created by use gift code" + // Create an AccountTransaction record + accountTransaction := &types.AccountTransaction{ + ID: uuid.New(), + Type: "GiftCode", + UserUID: ops.UID, + DeductionBalance: 0, + Balance: giftCode.CreditAmount, + Message: &message, + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + BillingID: giftCode.ID, + } + if err := tx.Create(accountTransaction).Error; err != nil { + return fmt.Errorf("failed to create account transaction: %w", err) + } + + // Mark the gift code as used + giftCode.Used = true + giftCode.UsedBy = ops.UID + giftCode.UsedAt = time.Now() + if err := tx.Save(giftCode).Error; err != nil { + return fmt.Errorf("failed to update gift code: %w", err) + } + + return nil + }) +} diff --git a/controllers/pkg/types/global.go b/controllers/pkg/types/global.go index 69fd3232fc9..8e25623f51c 100644 --- a/controllers/pkg/types/global.go +++ b/controllers/pkg/types/global.go @@ -280,3 +280,35 @@ func (Invoice) TableName() string { func (InvoicePayment) TableName() string { return "InvoicePayment" } + +type GiftCode struct { + ID uuid.UUID `gorm:"column:id;type:uuid;default:gen_random_uuid();primary_key"` + Code string `gorm:"column:code;type:text;not null;unique"` + CreditAmount int64 `gorm:"column:creditAmount;type:bigint;default:0;not null"` + Used bool `gorm:"column:used;type:boolean;default:false;not null"` + UsedBy uuid.UUID `gorm:"column:usedBy;type:uuid"` + UsedAt time.Time `gorm:"column:usedAt;type:timestamp(3) with time zone"` + CreatedAt time.Time `gorm:"column:createdAt;type:timestamp(3) with time zone;default:current_timestamp()"` + ExpiredAt time.Time `gorm:"column:expiredAt;type:timestamp(3) with time zone"` + Comment string `gorm:"column:comment;type:text"` +} + +func (GiftCode) TableName() string { + return "GiftCode" +} + +type AccountTransaction struct { + ID uuid.UUID `gorm:"column:id;type:uuid;default:gen_random_uuid();primary_key"` + Type string `gorm:"column:type;type:text"` + UserUID uuid.UUID `gorm:"column:userUid;type:uuid"` + DeductionBalance int64 `gorm:"column:deduction_balance;type:bigint"` + Balance int64 `gorm:"column:balance;type:bigint"` + Message *string `gorm:"column:message;type:text"` + CreatedAt time.Time `gorm:"column:created_at;type:timestamp(3) with time zone;default:current_timestamp()"` + UpdatedAt time.Time `gorm:"column:updated_at;type:timestamp(3) with time zone;default:current_timestamp()"` + BillingID uuid.UUID `gorm:"column:billing_id;type:uuid"` +} + +func (AccountTransaction) TableName() string { + return "AccountTransaction" +} diff --git a/service/account/api/api.go b/service/account/api/api.go index 4eda1cd1858..1b334688584 100644 --- a/service/account/api/api.go +++ b/service/account/api/api.go @@ -874,3 +874,44 @@ func GetInvoicePayment(c *gin.Context) { "data": payments, }) } + +// UseGiftCode +// @Summary Use a gift code +// @Description Redeem a gift code and apply the credit to the user's account +// @Tags UseGiftCode +// @Accept json +// @Produce json +// @Param request body helper.UseGiftCodeReq true "Use gift code request" +// @Success 200 {object} helper.UseGiftCodeResp "Successfully redeemed gift code" +// @Failure 400 {object} helper.ErrorMessage "Failed to parse use gift code request" +// @Failure 401 {object} helper.ErrorMessage "Authentication error" +// @Failure 500 {object} helper.ErrorMessage "Failed to redeem gift code" +// @Router /account/v1alpha1/gift-code/use [post] +func UseGiftCode(c *gin.Context) { + // Parse the use gift code request + req, err := helper.ParseUseGiftCodeReq(c) + if err != nil { + c.JSON(http.StatusBadRequest, helper.ErrorMessage{Error: fmt.Sprintf("failed to parse use gift code request: %v", err)}) + return + } + + // Check authentication + if err := CheckAuthAndCalibrate(req.Auth); err != nil { + c.JSON(http.StatusUnauthorized, helper.ErrorMessage{Error: fmt.Sprintf("authenticate error: %v", err)}) + return + } + + // Use the gift code (to be implemented) + if _, err := dao.DBClient.UseGiftCode(req); err != nil { + c.JSON(http.StatusInternalServerError, helper.ErrorMessage{Error: fmt.Sprintf("failed to use gift code: %v", err)}) + return + } + + // Return success response + c.JSON(http.StatusOK, helper.UseGiftCodeResp{ + Data: helper.UseGiftCodeRespData{ + UserID: req.UserID, + }, + Message: "Gift code successfully redeemed", + }) +} diff --git a/service/account/dao/interface.go b/service/account/dao/interface.go index 57341a5f08a..3b325138e07 100644 --- a/service/account/dao/interface.go +++ b/service/account/dao/interface.go @@ -55,6 +55,7 @@ type Interface interface { GetUserCrName(ops types.UserQueryOpts) (string, error) GetRegions() ([]types.Region, error) GetLocalRegion() types.Region + UseGiftCode(req *helper.UseGiftCodeReq) (*types.GiftCode, error) } type Account struct { @@ -1358,3 +1359,24 @@ func (m *Account) GetInvoicePayments(invoiceID string) ([]types.Payment, error) func (m *Account) SetStatusInvoice(req *helper.SetInvoiceStatusReq) error { return m.ck.SetInvoiceStatus(req.InvoiceIDList, req.Status) } + +func (m *Account) UseGiftCode(req *helper.UseGiftCodeReq) (*types.GiftCode, error) { + giftCode, err := m.ck.GetGiftCodeWithCode(req.Code) + if err != nil { + return nil, fmt.Errorf("failed to get gift code: %v", err) + } + + if !giftCode.ExpiredAt.IsZero() && time.Now().After(giftCode.ExpiredAt) { + return nil, fmt.Errorf("gift code has expired") + } + + if giftCode.Used { + return nil, fmt.Errorf("gift code is already used") + } + + if err = m.ck.UseGiftCode(giftCode, req.UserID); err != nil { + return nil, fmt.Errorf("failed to use gift code: %v", err) + } + + return giftCode, nil +} diff --git a/service/account/dao/interface_test.go b/service/account/dao/interface_test.go index 22b0c6d3aea..078c723eb4f 100644 --- a/service/account/dao/interface_test.go +++ b/service/account/dao/interface_test.go @@ -580,6 +580,27 @@ func TestAccount_SetStatusInvoice(t *testing.T) { } } +func TestAccount_UseGiftCode(t *testing.T) { + db, err := newAccountForTest("", os.Getenv("GLOBAL_COCKROACH_URI"), os.Getenv("LOCAL_COCKROACH_URI")) + if err != nil { + t.Fatalf("NewAccountInterface() error = %v", err) + return + } + + giftcode, err := db.UseGiftCode(&helper.UseGiftCodeReq{ + Code: "DfxAffaeEf", + Auth: &helper.Auth{ + UserID: "E1xAJ0fy4k", + }, + }) + + if err != nil { + t.Fatalf("UseGiftCode() error = %v", err) + return + } + t.Logf("giftcode = %+v", giftcode) +} + func init() { // set env os.Setenv("MONGO_URI", "") diff --git a/service/account/docs/docs.go b/service/account/docs/docs.go index 3d03f3c9d21..ba8f21858dd 100644 --- a/service/account/docs/docs.go +++ b/service/account/docs/docs.go @@ -761,6 +761,58 @@ const docTemplate = `{ } } }, + "/account/v1alpha1/gift-code/use": { + "post": { + "description": "Redeem a gift code and apply the credit to the user's account", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "UseGiftCode" + ], + "summary": "Use a gift code", + "parameters": [ + { + "description": "Use gift code request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/helper.UseGiftCodeReq" + } + } + ], + "responses": { + "200": { + "description": "Successfully redeemed gift code", + "schema": { + "$ref": "#/definitions/helper.UseGiftCodeResp" + } + }, + "400": { + "description": "Failed to parse use gift code request", + "schema": { + "$ref": "#/definitions/helper.ErrorMessage" + } + }, + "401": { + "description": "Authentication error", + "schema": { + "$ref": "#/definitions/helper.ErrorMessage" + } + }, + "500": { + "description": "Failed to redeem gift code", + "schema": { + "$ref": "#/definitions/helper.ErrorMessage" + } + } + } + } + }, "/account/v1alpha1/invoice/apply": { "post": { "description": "Apply invoice", @@ -1858,6 +1910,55 @@ const docTemplate = `{ } } }, + "helper.UseGiftCodeReq": { + "type": "object", + "required": [ + "code" + ], + "properties": { + "code": { + "description": "@Summary Gift code to be used\n@Description The code of the gift card to be redeemed\n@JSONSchema required", + "type": "string", + "example": "HAPPY2024" + }, + "kubeConfig": { + "type": "string" + }, + "owner": { + "type": "string", + "example": "admin" + }, + "token": { + "type": "string", + "example": "token" + }, + "userID": { + "type": "string", + "example": "admin" + } + } + }, + "helper.UseGiftCodeResp": { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/helper.UseGiftCodeRespData" + }, + "message": { + "type": "string", + "example": "Gift code successfully redeemed" + } + } + }, + "helper.UseGiftCodeRespData": { + "type": "object", + "properties": { + "userID": { + "type": "string", + "example": "user-123" + } + } + }, "helper.UserBaseReq": { "type": "object", "properties": { diff --git a/service/account/docs/swagger.json b/service/account/docs/swagger.json index 43ac3efc1c5..1345e163309 100644 --- a/service/account/docs/swagger.json +++ b/service/account/docs/swagger.json @@ -754,6 +754,58 @@ } } }, + "/account/v1alpha1/gift-code/use": { + "post": { + "description": "Redeem a gift code and apply the credit to the user's account", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "UseGiftCode" + ], + "summary": "Use a gift code", + "parameters": [ + { + "description": "Use gift code request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/helper.UseGiftCodeReq" + } + } + ], + "responses": { + "200": { + "description": "Successfully redeemed gift code", + "schema": { + "$ref": "#/definitions/helper.UseGiftCodeResp" + } + }, + "400": { + "description": "Failed to parse use gift code request", + "schema": { + "$ref": "#/definitions/helper.ErrorMessage" + } + }, + "401": { + "description": "Authentication error", + "schema": { + "$ref": "#/definitions/helper.ErrorMessage" + } + }, + "500": { + "description": "Failed to redeem gift code", + "schema": { + "$ref": "#/definitions/helper.ErrorMessage" + } + } + } + } + }, "/account/v1alpha1/invoice/apply": { "post": { "description": "Apply invoice", @@ -1851,6 +1903,55 @@ } } }, + "helper.UseGiftCodeReq": { + "type": "object", + "required": [ + "code" + ], + "properties": { + "code": { + "description": "@Summary Gift code to be used\n@Description The code of the gift card to be redeemed\n@JSONSchema required", + "type": "string", + "example": "HAPPY2024" + }, + "kubeConfig": { + "type": "string" + }, + "owner": { + "type": "string", + "example": "admin" + }, + "token": { + "type": "string", + "example": "token" + }, + "userID": { + "type": "string", + "example": "admin" + } + } + }, + "helper.UseGiftCodeResp": { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/helper.UseGiftCodeRespData" + }, + "message": { + "type": "string", + "example": "Gift code successfully redeemed" + } + } + }, + "helper.UseGiftCodeRespData": { + "type": "object", + "properties": { + "userID": { + "type": "string", + "example": "user-123" + } + } + }, "helper.UserBaseReq": { "type": "object", "properties": { diff --git a/service/account/docs/swagger.yaml b/service/account/docs/swagger.yaml index 4e9e0ffadc8..9337a3ac540 100644 --- a/service/account/docs/swagger.yaml +++ b/service/account/docs/swagger.yaml @@ -561,6 +561,43 @@ definitions: required: - toUser type: object + helper.UseGiftCodeReq: + properties: + code: + description: |- + @Summary Gift code to be used + @Description The code of the gift card to be redeemed + @JSONSchema required + example: HAPPY2024 + type: string + kubeConfig: + type: string + owner: + example: admin + type: string + token: + example: token + type: string + userID: + example: admin + type: string + required: + - code + type: object + helper.UseGiftCodeResp: + properties: + data: + $ref: '#/definitions/helper.UseGiftCodeRespData' + message: + example: Gift code successfully redeemed + type: string + type: object + helper.UseGiftCodeRespData: + properties: + userID: + example: user-123 + type: string + type: object helper.UserBaseReq: properties: endTime: @@ -1093,6 +1130,40 @@ paths: summary: Get transfer tags: - Transfer + /account/v1alpha1/gift-code/use: + post: + consumes: + - application/json + description: Redeem a gift code and apply the credit to the user's account + parameters: + - description: Use gift code request + in: body + name: request + required: true + schema: + $ref: '#/definitions/helper.UseGiftCodeReq' + produces: + - application/json + responses: + "200": + description: Successfully redeemed gift code + schema: + $ref: '#/definitions/helper.UseGiftCodeResp' + "400": + description: Failed to parse use gift code request + schema: + $ref: '#/definitions/helper.ErrorMessage' + "401": + description: Authentication error + schema: + $ref: '#/definitions/helper.ErrorMessage' + "500": + description: Failed to redeem gift code + schema: + $ref: '#/definitions/helper.ErrorMessage' + summary: Use a gift code + tags: + - UseGiftCode /account/v1alpha1/invoice/apply: post: consumes: diff --git a/service/account/helper/common.go b/service/account/helper/common.go index 90d9a8b946c..06c7ae9f487 100644 --- a/service/account/helper/common.go +++ b/service/account/helper/common.go @@ -25,6 +25,7 @@ const ( ApplyInvoice = "/invoice/apply" SetStatusInvoice = "/invoice/set-status" GetInvoicePayment = "/invoice/get-payment" + UseGiftCode = "/gift-code/use" ) // env diff --git a/service/account/helper/request.go b/service/account/helper/request.go index 8031c7ccfed..e2fe0fb0232 100644 --- a/service/account/helper/request.go +++ b/service/account/helper/request.go @@ -475,3 +475,38 @@ func ParseSetInvoiceStatusReq(c *gin.Context) (*SetInvoiceStatusReq, error) { } return invoiceStatus, nil } + +type UseGiftCodeRespData struct { + UserID string `json:"userID" bson:"userID" example:"user-123"` +} + +type UseGiftCodeResp struct { + Data UseGiftCodeRespData `json:"data,omitempty" bson:"data,omitempty"` + Message string `json:"message,omitempty" bson:"message" example:"Gift code successfully redeemed"` +} + +type UseGiftCodeReq struct { + // @Summary Gift code to be used + // @Description The code of the gift card to be redeemed + // @JSONSchema required + Code string `json:"code" bson:"code" binding:"required" example:"HAPPY2024"` + + // @Summary Authentication information + // @Description Authentication information + // @JSONSchema required + *Auth `json:",inline" bson:",inline"` +} + +func ParseUseGiftCodeReq(c *gin.Context) (*UseGiftCodeReq, error) { + useGiftCode := &UseGiftCodeReq{} + if err := c.ShouldBindJSON(useGiftCode); err != nil { + return nil, fmt.Errorf("bind json error: %v", err) + } + + // Additional validation can be added here if needed + if useGiftCode.Code == "" { + return nil, fmt.Errorf("gift code cannot be empty") + } + + return useGiftCode, nil +} diff --git a/service/account/router/router.go b/service/account/router/router.go index ea1aabf8378..2518012102d 100644 --- a/service/account/router/router.go +++ b/service/account/router/router.go @@ -53,7 +53,8 @@ func RegisterPayRouter() { POST(helper.GetInvoice, api.GetInvoice). POST(helper.ApplyInvoice, api.ApplyInvoice). POST(helper.SetStatusInvoice, api.SetStatusInvoice). - POST(helper.GetInvoicePayment, api.GetInvoicePayment) + POST(helper.GetInvoicePayment, api.GetInvoicePayment). + POST(helper.UseGiftCode, api.UseGiftCode) docs.SwaggerInfo.Host = env.GetEnvWithDefault("SWAGGER_HOST", "localhost:2333") router.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerfiles.Handler))