namespace foodmarket.Shared.Pos.V1; // ────────────────────────────────────────────────────────────────────────────── // Контракты синхронизации сервер ↔ POS (Windows-касса food-market.pos). // Namespace `Pos.V1` — версионирование на уровне неймспейса: v2 рядом, без // breaking changes для v1. POS-клиент закрепляет за собой конкретную версию // через URL `/api/pos/v1/...` (плюс заголовок `X-Pos-Api-Version: 1` для дабл-чека). // // Все DTO — record'ы с required-полями: компилятор обяжет POS-разработчика // заполнить новые поля, если v1.X добавит обязательные. Опциональные поля — // nullable; default-значения избегаем чтобы baseline на проде совпадал с // баг-репортами «у меня поля null». // ────────────────────────────────────────────────────────────────────────────── /// Полная карточка товара для оффлайн-кассы. Включает только то, что /// нужно при пробивке: ID для FK, идентификаторы (имя/артикул/штрихкоды), /// единица измерения, упаковка (штучная/весовая/разливная), НДС и текущая /// розничная цена. Изображения и история — не передаём (вес sync-payload'а /// важен на медленных каналах KZ-периферии). public record ProductSyncDto { public required Guid Id { get; init; } public required string Name { get; init; } public string? Article { get; init; } /// Все штрихкоды товара (для поиска по сканеру). Первый — primary. public required IReadOnlyList Barcodes { get; init; } /// Код единицы измерения (шт., кг, л, м…), не FK — раскрывается на UI. public required string UnitCode { get; init; } /// Тип упаковки: 1=штучный, 2=весовой, 3=разливной (см. Domain.Packaging). public required int Packaging { get; init; } /// Явный признак весового товара (Packaging==2) — для мобильного/кассы, /// чтобы не завязываться на числовой код упаковки. public bool IsWeighed { get; init; } public required decimal VatPercent { get; init; } public required bool VatEnabled { get; init; } public required bool IsMarked { get; init; } /// true — товар архивный, на кассе не продаётся, оставлен для отчётов. public required bool IsArchived { get; init; } public DateTime UpdatedAt { get; init; } } /// Розничная цена товара. Один товар может иметь несколько цен /// (разные типы цен — обычная, для VIP, со скидкой); POS обычно использует /// «системный/главный» PriceType, его помечает IsSystem=true. public record PriceSyncDto { public required Guid ProductId { get; init; } public required Guid PriceTypeId { get; init; } public required string PriceTypeName { get; init; } public required bool IsSystem { get; init; } public required decimal Amount { get; init; } public required string CurrencyCode { get; init; } public DateTime UpdatedAt { get; init; } } /// Текущий остаток на момент запроса. Не агрегируется на POS, просто /// показывается кассиру для подсказки/предупреждения «осталось 2 шт». Никаких /// reserve/available — это серверный учёт; на кассе только on-hand. public record StockSyncDto { public required Guid ProductId { get; init; } public required Guid StoreId { get; init; } public required decimal Quantity { get; init; } public DateTime AsOf { get; init; } } /// Контрагент (только покупатели — поставщики POS не нужны). Используется /// для лояльности/привязки чека к клиенту: ввёл телефон → нашёлся контакт. public record CounterpartySyncDto { public required Guid Id { get; init; } public required string Name { get; init; } public string? Phone { get; init; } public string? Email { get; init; } public string? Bin { get; init; } public string? Iin { get; init; } public DateTime UpdatedAt { get; init; } } /// Ответ GET /api/pos/sync: все изменения после since. /// Клиент кеширует и шлёт её обратно в следующем /// since — серверная reference time, чтобы клок-дрейф кассы не пропустил /// записи. — id товаров, которые с last sync /// были архивированы (на POS нужно их пометить, не удалять историю продаж). public record PosSyncResponse { public required DateTime ServerTime { get; init; } public required IReadOnlyList Products { get; init; } public required IReadOnlyList Prices { get; init; } public required IReadOnlyList Stocks { get; init; } public required IReadOnlyList Counterparties { get; init; } public required IReadOnlyList DeletedProductIds { get; init; } } /// Одна продажа из оффлайн-кассы. POS пробил чек локально, теперь /// шлёт на сервер. ClientSaleId — Guid, который POS присвоил при /// пробитии; служит частью idempotency-ключа (если батч придёт повторно из-за /// сетевой ошибки, сервер вернёт прежний результат без двойного списания). /// Сумма к оплате уже посчитана POS'ом — сервер пересчитывает по своим /// ценам и сверяет; mismatch → строка попадает в `Failed` ответа. public record PosSaleDto { public required Guid ClientSaleId { get; init; } public required DateTime OccurredAt { get; init; } public Guid? CustomerId { get; init; } public Guid? CashierUserId { get; init; } public required int Payment { get; init; } // PaymentMethod enum int (0=Cash,1=Card,2=BankTransfer,3=Bonus,99=Mixed) public required decimal PaidCash { get; init; } public required decimal PaidCard { get; init; } public required IReadOnlyList Lines { get; init; } public string? Notes { get; init; } // --- Привязка к смене/точке (добавлено, обратносовместимо с v1: nullable). /// Точка продаж, к которой привязана касса. Если задана — сервер /// проставит её в чек и возьмёт склад точки по умолчанию. public Guid? RetailPointId { get; init; } /// Открытая смена кассира. Нужна, чтобы чек попал в Z-отчёт и в /// материализацию денег смены (MoneyService агрегирует продажи по CashShiftId). public Guid? CashShiftId { get; init; } /// true — это возврат покупателю (движение +остаток, наличные к выдаче). public bool IsReturn { get; init; } /// Чек-основание для возврата по чеку (если возврат по номеру/QR). public Guid? ReferenceSaleId { get; init; } } public record PosSaleLineDto { public required Guid ProductId { get; init; } public required decimal Quantity { get; init; } public required decimal UnitPrice { get; init; } public required decimal Discount { get; init; } public decimal VatPercent { get; init; } } /// Батч продаж POS → сервер. — Guid, /// POS должен использовать СТАБИЛЬНЫЙ ключ на тот же набор продаж: повтор /// батча с тем же ключом гарантирует один и тот же результат без дублей /// в БД. (Внутри батча уникальность отдельных продаж обеспечивается /// PosSaleDto.ClientSaleId — он тоже идемпотентен.) public record PosSaleBatchDto { public required Guid IdempotencyKey { get; init; } public required IReadOnlyList Sales { get; init; } } /// Ответ на загрузку батча. /// — продажи, которые сервер реально провёл (Status=Posted) /// или нашёл уже проведённый по ClientSaleId (идемпотентность). С каждой — /// идентификатор серверной RetailSale. /// — продажи, которые отклонены (mismatch цен, отсутствие /// товара, нехватка остатка для проведения). POS должен показать кассиру и /// решить (перепробить с новой ценой, списать как акт расхождения). /// Информация о последней версии кассы (автообновление + ссылка в админке). public record PosVersionDto { public required string Version { get; init; } public string? WindowsUrl { get; init; } public string? LinuxUrl { get; init; } public string? Notes { get; init; } public bool Mandatory { get; init; } } public record PosSaleBatchResponse { public required Guid IdempotencyKey { get; init; } public required IReadOnlyList Accepted { get; init; } public required IReadOnlyList Failed { get; init; } /// true — этот ответ построен по кешу прежнего батча с тем же /// ключом, реальной обработки не было. public required bool ReplayedFromCache { get; init; } } public record PosSaleAcceptedDto { public required Guid ClientSaleId { get; init; } public required Guid ServerSaleId { get; init; } public required string ServerSaleNumber { get; init; } } public record PosSaleFailedDto { public required Guid ClientSaleId { get; init; } public required string Error { get; init; } public string? Field { get; init; } } /// Чек-основание для возврата по номеру/QR: шапка + позиции с /// количеством, доступным к возврату (продано − уже возвращено). public record PosReturnLookupDto { public required Guid SaleId { get; init; } public required string Number { get; init; } public required DateTime Date { get; init; } public required int Payment { get; init; } // PaymentMethod исходного чека public required IReadOnlyList Lines { get; init; } } public record PosReturnLineDto { public required Guid ProductId { get; init; } public required string Name { get; init; } public required string UnitCode { get; init; } public required decimal UnitPrice { get; init; } public required decimal SoldQty { get; init; } public required decimal ReturnedQty { get; init; } public required decimal ReturnableQty { get; init; } // SoldQty − ReturnedQty }