2026-04-20 12:24:06 -03:00
|
|
|
using FluentValidation.TestHelper;
|
|
|
|
|
using Microsoft.Extensions.Time.Testing;
|
|
|
|
|
using SIGCM2.Application.Pricing.ChargeableChars.Create;
|
|
|
|
|
using SIGCM2.Domain.Pricing.ChargeableChars;
|
|
|
|
|
|
|
|
|
|
namespace SIGCM2.Application.Tests.Pricing.ChargeableChars;
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// PRC-001 — Validator tests for CreateChargeableCharConfigCommand.
|
|
|
|
|
/// Covers: Symbol length, Category enum, PricePerUnit > 0, ValidFrom >= today_AR.
|
|
|
|
|
/// </summary>
|
|
|
|
|
public class CreateChargeableCharConfigCommandValidatorTests
|
|
|
|
|
{
|
|
|
|
|
private readonly FakeTimeProvider _time = new(new DateTimeOffset(2026, 4, 20, 12, 0, 0, TimeSpan.Zero));
|
|
|
|
|
private readonly CreateChargeableCharConfigCommandValidator _validator;
|
|
|
|
|
|
|
|
|
|
private static readonly DateOnly Today = new(2026, 4, 20);
|
|
|
|
|
private static readonly DateOnly Yesterday = new(2026, 4, 19);
|
|
|
|
|
private static readonly DateOnly Tomorrow = new(2026, 4, 21);
|
|
|
|
|
|
|
|
|
|
public CreateChargeableCharConfigCommandValidatorTests()
|
|
|
|
|
{
|
|
|
|
|
_validator = new CreateChargeableCharConfigCommandValidator(_time);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static CreateChargeableCharConfigCommand ValidCmd() => new(
|
2026-04-21 10:54:47 -03:00
|
|
|
ProductTypeId: null,
|
2026-04-20 12:24:06 -03:00
|
|
|
Symbol: "$",
|
|
|
|
|
Category: ChargeableCharCategories.Currency,
|
|
|
|
|
PricePerUnit: 1.0m,
|
|
|
|
|
ValidFrom: Today);
|
|
|
|
|
|
|
|
|
|
// ── Symbol ───────────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
[Fact]
|
|
|
|
|
public void Symbol_Empty_FailsValidation()
|
|
|
|
|
{
|
|
|
|
|
var cmd = ValidCmd() with { Symbol = "" };
|
|
|
|
|
_validator.TestValidate(cmd).ShouldHaveValidationErrorFor(x => x.Symbol);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[Fact]
|
|
|
|
|
public void Symbol_TooLong_FailsValidation()
|
|
|
|
|
{
|
|
|
|
|
// max 4 chars
|
|
|
|
|
var cmd = ValidCmd() with { Symbol = "ABCDE" };
|
|
|
|
|
_validator.TestValidate(cmd).ShouldHaveValidationErrorFor(x => x.Symbol);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[Fact]
|
|
|
|
|
public void Symbol_SingleChar_Passes()
|
|
|
|
|
{
|
|
|
|
|
var cmd = ValidCmd() with { Symbol = "$" };
|
|
|
|
|
_validator.TestValidate(cmd).ShouldNotHaveValidationErrorFor(x => x.Symbol);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[Fact]
|
|
|
|
|
public void Symbol_FourChars_Passes()
|
|
|
|
|
{
|
|
|
|
|
var cmd = ValidCmd() with { Symbol = "ABCD" };
|
|
|
|
|
_validator.TestValidate(cmd).ShouldNotHaveValidationErrorFor(x => x.Symbol);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ── Category ─────────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
[Fact]
|
|
|
|
|
public void Category_Invalid_FailsValidation()
|
|
|
|
|
{
|
|
|
|
|
var cmd = ValidCmd() with { Category = "Unknown" };
|
|
|
|
|
_validator.TestValidate(cmd).ShouldHaveValidationErrorFor(x => x.Category);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[Fact]
|
|
|
|
|
public void Category_Empty_FailsValidation()
|
|
|
|
|
{
|
|
|
|
|
var cmd = ValidCmd() with { Category = "" };
|
|
|
|
|
_validator.TestValidate(cmd).ShouldHaveValidationErrorFor(x => x.Category);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[Theory]
|
|
|
|
|
[InlineData("Currency")]
|
|
|
|
|
[InlineData("Percentage")]
|
|
|
|
|
[InlineData("Exclamation")]
|
|
|
|
|
[InlineData("Question")]
|
|
|
|
|
[InlineData("Other")]
|
|
|
|
|
public void Category_ValidValues_Pass(string category)
|
|
|
|
|
{
|
|
|
|
|
var cmd = ValidCmd() with { Category = category };
|
|
|
|
|
_validator.TestValidate(cmd).ShouldNotHaveValidationErrorFor(x => x.Category);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ── PricePerUnit ─────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
[Fact]
|
chore(prc-001): followups #54 #55 #57 #58 — emoji validator + opt-in pricing + demo seed + tsconfig
Resuelve 4 de los followups creados post-archive de PRC-001:
#55 — Decisión de negocio (2026-04-21): emojis NO se permiten en Symbol config.
- WordCounterService.ContainsEmoji(string): helper publico que reutiliza los
rangos Unicode de IsEmojiRune (Emoticons, Pictographs, Dingbats, VS-16, ZWJ).
- CreateChargeableCharConfigCommandValidator: regla .Must que rechaza emoji
en Symbol con mensaje claro. Defensiva: cubre clientes directos al API
(Postman, adversariales) mas alla del SymbolInput blocker del frontend.
- Tests: 5 emojis positivos (smile/car/fire/heart VS-16/sun) + 8 plain symbols
($, %, !, ¡, @, €, ##, ABCD) + actualizacion del Api test E2E (Post_WithEmojiSymbol).
#57 — Alineacion FluentValidation con opt-in billing (CK_Price_NonNegative >= 0).
- CreateChargeableCharConfigCommandValidator.PricePerUnit: GreaterThan(0)
-> GreaterThanOrEqualTo(0). Mensaje explica el significado: 0 = no cobra.
- Tests actualizados: PricePerUnit_Zero ahora Passes (era Fails). Negative
sigue fallando. Api e2e usa -1 para el caso de rechazo.
#58 — tsconfig ignoreDeprecations + MSW handler (parte a).
- src/web/tsconfig.json: agrega "ignoreDeprecations": "6.0" para silenciar
el warning TS5101 del baseUrl deprecated en TS 6.x.
- (El MSW handler de /api/v1/admin/product-types no aplica — los tests ya
mockean ProductTypeSelect directamente; warning residual no existe.)
#54 — Seeder demo de overrides ficticios per-ProductType (V025).
- database/migrations/V025__seed_chargeable_char_overrides_demo.sql:
MERGE idempotente que crea overrides de ChargeableCharConfig para
ProductTypes Clasificado/Notables/Fúnebres si existen en la DB.
Precios ficticios ($ 5-8, % 3-5, ! 2-4, ¡ 2-4). No-op si los tipos no
estan seedados (sera cuando PRD-008 haga seed de los 12 legacy).
- V025_ROLLBACK.sql: elimina overrides demo preservando globales.
- Aplicado en SIGCM2, SIGCM2_Test_App, SIGCM2_Test_Api.
- database/README.md: V025 agregada al indice.
Tests: 1659 .NET (1310 Application + 349 Api) + 510 vitest — todos GREEN.
Closes #54, #55, #57, #58.
2026-04-21 13:27:54 -03:00
|
|
|
public void PricePerUnit_Zero_Passes_OptInBilling()
|
2026-04-20 12:24:06 -03:00
|
|
|
{
|
chore(prc-001): followups #54 #55 #57 #58 — emoji validator + opt-in pricing + demo seed + tsconfig
Resuelve 4 de los followups creados post-archive de PRC-001:
#55 — Decisión de negocio (2026-04-21): emojis NO se permiten en Symbol config.
- WordCounterService.ContainsEmoji(string): helper publico que reutiliza los
rangos Unicode de IsEmojiRune (Emoticons, Pictographs, Dingbats, VS-16, ZWJ).
- CreateChargeableCharConfigCommandValidator: regla .Must que rechaza emoji
en Symbol con mensaje claro. Defensiva: cubre clientes directos al API
(Postman, adversariales) mas alla del SymbolInput blocker del frontend.
- Tests: 5 emojis positivos (smile/car/fire/heart VS-16/sun) + 8 plain symbols
($, %, !, ¡, @, €, ##, ABCD) + actualizacion del Api test E2E (Post_WithEmojiSymbol).
#57 — Alineacion FluentValidation con opt-in billing (CK_Price_NonNegative >= 0).
- CreateChargeableCharConfigCommandValidator.PricePerUnit: GreaterThan(0)
-> GreaterThanOrEqualTo(0). Mensaje explica el significado: 0 = no cobra.
- Tests actualizados: PricePerUnit_Zero ahora Passes (era Fails). Negative
sigue fallando. Api e2e usa -1 para el caso de rechazo.
#58 — tsconfig ignoreDeprecations + MSW handler (parte a).
- src/web/tsconfig.json: agrega "ignoreDeprecations": "6.0" para silenciar
el warning TS5101 del baseUrl deprecated en TS 6.x.
- (El MSW handler de /api/v1/admin/product-types no aplica — los tests ya
mockean ProductTypeSelect directamente; warning residual no existe.)
#54 — Seeder demo de overrides ficticios per-ProductType (V025).
- database/migrations/V025__seed_chargeable_char_overrides_demo.sql:
MERGE idempotente que crea overrides de ChargeableCharConfig para
ProductTypes Clasificado/Notables/Fúnebres si existen en la DB.
Precios ficticios ($ 5-8, % 3-5, ! 2-4, ¡ 2-4). No-op si los tipos no
estan seedados (sera cuando PRD-008 haga seed de los 12 legacy).
- V025_ROLLBACK.sql: elimina overrides demo preservando globales.
- Aplicado en SIGCM2, SIGCM2_Test_App, SIGCM2_Test_Api.
- database/README.md: V025 agregada al indice.
Tests: 1659 .NET (1310 Application + 349 Api) + 510 vitest — todos GREEN.
Closes #54, #55, #57, #58.
2026-04-21 13:27:54 -03:00
|
|
|
// PRC-001 followup #57: opt-in billing — 0 is a valid price meaning
|
|
|
|
|
// "this Symbol exists but does NOT charge extra for this ProductType".
|
|
|
|
|
// Aligned with DB check constraint CK_ChargeableCharConfig_Price_NonNegative (>= 0).
|
2026-04-20 12:24:06 -03:00
|
|
|
var cmd = ValidCmd() with { PricePerUnit = 0m };
|
chore(prc-001): followups #54 #55 #57 #58 — emoji validator + opt-in pricing + demo seed + tsconfig
Resuelve 4 de los followups creados post-archive de PRC-001:
#55 — Decisión de negocio (2026-04-21): emojis NO se permiten en Symbol config.
- WordCounterService.ContainsEmoji(string): helper publico que reutiliza los
rangos Unicode de IsEmojiRune (Emoticons, Pictographs, Dingbats, VS-16, ZWJ).
- CreateChargeableCharConfigCommandValidator: regla .Must que rechaza emoji
en Symbol con mensaje claro. Defensiva: cubre clientes directos al API
(Postman, adversariales) mas alla del SymbolInput blocker del frontend.
- Tests: 5 emojis positivos (smile/car/fire/heart VS-16/sun) + 8 plain symbols
($, %, !, ¡, @, €, ##, ABCD) + actualizacion del Api test E2E (Post_WithEmojiSymbol).
#57 — Alineacion FluentValidation con opt-in billing (CK_Price_NonNegative >= 0).
- CreateChargeableCharConfigCommandValidator.PricePerUnit: GreaterThan(0)
-> GreaterThanOrEqualTo(0). Mensaje explica el significado: 0 = no cobra.
- Tests actualizados: PricePerUnit_Zero ahora Passes (era Fails). Negative
sigue fallando. Api e2e usa -1 para el caso de rechazo.
#58 — tsconfig ignoreDeprecations + MSW handler (parte a).
- src/web/tsconfig.json: agrega "ignoreDeprecations": "6.0" para silenciar
el warning TS5101 del baseUrl deprecated en TS 6.x.
- (El MSW handler de /api/v1/admin/product-types no aplica — los tests ya
mockean ProductTypeSelect directamente; warning residual no existe.)
#54 — Seeder demo de overrides ficticios per-ProductType (V025).
- database/migrations/V025__seed_chargeable_char_overrides_demo.sql:
MERGE idempotente que crea overrides de ChargeableCharConfig para
ProductTypes Clasificado/Notables/Fúnebres si existen en la DB.
Precios ficticios ($ 5-8, % 3-5, ! 2-4, ¡ 2-4). No-op si los tipos no
estan seedados (sera cuando PRD-008 haga seed de los 12 legacy).
- V025_ROLLBACK.sql: elimina overrides demo preservando globales.
- Aplicado en SIGCM2, SIGCM2_Test_App, SIGCM2_Test_Api.
- database/README.md: V025 agregada al indice.
Tests: 1659 .NET (1310 Application + 349 Api) + 510 vitest — todos GREEN.
Closes #54, #55, #57, #58.
2026-04-21 13:27:54 -03:00
|
|
|
_validator.TestValidate(cmd).ShouldNotHaveValidationErrorFor(x => x.PricePerUnit);
|
2026-04-20 12:24:06 -03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[Fact]
|
|
|
|
|
public void PricePerUnit_Negative_FailsValidation()
|
|
|
|
|
{
|
|
|
|
|
var cmd = ValidCmd() with { PricePerUnit = -1m };
|
|
|
|
|
_validator.TestValidate(cmd).ShouldHaveValidationErrorFor(x => x.PricePerUnit);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[Fact]
|
|
|
|
|
public void PricePerUnit_Positive_Passes()
|
|
|
|
|
{
|
|
|
|
|
var cmd = ValidCmd() with { PricePerUnit = 0.01m };
|
|
|
|
|
_validator.TestValidate(cmd).ShouldNotHaveValidationErrorFor(x => x.PricePerUnit);
|
|
|
|
|
}
|
|
|
|
|
|
chore(prc-001): followups #54 #55 #57 #58 — emoji validator + opt-in pricing + demo seed + tsconfig
Resuelve 4 de los followups creados post-archive de PRC-001:
#55 — Decisión de negocio (2026-04-21): emojis NO se permiten en Symbol config.
- WordCounterService.ContainsEmoji(string): helper publico que reutiliza los
rangos Unicode de IsEmojiRune (Emoticons, Pictographs, Dingbats, VS-16, ZWJ).
- CreateChargeableCharConfigCommandValidator: regla .Must que rechaza emoji
en Symbol con mensaje claro. Defensiva: cubre clientes directos al API
(Postman, adversariales) mas alla del SymbolInput blocker del frontend.
- Tests: 5 emojis positivos (smile/car/fire/heart VS-16/sun) + 8 plain symbols
($, %, !, ¡, @, €, ##, ABCD) + actualizacion del Api test E2E (Post_WithEmojiSymbol).
#57 — Alineacion FluentValidation con opt-in billing (CK_Price_NonNegative >= 0).
- CreateChargeableCharConfigCommandValidator.PricePerUnit: GreaterThan(0)
-> GreaterThanOrEqualTo(0). Mensaje explica el significado: 0 = no cobra.
- Tests actualizados: PricePerUnit_Zero ahora Passes (era Fails). Negative
sigue fallando. Api e2e usa -1 para el caso de rechazo.
#58 — tsconfig ignoreDeprecations + MSW handler (parte a).
- src/web/tsconfig.json: agrega "ignoreDeprecations": "6.0" para silenciar
el warning TS5101 del baseUrl deprecated en TS 6.x.
- (El MSW handler de /api/v1/admin/product-types no aplica — los tests ya
mockean ProductTypeSelect directamente; warning residual no existe.)
#54 — Seeder demo de overrides ficticios per-ProductType (V025).
- database/migrations/V025__seed_chargeable_char_overrides_demo.sql:
MERGE idempotente que crea overrides de ChargeableCharConfig para
ProductTypes Clasificado/Notables/Fúnebres si existen en la DB.
Precios ficticios ($ 5-8, % 3-5, ! 2-4, ¡ 2-4). No-op si los tipos no
estan seedados (sera cuando PRD-008 haga seed de los 12 legacy).
- V025_ROLLBACK.sql: elimina overrides demo preservando globales.
- Aplicado en SIGCM2, SIGCM2_Test_App, SIGCM2_Test_Api.
- database/README.md: V025 agregada al indice.
Tests: 1659 .NET (1310 Application + 349 Api) + 510 vitest — todos GREEN.
Closes #54, #55, #57, #58.
2026-04-21 13:27:54 -03:00
|
|
|
// ── Symbol emoji rejection (PRC-001 followup #55) ───────────────────────────
|
|
|
|
|
|
|
|
|
|
[Theory]
|
|
|
|
|
[InlineData("😀")] // smiley face (U+1F600, Emoticons block)
|
|
|
|
|
[InlineData("🚗")] // car (U+1F697, Transport block)
|
|
|
|
|
[InlineData("🔥")] // fire (U+1F525, Misc Symbols & Pictographs)
|
|
|
|
|
[InlineData("❤️")] // red heart + VS-16 (U+2764 U+FE0F)
|
|
|
|
|
[InlineData("☀️")] // sun (U+2600 in Misc Symbols block)
|
|
|
|
|
public void Symbol_WithEmoji_FailsValidation(string emojiSymbol)
|
|
|
|
|
{
|
|
|
|
|
var cmd = ValidCmd() with { Symbol = emojiSymbol };
|
|
|
|
|
_validator.TestValidate(cmd).ShouldHaveValidationErrorFor(x => x.Symbol);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[Theory]
|
|
|
|
|
[InlineData("$")]
|
|
|
|
|
[InlineData("%")]
|
|
|
|
|
[InlineData("!")]
|
|
|
|
|
[InlineData("¡")]
|
|
|
|
|
[InlineData("@")]
|
|
|
|
|
[InlineData("€")]
|
|
|
|
|
[InlineData("##")]
|
|
|
|
|
[InlineData("ABCD")]
|
|
|
|
|
public void Symbol_WithoutEmoji_Passes(string plainSymbol)
|
|
|
|
|
{
|
|
|
|
|
var cmd = ValidCmd() with { Symbol = plainSymbol };
|
|
|
|
|
_validator.TestValidate(cmd).ShouldNotHaveValidationErrorFor(x => x.Symbol);
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-20 12:24:06 -03:00
|
|
|
// ── ValidFrom ────────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
[Fact]
|
|
|
|
|
public void ValidFrom_InPast_FailsValidation()
|
|
|
|
|
{
|
|
|
|
|
var cmd = ValidCmd() with { ValidFrom = Yesterday };
|
|
|
|
|
_validator.TestValidate(cmd).ShouldHaveValidationErrorFor(x => x.ValidFrom);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[Fact]
|
|
|
|
|
public void ValidFrom_Today_Passes()
|
|
|
|
|
{
|
|
|
|
|
var cmd = ValidCmd() with { ValidFrom = Today };
|
|
|
|
|
_validator.TestValidate(cmd).ShouldNotHaveValidationErrorFor(x => x.ValidFrom);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[Fact]
|
|
|
|
|
public void ValidFrom_Future_Passes()
|
|
|
|
|
{
|
|
|
|
|
var cmd = ValidCmd() with { ValidFrom = Tomorrow };
|
|
|
|
|
_validator.TestValidate(cmd).ShouldNotHaveValidationErrorFor(x => x.ValidFrom);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ── Happy path ────────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
[Fact]
|
|
|
|
|
public void ValidCommand_PassesAllRules()
|
|
|
|
|
{
|
|
|
|
|
_validator.TestValidate(ValidCmd()).ShouldNotHaveAnyValidationErrors();
|
|
|
|
|
}
|
|
|
|
|
}
|