|
Some checks failed
CI / Backend (.NET 8) (push) Waiting to run
CI / Web (React + Vite) (push) Waiting to run
CI / POS (WPF, Windows) (push) Waiting to run
Docker Web / Build + push Web (push) Waiting to run
Docker Web / Deploy Web on stage (push) Blocked by required conditions
Docker API / Build + push API (push) Has been cancelled
Docker API / Deploy API on stage (push) Has been cancelled
Domain:
- LoyaltyProgram { Type=Percentage|FixedAmount|PointsAccrual, Rate,
MinSubtotal, IsActive } — org-scoped.
- LoyaltyCard { ProgramId, CounterpartyId, CardNumber unique per org,
Balance, IsBlocked }.
- Promotion { Type=Percent|FixedDiscount, Value, Scope=All|ProductGroups|
Products, Code unique per org, period, ProductGroupIds/ProductIds (jsonb) }.
- RetailSale: LoyaltyCardId, LoyaltyBonusApplied, LoyaltyPointsAccrued,
PromotionId, PromotionCode (snapshot), PromotionDiscount.
EF:
- SalesConfigurations: indexes, FK Restrict, jsonb-converters для Guid-
списков Promotion (ValueComparer для change-tracker).
- Phase9b миграция: 3 таблицы + 6 колонок на retail_sales.
- RolePermissions: LoyaltyManage, PromotionsManage добавлены (попадают
в All() для Admin).
API:
- /api/loyalty/programs CRUD (Get/List/Create/Update/Delete; запрет delete
при существующих картах → 409).
- /api/loyalty/cards CRUD + /issue + /{id}/block + /{id}/unblock + /lookup
(POS использует при оплате — 404 если нет, 409 если blocked/inactive).
- /api/promotions CRUD; код уникален per org (БД-индекс + 23505 → 409).
- RetailSale.Create/Update: новые поля input.LoyaltyCardNumber +
input.PromotionCode. Метод ApplyLoyaltyAndPromotionAsync:
• Lookup карты, проверка active/blocked/MinSubtotal.
• Расчёт скидки или баллов в зависимости от Type.
• Lookup промокода, проверка периода/MinSaleAmount/scope.
• MatchingSubtotal для Scope=ProductGroups/Products считаем по
input.Lines (sale.Lines ещё пустой в этот момент).
• Финальный Total = Subtotal - DiscountTotal - LoyaltyBonusApplied
- PromotionDiscount, max(0).
- RetailSale.Post: начисление баллов на LoyaltyCard.Balance (внутри
транзакции, чтобы rollback не оставил orphan баллы).
UI:
- /loyalty/programs — list + create/edit modal с Type/Rate/MinSubtotal.
- /loyalty/cards — list + issue modal (Program select + AsyncSelect
counterparty + CardNumber).
- /promotions — list + create/edit modal (Type/Value/период/MinSaleAmount/Code).
- Sidebar: новый блок «Продажи» с пунктами Промокоды/Программы/Карты
(Admin-only).
- i18n: ru.json + en.json пополнены nav-ключами.
Тесты:
- LoyaltyFlowTests (3/3 ✓): percentage уменьшает Total на 10%, points-accrual
пополняет Balance после Post, multi-tenant lookup→404 чужой org.
- PromotionFlowTests (2/2 ✓): SALE20 уменьшает Total на 20%, невалидный
код→400 с понятной message и field=promotionCode.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|---|---|---|
| .claude | ||
| .forgejo/workflows | ||
| .github/workflows.disabled | ||
| assets/brand | ||
| deploy | ||
| docs | ||
| src | ||
| tests | ||
| .editorconfig | ||
| .gitattributes | ||
| .gitignore | ||
| .nvmrc | ||
| CLAUDE.md | ||
| Directory.Build.props | ||
| Directory.Packages.props | ||
| food-market.sln | ||
| global.json | ||
| README.md | ||
food-market
Аналог системы МойСклад для розничной торговли в Казахстане.
Состав системы
- Сервер (ASP.NET Core 8 + PostgreSQL) — мультитенантный API, web-админка на React
- Web-админка (React 18 + Vite + shadcn/ui) — управление магазином, справочниками, документами, отчётами
- Кассовая программа (WPF на .NET 8) — офлайн-работоспособная касса для Windows 10+, синхронизируется с сервером, работает с весами (Масса-К в первую очередь)
Структура репозитория
food-market/
├── src/
│ ├── food-market.domain/ # доменные сущности, enum'ы, события
│ ├── food-market.application/ # use cases (MediatR), DTO, интерфейсы
│ ├── food-market.infrastructure/ # EF Core, PostgreSQL, внешние сервисы
│ ├── food-market.api/ # ASP.NET Core + OpenIddict + SignalR
│ ├── food-market.web/ # React + Vite + shadcn/ui (SPA)
│ ├── food-market.shared/ # DTO-контракты сервер ↔ POS
│ ├── food-market.pos.core/ # логика POS (независима от UI)
│ └── food-market.pos/ # WPF + .NET 8 кассовая программа
├── tests/
├── deploy/
│ ├── docker-compose.yml # PostgreSQL для локальной разработки
│ └── Dockerfile.api
└── docs/
Именование
- Папки, проекты, csproj, docker-образы, URL — lowercase:
food-market,food-market.api - C# namespace —
foodmarket.Api,foodmarket.Domain(lowercase root; C# не допускает дефис в идентификаторах) - Отображаемое имя в UI — "Food Market"
Стек
Сервер
- .NET 8 LTS (до ноября 2026), ASP.NET Core Minimal APIs + Controllers
- EF Core 8 + Npgsql + PostgreSQL 16
- OpenIddict 5 (OAuth2/OIDC — password + refresh tokens)
- MediatR + FluentValidation (CQRS-lite)
- SignalR (real-time синхронизация)
- Hangfire (фоновые задачи)
- Serilog (структурированное логирование)
Web
- React 18 + Vite + TypeScript
- shadcn/ui + Tailwind CSS
- TanStack Query + TanStack Table
- AG Grid Community (для тяжёлых grid'ов)
- Recharts / Tremor (графики)
- react-hook-form + Zod
POS
- .NET 8 WPF, Windows 10+
- CommunityToolkit.Mvvm (source-generated MVVM)
- SQLite (локальная БД)
- Refit + Polly (API-клиент с retry)
- System.IO.Ports (драйверы весов: Масса-К и др.)
Мультитенантность
Каждая сущность имеет OrganizationId. Пользователь scoped к организации. EF Core query filter автоматически изолирует данные между тенантами.
Локальная разработка
# Поднять PostgreSQL
cd deploy && docker compose up -d
# Мигрировать БД
cd src/food-market.api && dotnet ef database update
# Запустить API
dotnet run --project src/food-market.api
# Запустить Web
cd src/food-market.web && pnpm install && pnpm dev
Статус
🚧 Phase 0: фундамент (scaffolding, auth, multi-tenancy)