Update pricing model to include half-year pricing and enhance validation
CI / Backend (build + test) (push) Successful in 1m24s
CI / Frontend (lint + typecheck + build) (push) Failing after 15s

- Introduced a new pricing field, `PricePerConfigPerHalfYear`, to the `PricingSettings` model, allowing for more flexible pricing options.
- Updated the `UpdatePricingSettingsCommand` and its validator to include the new half-year pricing, ensuring proper validation against the quarterly and yearly rates.
- Modified the `PricingSettingsDto` and related frontend components to accommodate the new half-year pricing field, including validation logic to prevent pricing discrepancies.
- Enhanced API documentation and frontend forms to reflect the updated pricing structure and validation rules.
This commit is contained in:
Leonid Pershin
2026-07-18 20:17:32 +03:00
parent 6dfd51ae7c
commit 32221af503
16 changed files with 1133 additions and 39 deletions
@@ -20,7 +20,7 @@ public sealed class GetPricingSettingsQueryHandler(IAppDbContext dbContext)
// Ещё не сидировано/не сохранено ни разу — цена не задана, а не ошибка.
return Result.Success(
settings is null
? new PricingSettingsDto(null, null)
? new PricingSettingsDto(null, null, null)
: PricingSettingsDto.FromDomain(settings)
);
}
@@ -2,8 +2,16 @@ using PnvPanel.Domain.Pricing;
namespace PnvPanel.Application.Admin.Pricing;
public sealed record PricingSettingsDto(int? PricePerConfigPerQuarter, int? PricePerConfigPerYear)
public sealed record PricingSettingsDto(
int? PricePerConfigPerQuarter,
int? PricePerConfigPerHalfYear,
int? PricePerConfigPerYear
)
{
public static PricingSettingsDto FromDomain(PricingSettings settings) =>
new(settings.PricePerConfigPerQuarter, settings.PricePerConfigPerYear);
new(
settings.PricePerConfigPerQuarter,
settings.PricePerConfigPerHalfYear,
settings.PricePerConfigPerYear
);
}
@@ -5,5 +5,6 @@ namespace PnvPanel.Application.Admin.Pricing;
public sealed record UpdatePricingSettingsCommand(
int? PricePerConfigPerQuarter,
int? PricePerConfigPerHalfYear,
int? PricePerConfigPerYear
) : ICommand<Result<PricingSettingsDto>>;
@@ -21,7 +21,11 @@ public sealed class UpdatePricingSettingsCommandHandler(IAppDbContext dbContext)
dbContext.PricingSettings.Add(settings);
}
settings.Update(command.PricePerConfigPerQuarter, command.PricePerConfigPerYear);
settings.Update(
command.PricePerConfigPerQuarter,
command.PricePerConfigPerHalfYear,
command.PricePerConfigPerYear
);
return Result.Success(PricingSettingsDto.FromDomain(settings));
}
@@ -10,14 +10,38 @@ public sealed class UpdatePricingSettingsCommandValidator
RuleFor(x => x.PricePerConfigPerQuarter!.Value)
.GreaterThanOrEqualTo(0)
.When(x => x.PricePerConfigPerQuarter.HasValue);
RuleFor(x => x.PricePerConfigPerHalfYear!.Value)
.GreaterThanOrEqualTo(0)
.When(x => x.PricePerConfigPerHalfYear.HasValue);
RuleFor(x => x.PricePerConfigPerYear!.Value)
.GreaterThanOrEqualTo(0)
.When(x => x.PricePerConfigPerYear.HasValue);
// Обе ставки — цена за конфиг В МЕСЯЦ при соответствующем тарифе оплаты; итог за период =
// ставка × число месяцев. Годовая ставка может быть ниже квартальной (скидка за годовую
// оплату), но итог за год (ставка×12) не должен быть дешевле итога за квартал (ставка×3) —
// иначе выгоднее купить "год" и не продлевать, чем платить за квартал.
// Все три ставки — цена за конфиг В МЕСЯЦ при соответствующем тарифе оплаты; итог за период =
// ставка × число месяцев. Более длинный период может быть дешевле "в пересчёте на месяц"
// (скидка за предоплату), но итог за него не должен быть дешевле итога за более короткий
// период — иначе выгоднее купить длинный тариф и не продлевать, чем платить за короткий.
RuleFor(x => x.PricePerConfigPerHalfYear)
.Must(
(command, halfYear) =>
!halfYear.HasValue
|| !command.PricePerConfigPerQuarter.HasValue
|| (decimal)halfYear.Value * 6 >= (decimal)command.PricePerConfigPerQuarter.Value * 3
)
.WithMessage("Цена за полгода (в пересчёте на 6 месяцев) не может быть меньше цены за 3 месяца.")
.When(x => x.PricePerConfigPerQuarter.HasValue && x.PricePerConfigPerHalfYear.HasValue);
RuleFor(x => x.PricePerConfigPerYear)
.Must(
(command, year) =>
!year.HasValue
|| !command.PricePerConfigPerHalfYear.HasValue
|| (decimal)year.Value * 12 >= (decimal)command.PricePerConfigPerHalfYear.Value * 6
)
.WithMessage("Цена за год (в пересчёте на 12 месяцев) не может быть меньше цены за полгода.")
.When(x => x.PricePerConfigPerHalfYear.HasValue && x.PricePerConfigPerYear.HasValue);
// Если полугодовая ставка не задана, год всё равно не должен быть дешевле квартала.
RuleFor(x => x.PricePerConfigPerYear)
.Must(
(command, year) =>
@@ -26,6 +50,10 @@ public sealed class UpdatePricingSettingsCommandValidator
|| (decimal)year.Value * 12 >= (decimal)command.PricePerConfigPerQuarter.Value * 3
)
.WithMessage("Цена за год (в пересчёте на 12 месяцев) не может быть меньше цены за 3 месяца.")
.When(x => x.PricePerConfigPerQuarter.HasValue && x.PricePerConfigPerYear.HasValue);
.When(x =>
!x.PricePerConfigPerHalfYear.HasValue
&& x.PricePerConfigPerQuarter.HasValue
&& x.PricePerConfigPerYear.HasValue
);
}
}