diff --git a/_mocks/opencsg.com/csghub-server/builder/store/database/mock_AccountPriceStore.go b/_mocks/opencsg.com/csghub-server/builder/store/database/mock_AccountPriceStore.go new file mode 100644 index 000000000..9f0dd4b45 --- /dev/null +++ b/_mocks/opencsg.com/csghub-server/builder/store/database/mock_AccountPriceStore.go @@ -0,0 +1,447 @@ +// Code generated by mockery v2.53.0. DO NOT EDIT. + +package database + +import ( + context "context" + + mock "github.com/stretchr/testify/mock" + database "opencsg.com/csghub-server/builder/store/database" + + types "opencsg.com/csghub-server/common/types" +) + +// MockAccountPriceStore is an autogenerated mock type for the AccountPriceStore type +type MockAccountPriceStore struct { + mock.Mock +} + +type MockAccountPriceStore_Expecter struct { + mock *mock.Mock +} + +func (_m *MockAccountPriceStore) EXPECT() *MockAccountPriceStore_Expecter { + return &MockAccountPriceStore_Expecter{mock: &_m.Mock} +} + +// Create provides a mock function with given fields: ctx, input +func (_m *MockAccountPriceStore) Create(ctx context.Context, input database.AccountPrice) (*database.AccountPrice, error) { + ret := _m.Called(ctx, input) + + if len(ret) == 0 { + panic("no return value specified for Create") + } + + var r0 *database.AccountPrice + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, database.AccountPrice) (*database.AccountPrice, error)); ok { + return rf(ctx, input) + } + if rf, ok := ret.Get(0).(func(context.Context, database.AccountPrice) *database.AccountPrice); ok { + r0 = rf(ctx, input) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*database.AccountPrice) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, database.AccountPrice) error); ok { + r1 = rf(ctx, input) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MockAccountPriceStore_Create_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Create' +type MockAccountPriceStore_Create_Call struct { + *mock.Call +} + +// Create is a helper method to define mock.On call +// - ctx context.Context +// - input database.AccountPrice +func (_e *MockAccountPriceStore_Expecter) Create(ctx interface{}, input interface{}) *MockAccountPriceStore_Create_Call { + return &MockAccountPriceStore_Create_Call{Call: _e.mock.On("Create", ctx, input)} +} + +func (_c *MockAccountPriceStore_Create_Call) Run(run func(ctx context.Context, input database.AccountPrice)) *MockAccountPriceStore_Create_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(database.AccountPrice)) + }) + return _c +} + +func (_c *MockAccountPriceStore_Create_Call) Return(_a0 *database.AccountPrice, _a1 error) *MockAccountPriceStore_Create_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *MockAccountPriceStore_Create_Call) RunAndReturn(run func(context.Context, database.AccountPrice) (*database.AccountPrice, error)) *MockAccountPriceStore_Create_Call { + _c.Call.Return(run) + return _c +} + +// Delete provides a mock function with given fields: ctx, input +func (_m *MockAccountPriceStore) Delete(ctx context.Context, input database.AccountPrice) error { + ret := _m.Called(ctx, input) + + if len(ret) == 0 { + panic("no return value specified for Delete") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, database.AccountPrice) error); ok { + r0 = rf(ctx, input) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// MockAccountPriceStore_Delete_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Delete' +type MockAccountPriceStore_Delete_Call struct { + *mock.Call +} + +// Delete is a helper method to define mock.On call +// - ctx context.Context +// - input database.AccountPrice +func (_e *MockAccountPriceStore_Expecter) Delete(ctx interface{}, input interface{}) *MockAccountPriceStore_Delete_Call { + return &MockAccountPriceStore_Delete_Call{Call: _e.mock.On("Delete", ctx, input)} +} + +func (_c *MockAccountPriceStore_Delete_Call) Run(run func(ctx context.Context, input database.AccountPrice)) *MockAccountPriceStore_Delete_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(database.AccountPrice)) + }) + return _c +} + +func (_c *MockAccountPriceStore_Delete_Call) Return(_a0 error) *MockAccountPriceStore_Delete_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *MockAccountPriceStore_Delete_Call) RunAndReturn(run func(context.Context, database.AccountPrice) error) *MockAccountPriceStore_Delete_Call { + _c.Call.Return(run) + return _c +} + +// GetByID provides a mock function with given fields: ctx, id +func (_m *MockAccountPriceStore) GetByID(ctx context.Context, id int64) (*database.AccountPrice, error) { + ret := _m.Called(ctx, id) + + if len(ret) == 0 { + panic("no return value specified for GetByID") + } + + var r0 *database.AccountPrice + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, int64) (*database.AccountPrice, error)); ok { + return rf(ctx, id) + } + if rf, ok := ret.Get(0).(func(context.Context, int64) *database.AccountPrice); ok { + r0 = rf(ctx, id) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*database.AccountPrice) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, int64) error); ok { + r1 = rf(ctx, id) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MockAccountPriceStore_GetByID_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetByID' +type MockAccountPriceStore_GetByID_Call struct { + *mock.Call +} + +// GetByID is a helper method to define mock.On call +// - ctx context.Context +// - id int64 +func (_e *MockAccountPriceStore_Expecter) GetByID(ctx interface{}, id interface{}) *MockAccountPriceStore_GetByID_Call { + return &MockAccountPriceStore_GetByID_Call{Call: _e.mock.On("GetByID", ctx, id)} +} + +func (_c *MockAccountPriceStore_GetByID_Call) Run(run func(ctx context.Context, id int64)) *MockAccountPriceStore_GetByID_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(int64)) + }) + return _c +} + +func (_c *MockAccountPriceStore_GetByID_Call) Return(_a0 *database.AccountPrice, _a1 error) *MockAccountPriceStore_GetByID_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *MockAccountPriceStore_GetByID_Call) RunAndReturn(run func(context.Context, int64) (*database.AccountPrice, error)) *MockAccountPriceStore_GetByID_Call { + _c.Call.Return(run) + return _c +} + +// GetLatestByTime provides a mock function with given fields: ctx, req +func (_m *MockAccountPriceStore) GetLatestByTime(ctx context.Context, req types.AcctPriceQueryReq) (*database.AccountPrice, error) { + ret := _m.Called(ctx, req) + + if len(ret) == 0 { + panic("no return value specified for GetLatestByTime") + } + + var r0 *database.AccountPrice + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, types.AcctPriceQueryReq) (*database.AccountPrice, error)); ok { + return rf(ctx, req) + } + if rf, ok := ret.Get(0).(func(context.Context, types.AcctPriceQueryReq) *database.AccountPrice); ok { + r0 = rf(ctx, req) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*database.AccountPrice) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, types.AcctPriceQueryReq) error); ok { + r1 = rf(ctx, req) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MockAccountPriceStore_GetLatestByTime_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetLatestByTime' +type MockAccountPriceStore_GetLatestByTime_Call struct { + *mock.Call +} + +// GetLatestByTime is a helper method to define mock.On call +// - ctx context.Context +// - req types.AcctPriceQueryReq +func (_e *MockAccountPriceStore_Expecter) GetLatestByTime(ctx interface{}, req interface{}) *MockAccountPriceStore_GetLatestByTime_Call { + return &MockAccountPriceStore_GetLatestByTime_Call{Call: _e.mock.On("GetLatestByTime", ctx, req)} +} + +func (_c *MockAccountPriceStore_GetLatestByTime_Call) Run(run func(ctx context.Context, req types.AcctPriceQueryReq)) *MockAccountPriceStore_GetLatestByTime_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(types.AcctPriceQueryReq)) + }) + return _c +} + +func (_c *MockAccountPriceStore_GetLatestByTime_Call) Return(_a0 *database.AccountPrice, _a1 error) *MockAccountPriceStore_GetLatestByTime_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *MockAccountPriceStore_GetLatestByTime_Call) RunAndReturn(run func(context.Context, types.AcctPriceQueryReq) (*database.AccountPrice, error)) *MockAccountPriceStore_GetLatestByTime_Call { + _c.Call.Return(run) + return _c +} + +// ListByIds provides a mock function with given fields: ctx, ids +func (_m *MockAccountPriceStore) ListByIds(ctx context.Context, ids []int64) ([]*types.AcctPriceResp, error) { + ret := _m.Called(ctx, ids) + + if len(ret) == 0 { + panic("no return value specified for ListByIds") + } + + var r0 []*types.AcctPriceResp + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, []int64) ([]*types.AcctPriceResp, error)); ok { + return rf(ctx, ids) + } + if rf, ok := ret.Get(0).(func(context.Context, []int64) []*types.AcctPriceResp); ok { + r0 = rf(ctx, ids) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]*types.AcctPriceResp) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, []int64) error); ok { + r1 = rf(ctx, ids) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MockAccountPriceStore_ListByIds_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ListByIds' +type MockAccountPriceStore_ListByIds_Call struct { + *mock.Call +} + +// ListByIds is a helper method to define mock.On call +// - ctx context.Context +// - ids []int64 +func (_e *MockAccountPriceStore_Expecter) ListByIds(ctx interface{}, ids interface{}) *MockAccountPriceStore_ListByIds_Call { + return &MockAccountPriceStore_ListByIds_Call{Call: _e.mock.On("ListByIds", ctx, ids)} +} + +func (_c *MockAccountPriceStore_ListByIds_Call) Run(run func(ctx context.Context, ids []int64)) *MockAccountPriceStore_ListByIds_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].([]int64)) + }) + return _c +} + +func (_c *MockAccountPriceStore_ListByIds_Call) Return(_a0 []*types.AcctPriceResp, _a1 error) *MockAccountPriceStore_ListByIds_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *MockAccountPriceStore_ListByIds_Call) RunAndReturn(run func(context.Context, []int64) ([]*types.AcctPriceResp, error)) *MockAccountPriceStore_ListByIds_Call { + _c.Call.Return(run) + return _c +} + +// ListBySkuType provides a mock function with given fields: ctx, req +func (_m *MockAccountPriceStore) ListBySkuType(ctx context.Context, req types.AcctPriceListDBReq) ([]database.AccountPrice, int, error) { + ret := _m.Called(ctx, req) + + if len(ret) == 0 { + panic("no return value specified for ListBySkuType") + } + + var r0 []database.AccountPrice + var r1 int + var r2 error + if rf, ok := ret.Get(0).(func(context.Context, types.AcctPriceListDBReq) ([]database.AccountPrice, int, error)); ok { + return rf(ctx, req) + } + if rf, ok := ret.Get(0).(func(context.Context, types.AcctPriceListDBReq) []database.AccountPrice); ok { + r0 = rf(ctx, req) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]database.AccountPrice) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, types.AcctPriceListDBReq) int); ok { + r1 = rf(ctx, req) + } else { + r1 = ret.Get(1).(int) + } + + if rf, ok := ret.Get(2).(func(context.Context, types.AcctPriceListDBReq) error); ok { + r2 = rf(ctx, req) + } else { + r2 = ret.Error(2) + } + + return r0, r1, r2 +} + +// MockAccountPriceStore_ListBySkuType_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ListBySkuType' +type MockAccountPriceStore_ListBySkuType_Call struct { + *mock.Call +} + +// ListBySkuType is a helper method to define mock.On call +// - ctx context.Context +// - req types.AcctPriceListDBReq +func (_e *MockAccountPriceStore_Expecter) ListBySkuType(ctx interface{}, req interface{}) *MockAccountPriceStore_ListBySkuType_Call { + return &MockAccountPriceStore_ListBySkuType_Call{Call: _e.mock.On("ListBySkuType", ctx, req)} +} + +func (_c *MockAccountPriceStore_ListBySkuType_Call) Run(run func(ctx context.Context, req types.AcctPriceListDBReq)) *MockAccountPriceStore_ListBySkuType_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(types.AcctPriceListDBReq)) + }) + return _c +} + +func (_c *MockAccountPriceStore_ListBySkuType_Call) Return(_a0 []database.AccountPrice, _a1 int, _a2 error) *MockAccountPriceStore_ListBySkuType_Call { + _c.Call.Return(_a0, _a1, _a2) + return _c +} + +func (_c *MockAccountPriceStore_ListBySkuType_Call) RunAndReturn(run func(context.Context, types.AcctPriceListDBReq) ([]database.AccountPrice, int, error)) *MockAccountPriceStore_ListBySkuType_Call { + _c.Call.Return(run) + return _c +} + +// Update provides a mock function with given fields: ctx, input +func (_m *MockAccountPriceStore) Update(ctx context.Context, input database.AccountPrice) (*database.AccountPrice, error) { + ret := _m.Called(ctx, input) + + if len(ret) == 0 { + panic("no return value specified for Update") + } + + var r0 *database.AccountPrice + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, database.AccountPrice) (*database.AccountPrice, error)); ok { + return rf(ctx, input) + } + if rf, ok := ret.Get(0).(func(context.Context, database.AccountPrice) *database.AccountPrice); ok { + r0 = rf(ctx, input) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*database.AccountPrice) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, database.AccountPrice) error); ok { + r1 = rf(ctx, input) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MockAccountPriceStore_Update_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Update' +type MockAccountPriceStore_Update_Call struct { + *mock.Call +} + +// Update is a helper method to define mock.On call +// - ctx context.Context +// - input database.AccountPrice +func (_e *MockAccountPriceStore_Expecter) Update(ctx interface{}, input interface{}) *MockAccountPriceStore_Update_Call { + return &MockAccountPriceStore_Update_Call{Call: _e.mock.On("Update", ctx, input)} +} + +func (_c *MockAccountPriceStore_Update_Call) Run(run func(ctx context.Context, input database.AccountPrice)) *MockAccountPriceStore_Update_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(database.AccountPrice)) + }) + return _c +} + +func (_c *MockAccountPriceStore_Update_Call) Return(_a0 *database.AccountPrice, _a1 error) *MockAccountPriceStore_Update_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *MockAccountPriceStore_Update_Call) RunAndReturn(run func(context.Context, database.AccountPrice) (*database.AccountPrice, error)) *MockAccountPriceStore_Update_Call { + _c.Call.Return(run) + return _c +} + +// NewMockAccountPriceStore creates a new instance of MockAccountPriceStore. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewMockAccountPriceStore(t interface { + mock.TestingT + Cleanup(func()) +}) *MockAccountPriceStore { + mock := &MockAccountPriceStore{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/_mocks/opencsg.com/csghub-server/builder/store/database/mock_SpaceResourceStore.go b/_mocks/opencsg.com/csghub-server/builder/store/database/mock_SpaceResourceStore.go index 1f4ea24fe..1d653cc65 100644 --- a/_mocks/opencsg.com/csghub-server/builder/store/database/mock_SpaceResourceStore.go +++ b/_mocks/opencsg.com/csghub-server/builder/store/database/mock_SpaceResourceStore.go @@ -247,6 +247,65 @@ func (_c *MockSpaceResourceStore_FindAllResourceTypes_Call) RunAndReturn(run fun return _c } +// FindByHardwareType provides a mock function with given fields: ctx, hardwareType +func (_m *MockSpaceResourceStore) FindByHardwareType(ctx context.Context, hardwareType string) ([]database.SpaceResource, error) { + ret := _m.Called(ctx, hardwareType) + + if len(ret) == 0 { + panic("no return value specified for FindByHardwareType") + } + + var r0 []database.SpaceResource + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string) ([]database.SpaceResource, error)); ok { + return rf(ctx, hardwareType) + } + if rf, ok := ret.Get(0).(func(context.Context, string) []database.SpaceResource); ok { + r0 = rf(ctx, hardwareType) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]database.SpaceResource) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, string) error); ok { + r1 = rf(ctx, hardwareType) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MockSpaceResourceStore_FindByHardwareType_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'FindByHardwareType' +type MockSpaceResourceStore_FindByHardwareType_Call struct { + *mock.Call +} + +// FindByHardwareType is a helper method to define mock.On call +// - ctx context.Context +// - hardwareType string +func (_e *MockSpaceResourceStore_Expecter) FindByHardwareType(ctx interface{}, hardwareType interface{}) *MockSpaceResourceStore_FindByHardwareType_Call { + return &MockSpaceResourceStore_FindByHardwareType_Call{Call: _e.mock.On("FindByHardwareType", ctx, hardwareType)} +} + +func (_c *MockSpaceResourceStore_FindByHardwareType_Call) Run(run func(ctx context.Context, hardwareType string)) *MockSpaceResourceStore_FindByHardwareType_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(string)) + }) + return _c +} + +func (_c *MockSpaceResourceStore_FindByHardwareType_Call) Return(_a0 []database.SpaceResource, _a1 error) *MockSpaceResourceStore_FindByHardwareType_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *MockSpaceResourceStore_FindByHardwareType_Call) RunAndReturn(run func(context.Context, string) ([]database.SpaceResource, error)) *MockSpaceResourceStore_FindByHardwareType_Call { + _c.Call.Return(run) + return _c +} + // FindByID provides a mock function with given fields: ctx, id func (_m *MockSpaceResourceStore) FindByID(ctx context.Context, id int64) (*database.SpaceResource, error) { ret := _m.Called(ctx, id) diff --git a/builder/store/database/account_price.go b/builder/store/database/account_price.go new file mode 100644 index 000000000..a3474405f --- /dev/null +++ b/builder/store/database/account_price.go @@ -0,0 +1,170 @@ +package database + +import ( + "context" + "fmt" + "strconv" + + "github.com/uptrace/bun" + + "opencsg.com/csghub-server/common/types" +) + +type accountPriceStoreImpl struct { + db *DB +} + +type AccountPriceStore interface { + Create(ctx context.Context, input AccountPrice) (*AccountPrice, error) + Update(ctx context.Context, input AccountPrice) (*AccountPrice, error) + Delete(ctx context.Context, input AccountPrice) error + GetByID(ctx context.Context, id int64) (*AccountPrice, error) + GetLatestByTime(ctx context.Context, req types.AcctPriceQueryReq) (*AccountPrice, error) + ListBySkuType(ctx context.Context, req types.AcctPriceListDBReq) ([]AccountPrice, int, error) + ListByIds(ctx context.Context, ids []int64) ([]*types.AcctPriceResp, error) +} + +func NewAccountPriceStore() AccountPriceStore { + return &accountPriceStoreImpl{ + db: defaultDB, + } +} + +func NewAccountPriceStoreWithDB(db *DB) AccountPriceStore { + return &accountPriceStoreImpl{ + db: db, + } +} + +type AccountPrice struct { + ID int64 `bun:",pk,autoincrement" json:"id"` + SkuType types.SKUType `bun:",notnull" json:"sku_type"` + SkuPrice int64 `bun:",notnull" json:"sku_price"` + SkuUnit int64 `bun:",notnull" json:"sku_unit"` + SkuDesc string `bun:",notnull" json:"sku_desc"` + ResourceID string `bun:",notnull" json:"resource_id"` + SkuUnitType types.SkuUnitType `json:"sku_unit_type"` + SkuPriceCurrency string `json:"sku_price_currency"` + SkuKind types.SKUKind `json:"sku_kind"` + Quota string `json:"quota"` + SkuPriceID int64 `json:"sku_price_id"` + Discount float64 `json:"discount"` // discount rate, e.g. 0.9 means 10% discount + UseLimitPrice int64 `json:"use_limit_price"` + times +} + +type PriceResp struct { + Prices []AccountPrice `json:"data"` + Total int `json:"total"` +} + +func (a *accountPriceStoreImpl) Create(ctx context.Context, input AccountPrice) (*AccountPrice, error) { + res, err := a.db.Core.NewInsert().Model(&input).Exec(ctx, &input) + if err := assertAffectedOneRow(res, err); err != nil { + return nil, fmt.Errorf("failed to insert price, error:%w", err) + } + return &input, nil +} + +func (a *accountPriceStoreImpl) Update(ctx context.Context, input AccountPrice) (*AccountPrice, error) { + _, err := a.db.Core.NewUpdate().Model(&input).WherePK().Exec(ctx) + if err != nil { + return nil, fmt.Errorf("failed to update price, error: %w", err) + } + return &input, nil +} + +func (a *accountPriceStoreImpl) Delete(ctx context.Context, input AccountPrice) error { + _, err := a.db.Core.NewDelete().Model(&input).WherePK().Exec(ctx) + if err != nil { + return fmt.Errorf("failed to delete price, error: %w", err) + } + return nil +} + +func (a *accountPriceStoreImpl) GetByID(ctx context.Context, id int64) (*AccountPrice, error) { + price := &AccountPrice{} + err := a.db.Core.NewSelect().Model(price).Where("id = ?", id).Scan(ctx, price) + if err != nil { + return nil, fmt.Errorf("select price by id %d, error: %w", id, err) + } + return price, nil +} + +func (a *accountPriceStoreImpl) GetLatestByTime(ctx context.Context, req types.AcctPriceQueryReq) (*AccountPrice, error) { + price := &AccountPrice{} + err := a.db.Core.NewSelect().Model(price). + Where("sku_type = ?", req.SkuType). + Where("sku_kind = ?", req.SkuKind). + Where("resource_id = ?", req.ResourceID). + Where("sku_unit_type = ?", req.SkuUnitType). + Where("created_at <= ?", req.PriceTime). + Order("created_at DESC").Limit(1).Scan(ctx, price) + if err != nil { + return nil, fmt.Errorf("select price by time, error: %w", err) + } + return price, nil +} + +func (a *accountPriceStoreImpl) ListBySkuType(ctx context.Context, req types.AcctPriceListDBReq) ([]AccountPrice, int, error) { + var result []AccountPrice + q := a.db.Core.NewSelect().Model(&result). + DistinctOn("sku_type, sku_kind, resource_id, sku_unit_type"). + Where("sku_type = ?", req.SkuType) + + if len(req.SkuKind) > 0 { + skuKindInt, err := strconv.Atoi(req.SkuKind) + if err != nil { + return nil, 0, fmt.Errorf("invalid sku kind %s, error: %w", req.SkuKind, err) + } + q.Where("sku_kind = ?", skuKindInt) + } + + resourceIDs := []string{} + for _, rid := range req.ResourceID { + if rid != "" { + resourceIDs = append(resourceIDs, rid) + } + } + if len(resourceIDs) > 0 { + q.Where("resource_id IN (?)", bun.In(resourceIDs)) + } + + count, err := q.Count(ctx) + if err != nil { + return nil, 0, fmt.Errorf("failed to counting recorder, error: %w", err) + } + _, err = q.Order("sku_type ASC").Order("sku_kind ASC").Order("resource_id ASC"). + Order("sku_unit_type ASC").Order("created_at DESC"). + Limit(req.Per).Offset((req.Page-1)*req.Per).Exec(ctx, &result) + if err != nil { + return nil, 0, fmt.Errorf("select prices by type and kind and resource, error: %w", err) + } + return result, count, nil +} + +func (a *accountPriceStoreImpl) ListByIds(ctx context.Context, ids []int64) ([]*types.AcctPriceResp, error) { + var result []*AccountPrice + if len(ids) == 0 { + return nil, nil + } + err := a.db.Core.NewSelect().Model(&result). + Where("id IN (?)", bun.In(ids)). + Scan(ctx) + + if err != nil { + return nil, fmt.Errorf("failed to select prices by IDs, error: %w", err) + } + res := make([]*types.AcctPriceResp, len(result)) + for index, price := range result { + res[index] = &types.AcctPriceResp{ + Id: price.ID, + SkuType: price.SkuType, + SkuPrice: price.SkuPrice, + SkuKind: price.SkuKind, + SkuDesc: price.SkuDesc, + } + } + + return res, nil +} diff --git a/builder/store/database/account_price_test.go b/builder/store/database/account_price_test.go new file mode 100644 index 000000000..c910a303c --- /dev/null +++ b/builder/store/database/account_price_test.go @@ -0,0 +1,285 @@ +package database_test + +import ( + "context" + "strings" + "testing" + "time" + + "github.com/spf13/cast" + "github.com/stretchr/testify/require" + "opencsg.com/csghub-server/builder/store/database" + "opencsg.com/csghub-server/common/tests" + "opencsg.com/csghub-server/common/types" +) + +func TestAccountPriceStore_Create(t *testing.T) { + db := tests.InitTestDB() + defer db.Close() + ctx := context.TODO() + + store := database.NewAccountPriceStoreWithDB(db) + created, err := store.Create(ctx, database.AccountPrice{ + SkuDesc: "sku", + }) + require.Nil(t, err) + require.Equal(t, "sku", created.SkuDesc) + + r := &database.AccountPrice{} + err = db.Core.NewSelect().Model(r).Where("id=?", created.ID).Scan(ctx) + require.Nil(t, err) + require.Equal(t, "sku", r.SkuDesc) + +} + +func TestAccountPriceStore_Update(t *testing.T) { + db := tests.InitTestDB() + defer db.Close() + ctx := context.TODO() + + store := database.NewAccountPriceStoreWithDB(db) + c, err := store.Create(ctx, database.AccountPrice{ + SkuDesc: "sku", + }) + require.Nil(t, err) + + c.SkuDesc = "new_sku" + _, err = store.Update(ctx, *c) + require.Nil(t, err) + + r := &database.AccountPrice{} + err = db.Core.NewSelect().Model(r).Where("id=?", c.ID).Scan(ctx) + require.Nil(t, err) + require.Equal(t, "new_sku", r.SkuDesc) + +} + +func TestAccountPriceStore_Delete(t *testing.T) { + db := tests.InitTestDB() + defer db.Close() + ctx := context.TODO() + + store := database.NewAccountPriceStoreWithDB(db) + c, err := store.Create(ctx, database.AccountPrice{ + SkuDesc: "sku", + }) + require.Nil(t, err) + + err = store.Delete(ctx, *c) + require.Nil(t, err) + + r := &database.AccountPrice{} + err = db.Core.NewSelect().Model(r).Where("id=?", c.ID).Scan(ctx) + require.NotNil(t, err) +} + +func TestAccountPriceStore_GetByID(t *testing.T) { + db := tests.InitTestDB() + defer db.Close() + ctx := context.TODO() + + store := database.NewAccountPriceStoreWithDB(db) + c, err := store.Create(ctx, database.AccountPrice{ + SkuDesc: "sku", + }) + require.Nil(t, err) + + g, err := store.GetByID(ctx, c.ID) + require.Nil(t, err) + require.Equal(t, "sku", g.SkuDesc) + +} + +func TestAccountPriceStore_GetLatestByTime(t *testing.T) { + db := tests.InitTestDB() + defer db.Close() + ctx := context.TODO() + + store := database.NewAccountPriceStoreWithDB(db) + + prices := []*database.AccountPrice{ + { + SkuType: types.SKUCSGHub, SkuKind: types.SKUPackageAddon, ResourceID: "r", + SkuUnitType: "u", SkuDesc: "a", + }, + { + SkuType: types.SKUReserve, SkuKind: types.SKUPackageAddon, ResourceID: "r", + SkuUnitType: "u", SkuDesc: "b", + }, + { + SkuType: types.SKUCSGHub, SkuKind: types.SKUPackageAddon, ResourceID: "r", + SkuUnitType: "u", SkuDesc: "c", + }, + { + SkuType: types.SKUCSGHub, SkuKind: types.SKUTimeSpan, ResourceID: "r", + SkuUnitType: "u", SkuDesc: "d", + }, + { + SkuType: types.SKUCSGHub, SkuKind: types.SKUPackageAddon, ResourceID: "r", + SkuUnitType: "u", SkuDesc: "e", + }, + } + + dt := time.Now() + times := []time.Time{ + dt.Add(-3 * time.Hour), dt.Add(6 * time.Hour), + dt.Add(2 * time.Hour), dt.Add(1 * time.Hour), + dt.Add(12 * time.Hour), + } + + for i, p := range prices { + p.CreatedAt = times[i] + } + + for _, p := range prices { + _, err := store.Create(ctx, *p) + require.Nil(t, err) + } + + p, err := store.GetLatestByTime(ctx, types.AcctPriceQueryReq{ + SkuType: types.SKUCSGHub, + ResourceID: "r", + PriceTime: dt.Add(8 * time.Hour), + SkuKind: types.SKUPackageAddon, + SkuUnitType: "u", + }) + require.Nil(t, err) + require.Equal(t, "c", p.SkuDesc) + +} + +func TestAccountPriceStore_ListBySkuType(t *testing.T) { + db := tests.InitTestDB() + defer db.Close() + ctx := context.TODO() + + store := database.NewAccountPriceStoreWithDB(db) + + prices := []*database.AccountPrice{ + { + SkuType: types.SKUCSGHub, SkuKind: types.SKUPackageAddon, ResourceID: "r", + SkuUnitType: "u", SkuDesc: "a", + }, + { + SkuType: types.SKUCSGHub, SkuKind: types.SKUPackageAddon, ResourceID: "c", + SkuUnitType: "u", SkuDesc: "b", + }, + { + SkuType: types.SKUCSGHub, SkuKind: types.SKUPayAsYouGo, ResourceID: "r", + SkuUnitType: "u", SkuDesc: "c", + }, + { + SkuType: types.SKUCSGHub, SkuKind: types.SKUPayAsYouGo, ResourceID: "r", + SkuUnitType: "u", SkuDesc: "d", + }, + { + SkuType: types.SKUCSGHub, SkuKind: types.SKUPayAsYouGo, ResourceID: "r", + SkuUnitType: "u", SkuDesc: "e", + }, + { + SkuType: types.SKUReserve, SkuKind: types.SKUPackageAddon, ResourceID: "r", + SkuUnitType: "u", SkuDesc: "f", + }, + } + + namesFunc := func(prices []database.AccountPrice) string { + names := []string{} + for _, p := range prices { + names = append(names, p.SkuDesc) + } + return strings.Join(names, "/") + } + + for _, p := range prices { + _, err := store.Create(ctx, *p) + require.Nil(t, err) + } + + data, count, err := store.ListBySkuType(ctx, types.AcctPriceListDBReq{ + SkuType: types.SKUCSGHub, + Page: 1, + Per: 10, + }) + require.Nil(t, err) + require.Equal(t, 3, count) + require.Equal(t, 3, len(data)) + require.Equal(t, "c/b/a", namesFunc(data)) + + data, count, err = store.ListBySkuType(ctx, types.AcctPriceListDBReq{ + SkuType: types.SKUCSGHub, + SkuKind: cast.ToString(int(types.SKUPackageAddon)), + Page: 1, + Per: 10, + }) + require.Nil(t, err) + require.Equal(t, 2, count) + require.Equal(t, 2, len(data)) + require.Equal(t, "b/a", namesFunc(data)) + + data, count, err = store.ListBySkuType(ctx, types.AcctPriceListDBReq{ + SkuType: types.SKUCSGHub, + SkuKind: cast.ToString(int(types.SKUPackageAddon)), + ResourceID: []string{"r"}, + Page: 1, + Per: 10, + }) + require.Nil(t, err) + require.Equal(t, 1, count) + require.Equal(t, 1, len(data)) + require.Equal(t, "a", namesFunc(data)) + +} + +func TestAccountPriceStore_ListByIds(t *testing.T) { + db := tests.InitTestDB() + defer db.Close() + ctx := context.TODO() + + store := database.NewAccountPriceStoreWithDB(db) + + price1 := &database.AccountPrice{ + SkuType: 1, + SkuPrice: 100, + SkuUnit: 10, + SkuDesc: "sku1", + ResourceID: "resource1", + SkuUnitType: "unit1", + SkuPriceCurrency: "USD", + SkuKind: 1, + Quota: "quota1", + SkuPriceID: 1, + } + price2 := &database.AccountPrice{ + SkuType: 2, + SkuPrice: 200, + SkuUnit: 20, + SkuDesc: "sku2", + ResourceID: "resource2", + SkuUnitType: "unit2", + SkuPriceCurrency: "EUR", + SkuKind: 2, + Quota: "quota2", + SkuPriceID: 2, + } + + _, err := db.Core.NewInsert().Model(price1).Exec(ctx) + require.Nil(t, err) + _, err = db.Core.NewInsert().Model(price2).Exec(ctx) + require.Nil(t, err) + + ids := []int64{price1.ID, price2.ID} + result, err := store.ListByIds(ctx, ids) + require.Nil(t, err) + + require.Equal(t, 2, len(result)) + + for _, price := range result { + if price.Id == price1.ID { + require.Equal(t, price1.SkuType, price.SkuType) + require.Equal(t, price1.SkuKind, price.SkuKind) + } else if price.Id == price2.ID { + require.Equal(t, price2.SkuType, price.SkuType) + require.Equal(t, price2.SkuKind, price.SkuKind) + } + } +} diff --git a/builder/store/database/space_resource.go b/builder/store/database/space_resource.go index 6f32446ab..f21304c73 100644 --- a/builder/store/database/space_resource.go +++ b/builder/store/database/space_resource.go @@ -22,6 +22,7 @@ type SpaceResourceStore interface { FindByName(ctx context.Context, name string) (*SpaceResource, error) FindAll(ctx context.Context) ([]SpaceResource, error) FindAllResourceTypes(ctx context.Context, clusterId string) ([]string, error) + FindByHardwareType(ctx context.Context, hardwareType string) ([]SpaceResource, error) } func NewSpaceResourceStore() SpaceResourceStore { @@ -168,3 +169,14 @@ func (s *spaceResourceStoreImpl) FindAllResourceTypes(ctx context.Context, clust return hardWareTypes, nil } + +func (s *spaceResourceStoreImpl) FindByHardwareType(ctx context.Context, hardwareType string) ([]SpaceResource, error) { + var result []SpaceResource + err := s.db.Operator.Core.NewSelect().Model(&result). + Where("EXISTS (SELECT 1 FROM jsonb_each(resources::jsonb) WHERE value->>'type' = ?)", hardwareType). + Scan(ctx) + if err != nil { + return nil, errorx.HandleDBError(err, nil) + } + return result, nil +} diff --git a/builder/store/database/space_resource_test.go b/builder/store/database/space_resource_test.go index 443b98ef2..cc43f0491 100644 --- a/builder/store/database/space_resource_test.go +++ b/builder/store/database/space_resource_test.go @@ -149,6 +149,90 @@ func TestSpaceResourceStore_FindAllResourceTypes_InvalidJSON(t *testing.T) { require.Empty(t, types) } +func TestSpaceResourceStore_FindByGPU(t *testing.T) { + db := tests.InitTestDB() + defer db.Close() + ctx := context.TODO() + + store := database.NewSpaceResourceStoreWithDB(db) + // Create test resources with different GPU types + _, err := store.Create(ctx, database.SpaceResource{ + Name: "r1", + ClusterID: "c1", + Resources: `{"gpu": {"type": "A10", "num": "1", "resource_name": "nvidia.com/gpu", "labels": {"aliyun.accelerator/nvidia_name": "NVIDIA-A10"}}, "cpu": {"type": "Intel", "num": "2"}, "memory": "20Gi","replicas":2}`, + }) + require.Nil(t, err) + + _, err = store.Create(ctx, database.SpaceResource{ + Name: "r2", + ClusterID: "c1", + Resources: `{"gpu": {"type": "A100", "num": "1", "resource_name": "nvidia.com/gpu", "labels": {"aliyun.accelerator/nvidia_name": "NVIDIA-A100"}}, "cpu": {"type": "Intel", "num": "4"}, "memory": "40Gi","replicas":1}`, + }) + require.Nil(t, err) + + _, err = store.Create(ctx, database.SpaceResource{ + Name: "r3", + ClusterID: "c1", + Resources: `{"cpu": {"type": "Intel", "num": "8"}, "memory": "60Gi","replicas":1}`, // No GPU + }) + require.Nil(t, err) + + // Test FindByHardwareType with A10 + results, err := store.FindByHardwareType(ctx, "A10") + require.Nil(t, err) + require.Equal(t, 1, len(results)) + require.Equal(t, "r1", results[0].Name) + + // Test FindByHardwareType with A100 + results, err = store.FindByHardwareType(ctx, "A100") + require.Nil(t, err) + require.Equal(t, 1, len(results)) + require.Equal(t, "r2", results[0].Name) + + // Test FindByHardwareType with non-existent GPU type + results, err = store.FindByHardwareType(ctx, "H100") + require.Nil(t, err) + require.Equal(t, 0, len(results)) + + // Test FindByHardwareType with different hardware types + results, err = store.FindByHardwareType(ctx, "A10") + require.Nil(t, err) + require.Equal(t, 1, len(results)) + require.Equal(t, "r1", results[0].Name) + + // Add test resources with other hardware types + _, err = store.Create(ctx, database.SpaceResource{ + Name: "r4", + ClusterID: "c1", + Resources: `{"npu": {"type": "Ascend910", "num": "1", "resource_name": "ascend.com/npu", "labels": {"vendor": "Huawei"}}, "cpu": {"type": "Huawei", "num": "8"}, "memory": "64Gi","replicas":1}`, + }) + require.Nil(t, err) + + _, err = store.Create(ctx, database.SpaceResource{ + Name: "r5", + ClusterID: "c1", + Resources: `{"gcu": {"type": "G100", "num": "2", "resource_name": "enflame.com/gcu", "labels": {"vendor": "Enflame"}}, "cpu": {"type": "Intel", "num": "16"}, "memory": "128Gi","replicas":1}`, + }) + require.Nil(t, err) + + // Test FindByHardwareType with NPU + results, err = store.FindByHardwareType(ctx, "Ascend910") + require.Nil(t, err) + require.Equal(t, 1, len(results)) + require.Equal(t, "r4", results[0].Name) + + // Test FindByHardwareType with GCU + results, err = store.FindByHardwareType(ctx, "G100") + require.Nil(t, err) + require.Equal(t, 1, len(results)) + require.Equal(t, "r5", results[0].Name) + + // Test FindByHardwareType with non-existent hardware type + results, err = store.FindByHardwareType(ctx, "MLU270") + require.Nil(t, err) + require.Equal(t, 0, len(results)) +} + func TestSpaceResourceStore_Filter(t *testing.T) { db := tests.InitTestDB() defer db.Close()