food-market/tests/food-market.UnitTests/PosContractsTests.cs
nns e022db30aa feat(pos-shared): контракты POS v1 в food-market.shared (P1-12a)
Доступные DTO для оффлайн-кассы (food-market.pos):
• ProductSyncDto/PriceSyncDto/StockSyncDto/CounterpartySyncDto — выгрузка
  изменений для последующей пробивки;
• PosSyncResponse — конверт всего sync-ответа с ServerTime (reference
  time против клок-дрейфа кассы) и DeletedProductIds;
• PosSaleDto/PosSaleLineDto/PosSaleBatchDto — батч продаж от кассы.
  PosSaleBatchDto несёт IdempotencyKey + каждая продажа имеет ClientSaleId
  (двойная идемпотентность);
• PosSaleBatchResponse — Accepted/Failed + ReplayedFromCache флаг.

Версионирование на уровне namespace  — для v2 будет рядом без
breaking changes. Required-поля везде where applicable: компилятор обяжет
заполнить новые обязательные поля при появлении v1.X добавок.

Тесты: 3 unit на сериализационный round-trip (компиляция падёт при удалении
любого поля контракта — это и есть тест public API).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 12:03:04 +05:00

83 lines
2.8 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using System.Text.Json;
using FluentAssertions;
using foodmarket.Shared.Pos.V1;
using Xunit;
namespace foodmarket.UnitTests;
/// <summary>Контракт POS — public API: ломая его, мы ломаем все Windows-кассы
/// в поле. Тестируем сериализацию round-trip + наличие required-полей.
/// При попытке убрать поле или поменять тип компиляция этих тестов упадёт.</summary>
public class PosContractsTests
{
[Fact]
public void Product_sync_dto_round_trips()
{
var dto = new ProductSyncDto
{
Id = Guid.NewGuid(),
Name = "Молоко 1л",
Article = "M-001",
Barcodes = new[] { "1234567890123", "2345678901234" },
UnitCode = "л",
Packaging = 1,
VatPercent = 12m,
VatEnabled = true,
IsMarked = false,
IsArchived = false,
UpdatedAt = DateTime.UtcNow,
};
var json = JsonSerializer.Serialize(dto);
var back = JsonSerializer.Deserialize<ProductSyncDto>(json)!;
back.Should().BeEquivalentTo(dto);
}
[Fact]
public void Batch_dto_carries_idempotency_key()
{
var batch = new PosSaleBatchDto
{
IdempotencyKey = Guid.NewGuid(),
Sales = new[]
{
new PosSaleDto
{
ClientSaleId = Guid.NewGuid(),
OccurredAt = DateTime.UtcNow,
Payment = 0, PaidCash = 1000m, PaidCard = 0m,
Lines = new[]
{
new PosSaleLineDto
{
ProductId = Guid.NewGuid(), Quantity = 1m,
UnitPrice = 1000m, Discount = 0m, VatPercent = 12m,
},
},
},
},
};
var json = JsonSerializer.Serialize(batch);
var back = JsonSerializer.Deserialize<PosSaleBatchDto>(json)!;
back.IdempotencyKey.Should().Be(batch.IdempotencyKey);
back.Sales.Should().HaveCount(1);
back.Sales[0].Lines.Should().HaveCount(1);
}
[Fact]
public void Sync_response_collects_all_groups()
{
var resp = new PosSyncResponse
{
ServerTime = DateTime.UtcNow,
Products = Array.Empty<ProductSyncDto>(),
Prices = Array.Empty<PriceSyncDto>(),
Stocks = Array.Empty<StockSyncDto>(),
Counterparties = Array.Empty<CounterpartySyncDto>(),
DeletedProductIds = Array.Empty<Guid>(),
};
var json = JsonSerializer.Serialize(resp);
var back = JsonSerializer.Deserialize<PosSyncResponse>(json)!;
back.Should().BeEquivalentTo(resp);
}
}