feat: расчёт стоимости платного тестирования/аренды GPU-серверов

Новая вкладка «Аренда» в конфигураторе (доступна для проектов с флагом
rental_enabled): расчёт по методичке "Регламент расчёта стоимости
платного тестирования и аренды GPU-серверов" — Разовый + Еженедельный
платёж по каждому компоненту (New/БУ чекбоксом), с аплифтом к Estimate.

Заодно: поддержка (support_code) в Base-вкладке теперь добавляется как
обычный LOT в спеку (SVC_{срок}y{уровень}_{платформа}) через
autocomplete-пикер вместо выпадающего списка — партномер собирается
тем же lot_name-based механизмом, что и для GPU/CPU/памяти, справочная
цена по регламенту техподдержки хранится в qt_settings.support_pricing.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Mikhail Chusavitin
2026-07-22 13:34:07 +03:00
co-authored by Claude Sonnet 5
parent b48436de93
commit 7edb80e498
25 changed files with 1052 additions and 71 deletions
+54 -12
View File
@@ -697,6 +697,7 @@ func setupRouter(cfg *config.Config, local *localdb.LocalDB, connMgr *db.Connect
// Local-first configuration service (replaces old ConfigurationService)
projectService = services.NewProjectService(local)
configService := services.NewLocalConfigurationService(local, syncService, quoteService, isOnline)
rentalService := services.NewRentalService(local)
// Data hygiene: remove empty nameless projects and ensure every configuration is attached to a project.
if removed, err := local.ConsolidateSystemProjects(); err == nil && removed > 0 {
@@ -1344,6 +1345,45 @@ func setupRouter(cfg *config.Config, local *localdb.LocalDB, connMgr *db.Connect
configs.POST("/:uuid/vendor-spec/resolve", vendorSpecHandler.ResolveVendorSpec)
configs.POST("/:uuid/vendor-spec/apply", vendorSpecHandler.ApplyVendorSpec)
// Rental / paid-testing pricing endpoints (Аренда tab)
configs.PUT("/:uuid/rental", func(c *gin.Context) {
var req services.RentalUpdateRequest
if err := c.ShouldBindJSON(&req); err != nil {
respondError(c, http.StatusBadRequest, "invalid request", err)
return
}
config, err := configService.UpdateRentalNoAuth(c.Param("uuid"), &req)
if err != nil {
switch {
case errors.Is(err, services.ErrConfigNotFound):
respondError(c, http.StatusNotFound, "resource not found", err)
default:
respondError(c, http.StatusInternalServerError, "internal server error", err)
}
return
}
c.JSON(http.StatusOK, config)
})
configs.POST("/:uuid/rental/calculate", func(c *gin.Context) {
var req services.RentalUpdateRequest
if err := c.ShouldBindJSON(&req); err != nil {
respondError(c, http.StatusBadRequest, "invalid request", err)
return
}
result, err := rentalService.Calculate(c.Param("uuid"), &req)
if err != nil {
switch {
case errors.Is(err, services.ErrConfigNotFound):
respondError(c, http.StatusNotFound, "resource not found", err)
default:
respondError(c, http.StatusInternalServerError, "internal server error", err)
}
return
}
c.JSON(http.StatusOK, result)
})
configs.PATCH("/:uuid/server-count", func(c *gin.Context) {
uuid := c.Param("uuid")
var req struct {
@@ -1550,23 +1590,25 @@ func setupRouter(cfg *config.Config, local *localdb.LocalDB, connMgr *db.Connect
// Return simplified list of all projects (UUID + Name only)
type ProjectSimple struct {
UUID string `json:"uuid"`
Code string `json:"code"`
Variant string `json:"variant"`
Name string `json:"name"`
IsActive bool `json:"is_active"`
CreatedAt time.Time `json:"created_at"`
UUID string `json:"uuid"`
Code string `json:"code"`
Variant string `json:"variant"`
Name string `json:"name"`
IsActive bool `json:"is_active"`
RentalEnabled bool `json:"rental_enabled"`
CreatedAt time.Time `json:"created_at"`
}
simplified := make([]ProjectSimple, 0, len(allProjects))
for _, p := range allProjects {
simplified = append(simplified, ProjectSimple{
UUID: p.UUID,
Code: p.Code,
Variant: p.Variant,
Name: derefString(p.Name),
IsActive: p.IsActive,
CreatedAt: p.CreatedAt,
UUID: p.UUID,
Code: p.Code,
Variant: p.Variant,
Name: derefString(p.Name),
IsActive: p.IsActive,
RentalEnabled: p.RentalEnabled,
CreatedAt: p.CreatedAt,
})
}