Compare commits
38
Commits
2401991870
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
67f4925bd3 | ||
|
|
c23fa95640 | ||
|
|
69af054774 | ||
|
|
e176d0c211 | ||
|
|
9854133ac0 | ||
|
|
804ad617ad | ||
|
|
92a904b8a9 | ||
|
|
4b0225008c | ||
|
|
cca20a9965 | ||
|
|
fa1199539b | ||
|
|
f4f3b3896a | ||
|
|
ded596d872 | ||
|
|
127ca92f63 | ||
|
|
fd408b102b | ||
|
|
0c10489a5e | ||
|
|
03e2d66ca4 | ||
|
|
182c26d9f8 | ||
|
|
25ce901021 | ||
|
|
2bc083fb95 | ||
|
|
2676240440 | ||
|
|
bad37a6ab9 | ||
|
|
fee137aee6 | ||
|
|
cadc321d96 | ||
|
|
58cc2e4096 | ||
|
|
5d3be19ca8 | ||
|
|
c163db3029 | ||
|
|
dcaa1a77db | ||
|
|
482a4b41f0 | ||
|
|
24c9e582c9 | ||
|
|
40e07bc1a6 | ||
|
|
a9815b54e0 | ||
|
|
1cbf965657 | ||
|
|
b3756238a3 | ||
|
|
0a5ef715eb | ||
|
|
6df1027699 | ||
|
|
67ec85b6cc | ||
|
|
70fb592cf7 | ||
|
|
6c78351a95 |
@@ -0,0 +1,39 @@
|
||||
# TarninaHub — backend runtime configuration example.
|
||||
# Copy to `.env` (not tracked) or provide as environment variables.
|
||||
# Never commit real secrets.
|
||||
|
||||
ASPNETCORE_ENVIRONMENT=Production
|
||||
ASPNETCORE_URLS=http://+:8080
|
||||
|
||||
# Auth — JWT signing key (min. 32 chars). REQUIRED in Production; never commit a real value.
|
||||
# Auth__SigningKey=
|
||||
# Auth__Issuer=tarninahub
|
||||
# Auth__Audience=tarninahub
|
||||
# Auth__TokenLifetimeMinutes=60
|
||||
|
||||
# SurrealDB — embedded RocksDB by default.
|
||||
SurrealDb__Namespace=hubmi
|
||||
SurrealDb__Database=hubmi
|
||||
SurrealDb__DataDirectory=data
|
||||
|
||||
# SurrealDB — remote mode (optional; leave DataDirectory unused when set).
|
||||
# SurrealDb__RemoteUrl=ws://127.0.0.1:8010/rpc
|
||||
# SurrealDb__Username=
|
||||
# SurrealDb__Password=
|
||||
|
||||
# LLM (F-8, matchmaking) — runtime secret only.
|
||||
# LLM__ApiKey=
|
||||
|
||||
# LLM provider/cost control (optional; disabled by default -> offline template rationales).
|
||||
# Llm__Enabled=false
|
||||
# Llm__Provider=openai
|
||||
# Llm__BaseUrl=https://api.openai.com/v1
|
||||
# Llm__Model=gpt-4o-mini
|
||||
# Llm__MaxTokens=256
|
||||
# Llm__MaxRequestsPerMinute=30
|
||||
# Llm__TimeoutSeconds=20
|
||||
|
||||
# Observability (optional).
|
||||
# Logging__LogLevel__Default=Information
|
||||
# OTEL_EXPORTER_OTLP_ENDPOINT=
|
||||
# OTEL_SERVICE_NAME=tarninahub
|
||||
@@ -0,0 +1,50 @@
|
||||
# CD workflow — DISABLED.
|
||||
# No Gitea Actions runner is attached to this repository yet, so the workflow
|
||||
# below is fully commented out to avoid failing runs. Uncomment everything once
|
||||
# a runner is registered (repository Settings -> Actions -> Runners).
|
||||
|
||||
# name: CD
|
||||
#
|
||||
# on:
|
||||
# push:
|
||||
# branches: [main]
|
||||
# tags: ["v*"]
|
||||
#
|
||||
# jobs:
|
||||
# publish:
|
||||
# runs-on: ubuntu-latest
|
||||
# steps:
|
||||
# - uses: actions/checkout@v4
|
||||
# - name: Read version
|
||||
# id: version
|
||||
# run: |
|
||||
# VERSION=$(sed -n 's:.*<Version>\(.*\)</Version>.*:\1:p' backend/TarninaHub.csproj | head -1)
|
||||
# echo "version=$VERSION" >> "$GITHUB_OUTPUT"
|
||||
# - name: Log in to registry
|
||||
# run: echo "${{ secrets.GITEA_TOKEN }}" | docker login git.rtynski.pl -u "${{ github.actor }}" --password-stdin
|
||||
# - name: Build image
|
||||
# run: |
|
||||
# docker build \
|
||||
# --build-arg APP_VERSION=${{ steps.version.outputs.version }} \
|
||||
# -t git.rtynski.pl/rtynski/hy2026:${{ steps.version.outputs.version }} \
|
||||
# -t git.rtynski.pl/rtynski/hy2026:latest .
|
||||
# - name: Push image
|
||||
# run: |
|
||||
# docker push git.rtynski.pl/rtynski/hy2026:${{ steps.version.outputs.version }}
|
||||
# docker push git.rtynski.pl/rtynski/hy2026:latest
|
||||
#
|
||||
# # Deploy demo runs after the image is published from `main`.
|
||||
# # Configure DEPLOY_HOST / DEPLOY_USER / DEPLOY_SSH_KEY as CI secrets.
|
||||
# # deploy:
|
||||
# # needs: publish
|
||||
# # if: github.ref == 'refs/heads/main'
|
||||
# # runs-on: ubuntu-latest
|
||||
# # steps:
|
||||
# # - name: Deploy over SSH
|
||||
# # run: |
|
||||
# # echo "${{ secrets.DEPLOY_SSH_KEY }}" > key && chmod 600 key
|
||||
# # ssh -i key -o StrictHostKeyChecking=no \
|
||||
# # "${{ secrets.DEPLOY_USER }}@${{ secrets.DEPLOY_HOST }}" \
|
||||
# # "cd /opt/tarninahub && APP_VERSION=latest docker compose pull && docker compose up -d"
|
||||
# # - name: Smoke test
|
||||
# # run: curl -fsS "http://${{ secrets.DEPLOY_HOST }}:8075/api/health"
|
||||
@@ -0,0 +1,47 @@
|
||||
# CI workflow — DISABLED.
|
||||
# No Gitea Actions runner is attached to this repository yet, so the workflow
|
||||
# below is fully commented out to avoid failing runs. Uncomment everything once
|
||||
# a runner is registered (repository Settings -> Actions -> Runners).
|
||||
|
||||
# name: CI
|
||||
#
|
||||
# on:
|
||||
# push:
|
||||
# branches: [main]
|
||||
# pull_request:
|
||||
# branches: [main]
|
||||
#
|
||||
# jobs:
|
||||
# backend:
|
||||
# runs-on: ubuntu-latest
|
||||
# steps:
|
||||
# - uses: actions/checkout@v4
|
||||
# - uses: actions/setup-dotnet@v4
|
||||
# with:
|
||||
# dotnet-version: "10.0.x"
|
||||
# - name: Restore
|
||||
# run: dotnet restore backend/TarninaHub.csproj
|
||||
# - name: Build
|
||||
# run: dotnet build backend/TarninaHub.csproj --configuration Release --no-restore
|
||||
# - name: Format check
|
||||
# run: dotnet format whitespace --verify-no-changes TarninaHub.slnx
|
||||
# - name: Test
|
||||
# run: dotnet test TarninaHub.slnx --configuration Release --no-build
|
||||
#
|
||||
# frontend:
|
||||
# runs-on: ubuntu-latest
|
||||
# defaults:
|
||||
# run:
|
||||
# working-directory: client
|
||||
# steps:
|
||||
# - uses: actions/checkout@v4
|
||||
# - uses: actions/setup-node@v4
|
||||
# with:
|
||||
# node-version: "22"
|
||||
# cache: npm
|
||||
# cache-dependency-path: client/package-lock.json
|
||||
# - name: Install
|
||||
# run: npm ci
|
||||
# - name: Build (typecheck + bundle)
|
||||
# run: npm run build
|
||||
# # Lint: enabled once ESLint is wired up (`npm run lint`).
|
||||
@@ -35,3 +35,6 @@ backend/local-docs/
|
||||
# Docker — lokalne nadpisania
|
||||
docker-compose.override.yaml
|
||||
docker-compose.override.yml
|
||||
|
||||
# Artefakty narzędzi (np. historia REPL `surreal sql`)
|
||||
history.txt
|
||||
|
||||
@@ -12,6 +12,9 @@ RUN npm run build
|
||||
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
|
||||
WORKDIR /src
|
||||
COPY backend/TarninaHub.csproj ./backend/
|
||||
COPY backend/Domain/TarninaHub.Domain.csproj ./backend/Domain/
|
||||
COPY backend/Application/TarninaHub.Application.csproj ./backend/Application/
|
||||
COPY backend/Infrastructure/TarninaHub.Infrastructure.csproj ./backend/Infrastructure/
|
||||
RUN dotnet restore ./backend/TarninaHub.csproj
|
||||
COPY backend/ ./backend/
|
||||
COPY --from=client /src/client/dist ./backend/wwwroot/
|
||||
|
||||
@@ -19,24 +19,34 @@ Platforma Małopolskiego Hubu Innowacji Społecznych (MVP hackathonu).
|
||||
Windows nie kojarzy domyślnie plików `.ps1`, dlatego skrypty uruchamiaj przez
|
||||
`pwsh` (np. `pwsh ./scripts/docker-push.ps1`), a nie przez dwuklik / samo
|
||||
`./scripts/docker-push.ps1`. Instalacja: https://aka.ms/powershell
|
||||
(PowerShell 7.6.6: https://github.com/powershell/powershell/releases#release-v7.6.6)
|
||||
|
||||
## Praca lokalna
|
||||
|
||||
Backend (dev) nasłuchuje na `https://localhost:7080`.
|
||||
Backend (dev) domyślnie nasłuchuje na `http://localhost:5078` (profil `http`), a opcjonalnie na `https://localhost:7080` (profil `https`). Dev-serwer Vite działa po HTTPS na `https://localhost:7081` i przekierowuje `/api` do backendu (`http://localhost:5078`).
|
||||
|
||||
Certyfikat HTTPS dla Vite pochodzi z `dotnet dev-certs` — wygeneruj go raz (wymaga `pwsh`):
|
||||
|
||||
```pwsh
|
||||
# terminal 1 — backend
|
||||
pwsh ./scripts/dev-certs.ps1 # lub: npm --prefix client run certs
|
||||
```
|
||||
|
||||
```pwsh
|
||||
# terminal 1 — backend (profil http, port 5078)
|
||||
dotnet run --project backend
|
||||
|
||||
# terminal 2 — client (Vite, proxy /api -> http://localhost:5078)
|
||||
# terminal 2 — client (Vite, https://localhost:7081, proxy /api -> http://localhost:5078)
|
||||
npm --prefix client install
|
||||
npm --prefix client run dev
|
||||
```
|
||||
|
||||
Aby użyć profilu `https` backendu (port 7080), uruchom `dotnet run --project backend --launch-profile https` i zmień `proxy.target` w `client/vite.config.ts` na `https://localhost:7080`. Domyślna konfiguracja nie wymaga żadnych zmiennych środowiskowych.
|
||||
|
||||
## Skrypty
|
||||
|
||||
```pwsh
|
||||
npm --prefix client run dev # uruchamia Vite (dev server)
|
||||
npm --prefix client run dev # uruchamia Vite (dev server, HTTPS :7081)
|
||||
npm --prefix client run certs # eksport certyfikatu dev do client/.certs (dotnet dev-certs)
|
||||
npm --prefix client run build # buduje aplikację Vite do client/dist
|
||||
npm --prefix client run copy # kopiuje client/dist -> backend/wwwroot
|
||||
node scripts/build.mjs # buduje całość (client + copy + backend)
|
||||
@@ -84,12 +94,45 @@ kopiuje go do `backend/wwwroot`, buduje backend, buduje obraz Docker i wypycha g
|
||||
do `git.rtynski.pl/rtynski/hy2026` jako `<wersja>` oraz `latest`. Nadpisanie
|
||||
sprawdzenia: `-Force`.
|
||||
|
||||
## CI/CD
|
||||
|
||||
Pipeline działa w **Gitea Actions** (`.gitea/workflows/`):
|
||||
|
||||
> **Uwaga:** workflow są zakomentowane w całości — do repozytorium nie jest podpięty runner Gitea. Odkomentuj po zarejestrowaniu runera (Settings → Actions → Runners).
|
||||
|
||||
- `ci.yml` — na `push`/PR do `main`: build backendu (.NET 10), `dotnet format --verify-no-changes` oraz build klienta (Node 22, `vue-tsc` + bundle).
|
||||
- `cd.yml` — na `main`/tag `v*`: build obrazu i push do `git.rtynski.pl/rtynski/hy2026` (`<wersja>` + `latest`); deploy demo opcjonalnie przez sekrety CI.
|
||||
|
||||
Sekrety CI: `GITEA_TOKEN` (`write:package`), opcjonalnie `DEPLOY_HOST`/`DEPLOY_USER`/`DEPLOY_SSH_KEY`. Klucz `LLM__ApiKey` przekazujemy wyłącznie na runtime — nigdy do repo ani warstw obrazu.
|
||||
|
||||
## Środowiska i konfiguracja
|
||||
|
||||
- **dev** — `appsettings.Development.json`, dane w `backend/data`.
|
||||
- **demo** — obraz z rejestru, `docker-compose.yaml.example`, port `TARNINAHUB_PORT` (8075), wersja `APP_VERSION`.
|
||||
- **staging** (opcjonalne) — kandydat wydania przed demo.
|
||||
|
||||
Konfiguracja przez zmienne środowiskowe (`SurrealDb__*`, `LLM__ApiKey`, `OTEL_*`); szablon: [`.env.example`](.env.example).
|
||||
|
||||
## Observability
|
||||
|
||||
- Healthcheck: `GET /api/health` (używany też przez Docker healthcheck).
|
||||
- Logi strukturalne (JSON) do `stdout`/`stderr`, z correlation ID.
|
||||
- Docelowo: `/metrics` (Prometheus) i OpenTelemetry (traces).
|
||||
|
||||
## Struktura
|
||||
|
||||
```
|
||||
TarninaHub/
|
||||
├─ backend/ # ASP.NET Core Web API + wwwroot (build z client)
|
||||
├─ backend/ # ASP.NET Core Web API (API host) + wwwroot + migrations
|
||||
│ ├─ Controllers/ # warstwa API (kontrolery, DTO wejścia/wyjścia)
|
||||
│ ├─ Errors/ # globalna obsługa błędów (ProblemDetails)
|
||||
│ ├─ Configuration/ # konfiguracja Swagger/OpenAPI
|
||||
│ ├─ Domain/ # warstwa domeny (enumy, reguły)
|
||||
│ ├─ Application/ # DTO i abstrakcje (interfejsy)
|
||||
│ ├─ Infrastructure/ # SurrealDB, migracje, DI
|
||||
│ └─ migrations/ # *.surql (aplikowane przy starcie)
|
||||
├─ client/ # Vue 3 + TS + Vite
|
||||
├─ tests/TarninaHub.Tests/ # testy jednostkowe (xUnit)
|
||||
├─ scripts/
|
||||
│ ├─ build.mjs # build całości
|
||||
│ └─ copy-client.mjs # client/dist -> backend/wwwroot
|
||||
@@ -99,3 +142,12 @@ TarninaHub/
|
||||
├─ TarninaHub.slnx
|
||||
└─ README.md
|
||||
```
|
||||
|
||||
## API
|
||||
|
||||
- Swagger UI: `GET /swagger` (dokument per wersja, np. `/swagger/v1/swagger.json`).
|
||||
- Wersjonowanie: `?api-version=1.0` lub nagłówek `X-Api-Version` (domyślnie `1.0`).
|
||||
- Błędy: `ProblemDetails` (RFC 7807), globalny handler wyjątków.
|
||||
- Healthcheck: `GET /api/health` (bez wersji, dla infrastruktury).
|
||||
- Migracje SurrealQL są aplikowane automatycznie przy starcie (katalog `migrations/`).
|
||||
- Uwierzytelnianie: JWT Bearer (`POST /api/auth/signup`, `POST /api/auth/signin`), role `user`/`jst`/`operator`/`manager`/`admin` i polityki RBAC. Hasła (Argon2) i zgody w SurrealDB; różne poziomy dostępu (`by-consent`/`temporary`/`anonymized`) + rejestr odczytu (`audit_log`).
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
<Solution>
|
||||
<Folder Name="/backend/">
|
||||
<Project Path="backend/TarninaHub.csproj" />
|
||||
<Project Path="backend/Application/TarninaHub.Application.csproj" />
|
||||
<Project Path="backend/Domain/TarninaHub.Domain.csproj" />
|
||||
<Project Path="backend/Infrastructure/TarninaHub.Infrastructure.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/tests/">
|
||||
<Project Path="tests/TarninaHub.Tests/TarninaHub.Tests.csproj" />
|
||||
</Folder>
|
||||
</Solution>
|
||||
|
||||
+15
-2
@@ -1,6 +1,6 @@
|
||||
# AGENTS.md — Standardy warstwy Controller
|
||||
# AGENTS.md — Założenia i standardy backendu (C#)
|
||||
|
||||
Dotyczy projektu `backend` (ASP.NET Core Web API, .NET 10).
|
||||
Dotyczy projektu `backend` oraz testów `tests/TarninaHub.Tests` (ASP.NET Core Web API, .NET 10).
|
||||
|
||||
## Komunikaty commitów
|
||||
|
||||
@@ -8,6 +8,19 @@ Dotyczy projektu `backend` (ASP.NET Core Web API, .NET 10).
|
||||
- Zasada obowiązuje **w obu projektach**: repozytorium `DevHeyah` oraz `20_projects/TarninaHub`.
|
||||
- Stosujemy konwencję Conventional Commits (typ: `feat`, `fix`, `docs`, `chore` itd.).
|
||||
|
||||
## Założenia języka C#
|
||||
|
||||
- **Platforma:** .NET 10 (`net10.0`), `Nullable enable`, `ImplicitUsings enable`. Nie wyłączaj nullowalności; unikaj operatora `!` (jeśli musisz — uzasadnij).
|
||||
- **Jeden typ = jeden plik:** każdy typ najwyższego poziomu (klasa, `record`, interfejs, `enum`, `struct`) w osobnym pliku, nazwa pliku zgodna z nazwą typu. Nie zagnieżdżaj typów pomocniczych poza przypadkami prywatnych szczegółów implementacji.
|
||||
- **Przestrzeń nazw:** wyłącznie file-scoped (`namespace X;`). Kolejność: `using` → `namespace` → typy. Grupuj `using` bez zbędnych duplikatów.
|
||||
- **Typy:** klasy usług i kontrolerów oznaczaj `sealed`; modele wierszy i typy wewnętrzne jako `internal`. DTO to `public sealed record` (niemutowalne, pozycyjne).
|
||||
- **Zakaz map z wartościami `object`:** w DTO, interfejsach, `Domain` i modelach `*Row` nie stosuj `Dictionary<string, object>` ani `IReadOnlyDictionary<string, object>`. Elastyczne dane (`content`, `payload`, `contact`, `media`, `attachments`) to mapy wartości tekstowych (`IReadOnlyDictionary<string, string?>`), materializowane do konkretnych `Dictionary<string, string?>` przed przekazaniem do `RawQuery`. Powód: błąd deserializacji CBOR (Dahomey) i nieobsługiwana serializacja typów interfejsowych. Egzekwuje test `NoObjectValuedDictionariesTests`.
|
||||
- **Wzorce:** promuj `record`, pattern matching, wyrażenia (`=>`), kolekcje `[]`; unikaj `null`-owych kolekcji (inicjalizuj puste).
|
||||
- **Warstwy:** `Domain` (reguły, enumy), `Application` (DTO, abstrakcje, mapowania), `Infrastructure` (persistence, integracje), `Controllers` (tylko HTTP). Zależności kieruj do wewnątrz (Infrastructure → Application → Domain).
|
||||
- **Nazewnictwo:** typy/metody/właściwości `PascalCase`, parametry i zmienne lokalne `camelCase`, pola prywatne `_camelCase`, interfejsy `I...`, typy generyczne `T...`. Metody asynchroniczne z sufiksem `Async`.
|
||||
- **Formatowanie:** zgodnie z `.editorconfig` (C#: 4 spacje, LF, final newline, bez trailing whitespace). Weryfikacja: `dotnet format whitespace --verify-no-changes TarninaHub.slnx` i `dotnet format style --verify-no-changes --severity warn TarninaHub.slnx`.
|
||||
- **Zmienne:** preferuj `var`, gdy typ jest oczywisty; jawne typy przy niejasności.
|
||||
|
||||
## Język nazewnictwa
|
||||
|
||||
- Wszystkie nazwy zmiennych, parametrów, pól, metod, klas i plików są **wyłącznie w języku angielskim**.
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
using TarninaHub.Application.Dtos;
|
||||
|
||||
namespace TarninaHub.Application.Abstractions;
|
||||
|
||||
public interface IAdminService
|
||||
{
|
||||
Task<IReadOnlyList<SubmissionResponse>> ListSubmissionsAsync(
|
||||
string? status,
|
||||
string? challengeId,
|
||||
CancellationToken cancellationToken);
|
||||
|
||||
Task<KnowledgeItemResponse?> VerifyKnowledgeItemAsync(
|
||||
string knowledgeItemId,
|
||||
VerifyKnowledgeItemRequest request,
|
||||
CancellationToken cancellationToken);
|
||||
|
||||
Task<AdminMetricsResponse> GetMetricsAsync(CancellationToken cancellationToken);
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
using TarninaHub.Application.Dtos;
|
||||
|
||||
namespace TarninaHub.Application.Abstractions;
|
||||
|
||||
|
||||
public enum CreateApplicationOutcome
|
||||
{
|
||||
Created,
|
||||
CallNotFound,
|
||||
CallNotOpen,
|
||||
IdeaNotFound,
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
using TarninaHub.Application.Dtos;
|
||||
|
||||
namespace TarninaHub.Application.Abstractions;
|
||||
|
||||
|
||||
public sealed record CreateApplicationResult(
|
||||
CreateApplicationOutcome Outcome,
|
||||
ApplicationResponse? Application);
|
||||
@@ -0,0 +1,18 @@
|
||||
using TarninaHub.Application.Dtos;
|
||||
|
||||
namespace TarninaHub.Application.Abstractions;
|
||||
|
||||
|
||||
public interface IApplicationService
|
||||
{
|
||||
Task<CreateApplicationResult> CreateAsync(
|
||||
string userId,
|
||||
CreateApplicationRequest request,
|
||||
CancellationToken cancellationToken);
|
||||
|
||||
Task<ApplicationResponse?> GetAsync(string applicationId, CancellationToken cancellationToken);
|
||||
|
||||
Task<IReadOnlyList<ApplicationResponse>> ListForAuthorAsync(
|
||||
string authorId,
|
||||
CancellationToken cancellationToken);
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
namespace TarninaHub.Application.Abstractions;
|
||||
|
||||
public interface IAuditLogService
|
||||
{
|
||||
Task RecordAsync(
|
||||
string? actorId,
|
||||
string action,
|
||||
string target,
|
||||
IReadOnlyDictionary<string, string?> context,
|
||||
CancellationToken cancellationToken);
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
using TarninaHub.Application.Dtos;
|
||||
|
||||
namespace TarninaHub.Application.Abstractions;
|
||||
|
||||
public interface IChallengeService
|
||||
{
|
||||
Task<IReadOnlyList<SocialChallengeResponse>> ListAsync(CancellationToken cancellationToken);
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
using TarninaHub.Application.Dtos;
|
||||
|
||||
namespace TarninaHub.Application.Abstractions;
|
||||
|
||||
public interface IConsentService
|
||||
{
|
||||
Task<ConsentResponse> GrantAsync(
|
||||
string userId,
|
||||
GrantConsentRequest request,
|
||||
CancellationToken cancellationToken);
|
||||
|
||||
Task<IReadOnlyList<ConsentResponse>> ListForUserAsync(
|
||||
string userId,
|
||||
CancellationToken cancellationToken);
|
||||
|
||||
Task<bool> RevokeAsync(
|
||||
string consentId,
|
||||
string userId,
|
||||
CancellationToken cancellationToken);
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
using TarninaHub.Application.Dtos;
|
||||
|
||||
namespace TarninaHub.Application.Abstractions;
|
||||
|
||||
|
||||
public interface IGrantCallService
|
||||
{
|
||||
Task<IReadOnlyList<GrantCallResponse>> ListAsync(string? status, CancellationToken cancellationToken);
|
||||
|
||||
Task<GrantCallResponse?> GetAsync(string grantCallId, CancellationToken cancellationToken);
|
||||
|
||||
Task<GrantCallResponse?> UpdateStatusAsync(
|
||||
string grantCallId,
|
||||
string status,
|
||||
CancellationToken cancellationToken);
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
using TarninaHub.Application.Dtos;
|
||||
|
||||
namespace TarninaHub.Application.Abstractions;
|
||||
|
||||
|
||||
public interface IIdeaAssistant
|
||||
{
|
||||
Task<IdeaAssistResponse?> AssistAsync(
|
||||
string ideaId,
|
||||
IdeaAssistRequest request,
|
||||
CancellationToken cancellationToken);
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
using TarninaHub.Application.Dtos;
|
||||
|
||||
namespace TarninaHub.Application.Abstractions;
|
||||
|
||||
|
||||
public interface IIdeaService
|
||||
{
|
||||
Task<IdeaResponse> CreateAsync(string authorId, CreateIdeaRequest request, CancellationToken cancellationToken);
|
||||
|
||||
Task<IdeaResponse?> GetAsync(string ideaId, CancellationToken cancellationToken);
|
||||
|
||||
Task<IReadOnlyList<IdeaResponse>> ListForAuthorAsync(string authorId, CancellationToken cancellationToken);
|
||||
|
||||
Task<IReadOnlyList<string>> ListDistinctAuthorIdsAsync(CancellationToken cancellationToken);
|
||||
|
||||
Task<IdeaResponse?> UpdateAsync(string ideaId, UpdateIdeaRequest request, CancellationToken cancellationToken);
|
||||
|
||||
Task<IdeaCardResponse?> GetCardAsync(string ideaId, CancellationToken cancellationToken);
|
||||
|
||||
Task<IdeaCardResponse?> UpdateCardAsync(string ideaId, IdeaCardRequest request, CancellationToken cancellationToken);
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
using TarninaHub.Application.Dtos;
|
||||
|
||||
namespace TarninaHub.Application.Abstractions;
|
||||
|
||||
public interface IInnovationService
|
||||
{
|
||||
Task<IReadOnlyList<InnovationResponse>> ListAsync(
|
||||
string? challengeId,
|
||||
string? stage,
|
||||
string? query,
|
||||
bool onlyPublished,
|
||||
CancellationToken cancellationToken);
|
||||
|
||||
Task<InnovationResponse?> GetAsync(string innovationId, CancellationToken cancellationToken);
|
||||
|
||||
Task<InnovationResponse> CreateAsync(InnovationRequest request, CancellationToken cancellationToken);
|
||||
|
||||
Task<InnovationResponse?> UpdateAsync(
|
||||
string innovationId,
|
||||
InnovationRequest request,
|
||||
CancellationToken cancellationToken);
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
using TarninaHub.Application.Dtos;
|
||||
|
||||
namespace TarninaHub.Application.Abstractions;
|
||||
|
||||
public interface IKnowledgeItemService
|
||||
{
|
||||
Task<IReadOnlyList<KnowledgeItemResponse>> ListAsync(
|
||||
string? kind,
|
||||
string? challengeId,
|
||||
string? query,
|
||||
bool onlyPublished,
|
||||
CancellationToken cancellationToken);
|
||||
|
||||
Task<KnowledgeItemResponse?> GetAsync(string knowledgeItemId, CancellationToken cancellationToken);
|
||||
|
||||
Task<KnowledgeItemResponse> CreateAsync(KnowledgeItemRequest request, CancellationToken cancellationToken);
|
||||
|
||||
Task<KnowledgeItemResponse?> UpdateAsync(
|
||||
string knowledgeItemId,
|
||||
KnowledgeItemRequest request,
|
||||
CancellationToken cancellationToken);
|
||||
|
||||
Task<KnowledgeItemResponse?> PublishAsync(string knowledgeItemId, CancellationToken cancellationToken);
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
using TarninaHub.Application.Dtos;
|
||||
|
||||
namespace TarninaHub.Application.Abstractions;
|
||||
|
||||
public interface IMatchmakingService
|
||||
{
|
||||
Task IndexInnovationAsync(string innovationId, CancellationToken cancellationToken);
|
||||
|
||||
Task IndexProblemAsync(string problemId, CancellationToken cancellationToken);
|
||||
|
||||
Task IndexSubmissionAsync(string submissionId, CancellationToken cancellationToken);
|
||||
|
||||
Task<int> ReindexAsync(CancellationToken cancellationToken);
|
||||
|
||||
Task<MatchResponse?> MatchAsync(
|
||||
string submissionId,
|
||||
MatchRequest request,
|
||||
CancellationToken cancellationToken);
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace TarninaHub.Application.Abstractions;
|
||||
|
||||
public interface IMigrationRunner
|
||||
{
|
||||
Task ApplyAsync(CancellationToken cancellationToken);
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
using TarninaHub.Application.Dtos;
|
||||
|
||||
namespace TarninaHub.Application.Abstractions;
|
||||
|
||||
|
||||
public interface INotificationService
|
||||
{
|
||||
Task NotifyStaffAsync(
|
||||
string type,
|
||||
IReadOnlyDictionary<string, string?> payload,
|
||||
CancellationToken cancellationToken);
|
||||
|
||||
Task NotifyUserAsync(
|
||||
string userId,
|
||||
string type,
|
||||
IReadOnlyDictionary<string, string?> payload,
|
||||
CancellationToken cancellationToken);
|
||||
|
||||
Task<IReadOnlyList<NotificationResponse>> ListForUserAsync(
|
||||
string userId,
|
||||
CancellationToken cancellationToken);
|
||||
|
||||
Task<NotificationResponse?> MarkReadAsync(
|
||||
string notificationId,
|
||||
string userId,
|
||||
CancellationToken cancellationToken);
|
||||
}
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
using TarninaHub.Models;
|
||||
using TarninaHub.Application.Dtos;
|
||||
|
||||
namespace TarninaHub.Services;
|
||||
namespace TarninaHub.Application.Abstractions;
|
||||
|
||||
public interface IStorageService
|
||||
{
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace TarninaHub.Application.Abstractions;
|
||||
|
||||
|
||||
public interface ISubmissionEventPublisher
|
||||
{
|
||||
Task PublishAsync(SubmissionEvent submissionEvent, CancellationToken cancellationToken);
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
using TarninaHub.Application.Dtos;
|
||||
|
||||
namespace TarninaHub.Application.Abstractions;
|
||||
|
||||
public interface ISubmissionService
|
||||
{
|
||||
Task<SubmissionResponse> CreateAsync(
|
||||
string authorId,
|
||||
CreateSubmissionRequest request,
|
||||
CancellationToken cancellationToken);
|
||||
|
||||
Task<PagedResponse<SubmissionResponse>> ListAsync(
|
||||
string viewerId,
|
||||
bool isStaff,
|
||||
string? status,
|
||||
int page,
|
||||
int pageSize,
|
||||
CancellationToken cancellationToken);
|
||||
|
||||
Task<SubmissionResponse?> GetAsync(
|
||||
string submissionId,
|
||||
CancellationToken cancellationToken);
|
||||
|
||||
Task<SubmissionResponse?> UpdateStatusAsync(
|
||||
string submissionId,
|
||||
string newStatus,
|
||||
CancellationToken cancellationToken);
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
using TarninaHub.Application.Dtos;
|
||||
|
||||
namespace TarninaHub.Application.Abstractions;
|
||||
|
||||
|
||||
public interface IThreadService
|
||||
{
|
||||
Task<IReadOnlyList<ThreadResponse>> ListForUserAsync(
|
||||
string userId,
|
||||
bool isStaff,
|
||||
CancellationToken cancellationToken);
|
||||
|
||||
Task<ThreadResponse?> GetAsync(
|
||||
string threadId,
|
||||
string userId,
|
||||
bool isStaff,
|
||||
CancellationToken cancellationToken);
|
||||
|
||||
Task<ThreadResponse> CreateAsync(
|
||||
string creatorId,
|
||||
CreateThreadRequest request,
|
||||
CancellationToken cancellationToken);
|
||||
|
||||
Task<MessageResponse?> AddMessageAsync(
|
||||
string threadId,
|
||||
string authorId,
|
||||
bool isStaff,
|
||||
CreateMessageRequest request,
|
||||
CancellationToken cancellationToken);
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
using TarninaHub.Application.Models;
|
||||
|
||||
namespace TarninaHub.Application.Abstractions;
|
||||
|
||||
|
||||
public interface ITokenService
|
||||
{
|
||||
TokenResult CreateToken(UserAccount user);
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
using TarninaHub.Application.Dtos;
|
||||
|
||||
namespace TarninaHub.Application.Abstractions;
|
||||
|
||||
public interface ITrendService
|
||||
{
|
||||
Task<TrendsResponse> GetAsync(
|
||||
string? challengeId,
|
||||
DateTimeOffset from,
|
||||
DateTimeOffset to,
|
||||
CancellationToken cancellationToken);
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
using TarninaHub.Application.Dtos;
|
||||
using TarninaHub.Application.Models;
|
||||
|
||||
namespace TarninaHub.Application.Abstractions;
|
||||
|
||||
public interface IUserService
|
||||
{
|
||||
Task<UserAccount?> FindByPhoneAsync(string phone, CancellationToken cancellationToken);
|
||||
|
||||
Task<UserAccount?> GetByIdAsync(string userId, CancellationToken cancellationToken);
|
||||
|
||||
Task<UserAccount?> CreateAsync(RegisterRequest request, string role, CancellationToken cancellationToken);
|
||||
|
||||
Task<bool> VerifyCredentialsAsync(string phone, string password, CancellationToken cancellationToken);
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
using TarninaHub.Application.Dtos;
|
||||
|
||||
namespace TarninaHub.Application.Abstractions;
|
||||
|
||||
public interface IMiddlemanService
|
||||
{
|
||||
Task<MiddlemanResponse?> AdaptAsync(MiddlemanRequest request, CancellationToken cancellationToken);
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
using TarninaHub.Application.Dtos;
|
||||
|
||||
namespace TarninaHub.Application.Abstractions;
|
||||
|
||||
public interface IPartnershipService
|
||||
{
|
||||
Task<IReadOnlyList<PartnershipResponse>> ListForUserAsync(
|
||||
string userId,
|
||||
bool isStaff,
|
||||
CancellationToken cancellationToken);
|
||||
|
||||
Task<PartnershipResponse> CreateAsync(
|
||||
string initiatorId,
|
||||
CreatePartnershipRequest request,
|
||||
CancellationToken cancellationToken);
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace TarninaHub.Application.Abstractions;
|
||||
|
||||
|
||||
public sealed record SubmissionEvent(
|
||||
string Type,
|
||||
string SubmissionId,
|
||||
string? ActorId,
|
||||
string Status,
|
||||
DateTimeOffset At);
|
||||
@@ -0,0 +1,16 @@
|
||||
using TarninaHub.Application.Dtos;
|
||||
|
||||
namespace TarninaHub.Application.Abstractions;
|
||||
|
||||
public interface ITestFeedbackService
|
||||
{
|
||||
Task<TestFeedbackResponse?> CreateAsync(
|
||||
string innovationId,
|
||||
string authorId,
|
||||
CreateTestFeedbackRequest request,
|
||||
CancellationToken cancellationToken);
|
||||
|
||||
Task<IReadOnlyList<TestFeedbackResponse>> ListForInnovationAsync(
|
||||
string innovationId,
|
||||
CancellationToken cancellationToken);
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
using TarninaHub.Application.Models;
|
||||
|
||||
namespace TarninaHub.Application.Abstractions;
|
||||
|
||||
|
||||
public sealed record TokenResult(string AccessToken, DateTimeOffset ExpiresAt);
|
||||
@@ -0,0 +1,14 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace TarninaHub.Application.Dtos;
|
||||
|
||||
|
||||
public sealed record AdminMetricsResponse(
|
||||
int Submissions,
|
||||
int Problems,
|
||||
int Ideas,
|
||||
int Innovations,
|
||||
int KnowledgeItems,
|
||||
int Users,
|
||||
int OpenGrantCalls,
|
||||
int Applications);
|
||||
@@ -0,0 +1,12 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace TarninaHub.Application.Dtos;
|
||||
|
||||
|
||||
public sealed record ApplicationResponse(
|
||||
string Id,
|
||||
string GrantCallId,
|
||||
string IdeaId,
|
||||
IReadOnlyDictionary<string, string?> Content,
|
||||
string Status,
|
||||
DateTimeOffset CreatedAt);
|
||||
@@ -0,0 +1,10 @@
|
||||
namespace TarninaHub.Application.Dtos;
|
||||
|
||||
|
||||
public sealed record ConsentResponse(
|
||||
string Id,
|
||||
string UserId,
|
||||
string Scope,
|
||||
DateTimeOffset GrantedAt,
|
||||
DateTimeOffset? ExpiresAt,
|
||||
DateTimeOffset? RevokedAt);
|
||||
@@ -0,0 +1,9 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace TarninaHub.Application.Dtos;
|
||||
|
||||
|
||||
public sealed record CreateApplicationRequest(
|
||||
[Required] string GrantCallId,
|
||||
[Required] string IdeaId,
|
||||
IReadOnlyDictionary<string, string?>? Content);
|
||||
@@ -0,0 +1,9 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace TarninaHub.Application.Dtos;
|
||||
|
||||
|
||||
public sealed record CreateIdeaRequest(
|
||||
[Required, StringLength(200, MinimumLength = 3)] string Title,
|
||||
[Required, StringLength(4000, MinimumLength = 10)] string Description,
|
||||
IdeaCardRequest? Card);
|
||||
@@ -0,0 +1,7 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace TarninaHub.Application.Dtos;
|
||||
|
||||
|
||||
public sealed record CreateMessageRequest(
|
||||
[Required, StringLength(4000, MinimumLength = 1)] string Body);
|
||||
@@ -0,0 +1,8 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace TarninaHub.Application.Dtos;
|
||||
|
||||
|
||||
public sealed record CreatePartnershipRequest(
|
||||
[Required, StringLength(200, MinimumLength = 3)] string Subject,
|
||||
IReadOnlyList<string>? Partners);
|
||||
@@ -0,0 +1,11 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace TarninaHub.Application.Dtos;
|
||||
|
||||
|
||||
public sealed record CreateSubmissionRequest(
|
||||
[Required, StringLength(200, MinimumLength = 3)] string Title,
|
||||
[Required, StringLength(4000, MinimumLength = 10)] string Description,
|
||||
[Required] string ChallengeId,
|
||||
[Required] string LocationId,
|
||||
IReadOnlyList<string>? Keywords);
|
||||
@@ -0,0 +1,9 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace TarninaHub.Application.Dtos;
|
||||
|
||||
|
||||
public sealed record CreateTestFeedbackRequest(
|
||||
[Range(1, 5)] int Rating,
|
||||
string? Comment,
|
||||
string? Improvements);
|
||||
@@ -0,0 +1,8 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace TarninaHub.Application.Dtos;
|
||||
|
||||
|
||||
public sealed record CreateThreadRequest(
|
||||
[Required, StringLength(200, MinimumLength = 3)] string Subject,
|
||||
IReadOnlyList<string>? Participants);
|
||||
@@ -0,0 +1,12 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace TarninaHub.Application.Dtos;
|
||||
|
||||
|
||||
public sealed record GrantCallResponse(
|
||||
string Id,
|
||||
string Name,
|
||||
DateTimeOffset OpensAt,
|
||||
DateTimeOffset ClosesAt,
|
||||
string Status,
|
||||
string? ExternalId);
|
||||
@@ -0,0 +1,4 @@
|
||||
namespace TarninaHub.Application.Dtos;
|
||||
|
||||
|
||||
public sealed record GrantConsentRequest(string Scope, DateTimeOffset? ExpiresAt);
|
||||
@@ -0,0 +1,6 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace TarninaHub.Application.Dtos;
|
||||
|
||||
|
||||
public sealed record IdeaAssistRequest(string? Focus);
|
||||
@@ -0,0 +1,9 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace TarninaHub.Application.Dtos;
|
||||
|
||||
|
||||
public sealed record IdeaAssistResponse(
|
||||
IReadOnlyList<string> Suggestions,
|
||||
string ImprovedDescription,
|
||||
bool FromLlm);
|
||||
@@ -0,0 +1,9 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace TarninaHub.Application.Dtos;
|
||||
|
||||
|
||||
public sealed record IdeaCardRequest(
|
||||
string? Essence,
|
||||
string? Audience,
|
||||
string? Stage);
|
||||
@@ -0,0 +1,11 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace TarninaHub.Application.Dtos;
|
||||
|
||||
|
||||
public sealed record IdeaCardResponse(
|
||||
string Id,
|
||||
string IdeaId,
|
||||
string Essence,
|
||||
string Audience,
|
||||
string Stage);
|
||||
@@ -0,0 +1,13 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace TarninaHub.Application.Dtos;
|
||||
|
||||
|
||||
public sealed record IdeaResponse(
|
||||
string Id,
|
||||
string Title,
|
||||
string Description,
|
||||
string AuthorId,
|
||||
string Status,
|
||||
IdeaCardResponse? Card,
|
||||
DateTimeOffset? UpdatedAt);
|
||||
@@ -0,0 +1,15 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace TarninaHub.Application.Dtos;
|
||||
|
||||
|
||||
public sealed record InnovationRequest(
|
||||
[Required, StringLength(200, MinimumLength = 3)] string Title,
|
||||
[StringLength(2000)] string? Summary,
|
||||
[StringLength(8000)] string? Description,
|
||||
[Required] string ChallengeId,
|
||||
[Required] string Stage,
|
||||
string? Cost,
|
||||
IReadOnlyDictionary<string, string?>? Contact,
|
||||
IReadOnlyList<IReadOnlyDictionary<string, string?>>? Media,
|
||||
bool Published);
|
||||
@@ -0,0 +1,17 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace TarninaHub.Application.Dtos;
|
||||
|
||||
|
||||
public sealed record InnovationResponse(
|
||||
string Id,
|
||||
string Title,
|
||||
string Summary,
|
||||
string Description,
|
||||
string ChallengeId,
|
||||
string Stage,
|
||||
string? Cost,
|
||||
IReadOnlyDictionary<string, string?>? Contact,
|
||||
IReadOnlyList<IReadOnlyDictionary<string, string?>> Media,
|
||||
bool Published,
|
||||
DateTimeOffset? UpdatedAt);
|
||||
@@ -0,0 +1,9 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace TarninaHub.Application.Dtos;
|
||||
|
||||
|
||||
public sealed record InstitutionProfile(
|
||||
string? Type,
|
||||
string? Area,
|
||||
IReadOnlyList<string>? Audiences);
|
||||
@@ -0,0 +1,12 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace TarninaHub.Application.Dtos;
|
||||
|
||||
|
||||
public sealed record KnowledgeItemRequest(
|
||||
[Required, StringLength(200, MinimumLength = 3)] string Title,
|
||||
[Required] string Kind,
|
||||
string? ChallengeId,
|
||||
string? SourceUrl,
|
||||
IReadOnlyList<IReadOnlyDictionary<string, string?>>? Attachments,
|
||||
bool Published);
|
||||
@@ -0,0 +1,14 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace TarninaHub.Application.Dtos;
|
||||
|
||||
|
||||
public sealed record KnowledgeItemResponse(
|
||||
string Id,
|
||||
string Title,
|
||||
string Kind,
|
||||
string? ChallengeId,
|
||||
string? SourceUrl,
|
||||
IReadOnlyList<IReadOnlyDictionary<string, string?>> Attachments,
|
||||
bool Published,
|
||||
DateTimeOffset? UpdatedAt);
|
||||
@@ -0,0 +1,4 @@
|
||||
namespace TarninaHub.Application.Dtos;
|
||||
|
||||
|
||||
public sealed record LoginRequest(string Phone, string Password);
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace TarninaHub.Application.Dtos;
|
||||
|
||||
|
||||
public sealed record MatchRequest(
|
||||
int? MaxResults,
|
||||
string? ChallengeId);
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace TarninaHub.Application.Dtos;
|
||||
|
||||
|
||||
public sealed record MatchResponse(
|
||||
IReadOnlyList<SimilarCaseItem> SimilarCases,
|
||||
IReadOnlyList<SolutionItem> Solutions,
|
||||
bool Fallback);
|
||||
@@ -0,0 +1,11 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace TarninaHub.Application.Dtos;
|
||||
|
||||
|
||||
public sealed record MessageResponse(
|
||||
string Id,
|
||||
string ThreadId,
|
||||
string AuthorId,
|
||||
string Body,
|
||||
DateTimeOffset CreatedAt);
|
||||
@@ -0,0 +1,9 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace TarninaHub.Application.Dtos;
|
||||
|
||||
|
||||
public sealed record MiddlemanRequest(
|
||||
[Required] string InnovationId,
|
||||
InstitutionProfile? InstitutionProfile,
|
||||
string? Needs);
|
||||
@@ -0,0 +1,10 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace TarninaHub.Application.Dtos;
|
||||
|
||||
|
||||
public sealed record MiddlemanResponse(
|
||||
string ServiceForm,
|
||||
IReadOnlyList<string> RequiredResources,
|
||||
string Rationale,
|
||||
bool FromLlm);
|
||||
@@ -0,0 +1,11 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace TarninaHub.Application.Dtos;
|
||||
|
||||
|
||||
public sealed record NotificationResponse(
|
||||
string Id,
|
||||
string Type,
|
||||
IReadOnlyDictionary<string, string?> Payload,
|
||||
DateTimeOffset? ReadAt,
|
||||
DateTimeOffset CreatedAt);
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace TarninaHub.Application.Dtos;
|
||||
|
||||
public sealed record PagedResponse<T>(
|
||||
IReadOnlyList<T> Items,
|
||||
int Page,
|
||||
int PageSize,
|
||||
int Total);
|
||||
@@ -0,0 +1,11 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace TarninaHub.Application.Dtos;
|
||||
|
||||
|
||||
public sealed record PartnershipResponse(
|
||||
string Id,
|
||||
string InitiatorId,
|
||||
string Subject,
|
||||
IReadOnlyList<string> Partners,
|
||||
string Status);
|
||||
@@ -0,0 +1,4 @@
|
||||
namespace TarninaHub.Application.Dtos;
|
||||
|
||||
|
||||
public sealed record RegisterRequest(string Phone, string? Email, string Password);
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace TarninaHub.Application.Dtos;
|
||||
|
||||
|
||||
public sealed record SimilarCaseItem(
|
||||
string Id,
|
||||
string Title,
|
||||
double Score);
|
||||
@@ -0,0 +1,10 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace TarninaHub.Application.Dtos;
|
||||
|
||||
|
||||
public sealed record SocialChallengeResponse(
|
||||
string Id,
|
||||
string Name,
|
||||
string Description,
|
||||
int Order);
|
||||
@@ -0,0 +1,10 @@
|
||||
namespace TarninaHub.Application.Dtos;
|
||||
|
||||
|
||||
public sealed record SolutionItem(
|
||||
string InnovationId,
|
||||
string Title,
|
||||
string Stage,
|
||||
string ChallengeId,
|
||||
double Score,
|
||||
string Rationale);
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace TarninaHub.Application.Dtos;
|
||||
|
||||
|
||||
public sealed record StorageInfo(
|
||||
string Engine,
|
||||
string DataDirectory,
|
||||
string Namespace,
|
||||
string Database);
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace TarninaHub.Application.Dtos;
|
||||
|
||||
|
||||
public sealed class StorageProbe
|
||||
{
|
||||
public string Label { get; set; } = "probe";
|
||||
|
||||
public DateTimeOffset CreatedAt { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace TarninaHub.Application.Dtos;
|
||||
|
||||
|
||||
public sealed record StorageProbeResponse(
|
||||
string Label,
|
||||
DateTimeOffset CreatedAt);
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace TarninaHub.Application.Dtos;
|
||||
|
||||
|
||||
public sealed record StorageStatusResponse(
|
||||
string Engine,
|
||||
string DataDirectory,
|
||||
string Namespace,
|
||||
string Database,
|
||||
int ProbeCount);
|
||||
@@ -0,0 +1,17 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace TarninaHub.Application.Dtos;
|
||||
|
||||
|
||||
public sealed record SubmissionResponse(
|
||||
string Id,
|
||||
string ProblemId,
|
||||
string Title,
|
||||
string Description,
|
||||
string ChallengeId,
|
||||
string LocationId,
|
||||
string AuthorId,
|
||||
string Status,
|
||||
IReadOnlyList<string> Keywords,
|
||||
DateTimeOffset CreatedAt,
|
||||
DateTimeOffset? UpdatedAt);
|
||||
@@ -0,0 +1,13 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace TarninaHub.Application.Dtos;
|
||||
|
||||
|
||||
public sealed record TestFeedbackResponse(
|
||||
string Id,
|
||||
string InnovationId,
|
||||
string AuthorId,
|
||||
int Rating,
|
||||
string Comment,
|
||||
string Improvements,
|
||||
DateTimeOffset CreatedAt);
|
||||
@@ -0,0 +1,11 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace TarninaHub.Application.Dtos;
|
||||
|
||||
|
||||
public sealed record ThreadResponse(
|
||||
string Id,
|
||||
string Subject,
|
||||
IReadOnlyList<string> Participants,
|
||||
DateTimeOffset CreatedAt,
|
||||
IReadOnlyList<MessageResponse> Messages);
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace TarninaHub.Application.Dtos;
|
||||
|
||||
|
||||
public sealed record TokenResponse(
|
||||
string AccessToken,
|
||||
string TokenType,
|
||||
DateTimeOffset ExpiresAt,
|
||||
UserResponse User);
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace TarninaHub.Application.Dtos;
|
||||
|
||||
|
||||
public sealed record TrendPoint(
|
||||
string ChallengeId,
|
||||
string ChallengeName,
|
||||
int SubmissionCount);
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace TarninaHub.Application.Dtos;
|
||||
|
||||
|
||||
public sealed record TrendsResponse(
|
||||
DateTimeOffset From,
|
||||
DateTimeOffset To,
|
||||
IReadOnlyList<TrendPoint> Points);
|
||||
@@ -0,0 +1,8 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace TarninaHub.Application.Dtos;
|
||||
|
||||
|
||||
public sealed record UpdateApplicationRequest(
|
||||
IReadOnlyDictionary<string, string?>? Content,
|
||||
string? Status);
|
||||
@@ -0,0 +1,6 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace TarninaHub.Application.Dtos;
|
||||
|
||||
|
||||
public sealed record UpdateGrantCallStatusRequest([Required] string Status);
|
||||
@@ -0,0 +1,9 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace TarninaHub.Application.Dtos;
|
||||
|
||||
|
||||
public sealed record UpdateIdeaRequest(
|
||||
[Required, StringLength(200, MinimumLength = 3)] string Title,
|
||||
[Required, StringLength(4000, MinimumLength = 10)] string Description,
|
||||
string? Status);
|
||||
@@ -0,0 +1,7 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace TarninaHub.Application.Dtos;
|
||||
|
||||
|
||||
public sealed record UpdateSubmissionStatusRequest(
|
||||
[Required] string Status);
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace TarninaHub.Application.Dtos;
|
||||
|
||||
|
||||
public sealed record UserResponse(
|
||||
string Id,
|
||||
string Phone,
|
||||
string? Email,
|
||||
string Role,
|
||||
string? OrganizationId);
|
||||
@@ -0,0 +1,6 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace TarninaHub.Application.Dtos;
|
||||
|
||||
|
||||
public sealed record VerifyKnowledgeItemRequest(bool Verified);
|
||||
@@ -0,0 +1,26 @@
|
||||
using TarninaHub.Domain.Enums;
|
||||
|
||||
namespace TarninaHub.Application.Mappings;
|
||||
|
||||
public static class IdeaStatusWire
|
||||
{
|
||||
public static string ToWire(this IdeaStatus status) => status switch
|
||||
{
|
||||
IdeaStatus.Draft => "draft",
|
||||
IdeaStatus.Submitted => "submitted",
|
||||
IdeaStatus.InReview => "in_review",
|
||||
IdeaStatus.Accepted => "accepted",
|
||||
IdeaStatus.Rejected => "rejected",
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(status), status, null),
|
||||
};
|
||||
|
||||
public static IdeaStatus? Parse(string? value) => value switch
|
||||
{
|
||||
"draft" => IdeaStatus.Draft,
|
||||
"submitted" => IdeaStatus.Submitted,
|
||||
"in_review" => IdeaStatus.InReview,
|
||||
"accepted" => IdeaStatus.Accepted,
|
||||
"rejected" => IdeaStatus.Rejected,
|
||||
_ => null,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
using TarninaHub.Domain.Enums;
|
||||
|
||||
namespace TarninaHub.Application.Mappings;
|
||||
|
||||
public static class InnovationStageWire
|
||||
{
|
||||
private static readonly Dictionary<string, InnovationStage> ByWire = new(StringComparer.Ordinal)
|
||||
{
|
||||
["idea"] = InnovationStage.Idea,
|
||||
["pilot"] = InnovationStage.Pilot,
|
||||
["scaling"] = InnovationStage.Scaling,
|
||||
["service"] = InnovationStage.Service,
|
||||
};
|
||||
|
||||
public static bool IsValid(string? value) =>
|
||||
!string.IsNullOrWhiteSpace(value) && ByWire.ContainsKey(value);
|
||||
|
||||
public static string ToWire(this InnovationStage stage) => stage switch
|
||||
{
|
||||
InnovationStage.Idea => "idea",
|
||||
InnovationStage.Pilot => "pilot",
|
||||
InnovationStage.Scaling => "scaling",
|
||||
InnovationStage.Service => "service",
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(stage), stage, null),
|
||||
};
|
||||
|
||||
public static InnovationStage? Parse(string? value) =>
|
||||
value is not null && ByWire.TryGetValue(value, out var stage) ? stage : null;
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
using TarninaHub.Domain.Enums;
|
||||
|
||||
namespace TarninaHub.Application.Mappings;
|
||||
|
||||
public static class PartnershipStatusWire
|
||||
{
|
||||
public static string ToWire(this PartnershipStatus status) => status switch
|
||||
{
|
||||
PartnershipStatus.Proposed => "proposed",
|
||||
PartnershipStatus.Active => "active",
|
||||
PartnershipStatus.Closed => "closed",
|
||||
_ => "proposed",
|
||||
};
|
||||
|
||||
public static PartnershipStatus? Parse(string? value) => value?.Trim().ToLowerInvariant() switch
|
||||
{
|
||||
"proposed" => PartnershipStatus.Proposed,
|
||||
"active" => PartnershipStatus.Active,
|
||||
"closed" => PartnershipStatus.Closed,
|
||||
_ => null,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
using TarninaHub.Domain.Enums;
|
||||
|
||||
namespace TarninaHub.Application.Mappings;
|
||||
|
||||
public static class SubmissionStatusWire
|
||||
{
|
||||
public static string ToWire(this SubmissionStatus status) => status switch
|
||||
{
|
||||
SubmissionStatus.New => "new",
|
||||
SubmissionStatus.Triaged => "triaged",
|
||||
SubmissionStatus.Matched => "matched",
|
||||
SubmissionStatus.InProgress => "in_progress",
|
||||
SubmissionStatus.Closed => "closed",
|
||||
SubmissionStatus.Rejected => "rejected",
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(status), status, null),
|
||||
};
|
||||
|
||||
public static SubmissionStatus? Parse(string? value)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return value switch
|
||||
{
|
||||
"new" => SubmissionStatus.New,
|
||||
"triaged" => SubmissionStatus.Triaged,
|
||||
"matched" => SubmissionStatus.Matched,
|
||||
"in_progress" => SubmissionStatus.InProgress,
|
||||
"closed" => SubmissionStatus.Closed,
|
||||
"rejected" => SubmissionStatus.Rejected,
|
||||
_ => null,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
using TarninaHub.Application.Dtos;
|
||||
using TarninaHub.Application.Models;
|
||||
|
||||
namespace TarninaHub.Application.Mappings;
|
||||
|
||||
public static class UserMappings
|
||||
{
|
||||
public static UserResponse ToResponse(this UserAccount user) => new(
|
||||
user.Id,
|
||||
user.Phone,
|
||||
user.Email,
|
||||
user.Role,
|
||||
user.OrganizationId);
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace TarninaHub.Application.Matchmaking;
|
||||
|
||||
public static class EmbeddingConstants
|
||||
{
|
||||
/// <summary>Embedding dimension; must match the HNSW index definition.</summary>
|
||||
public const int Dimension = 1536;
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
using System.Text;
|
||||
|
||||
namespace TarninaHub.Application.Matchmaking;
|
||||
|
||||
/// <summary>
|
||||
/// Deterministic, offline embedding provider (feature hashing). Produces
|
||||
/// L2-normalized vectors so cosine similarity reflects shared vocabulary.
|
||||
/// Interchangeable with a real LLM provider behind <see cref="IEmbeddingProvider"/>
|
||||
/// (see ADR-0005). Used as the default until an external provider is wired.
|
||||
/// </summary>
|
||||
public sealed class HashingEmbeddingProvider : IEmbeddingProvider
|
||||
{
|
||||
public int Dimension => EmbeddingConstants.Dimension;
|
||||
|
||||
public Task<float[]> EmbedAsync(string text, CancellationToken cancellationToken)
|
||||
{
|
||||
return Task.FromResult(Embed(text));
|
||||
}
|
||||
|
||||
public Task<IReadOnlyList<float[]>> EmbedBatchAsync(
|
||||
IReadOnlyList<string> texts,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
IReadOnlyList<float[]> result = texts.Select(Embed).ToList();
|
||||
|
||||
return Task.FromResult(result);
|
||||
}
|
||||
|
||||
public float[] Embed(string text)
|
||||
{
|
||||
var vector = new float[Dimension];
|
||||
|
||||
if (string.IsNullOrWhiteSpace(text))
|
||||
{
|
||||
return vector;
|
||||
}
|
||||
|
||||
foreach (var token in Tokenize(text))
|
||||
{
|
||||
var index = (int)(Fnv1a(token) % Dimension);
|
||||
vector[index] += 1f;
|
||||
}
|
||||
|
||||
Normalize(vector);
|
||||
|
||||
return vector;
|
||||
}
|
||||
|
||||
private static IEnumerable<string> Tokenize(string text)
|
||||
{
|
||||
var buffer = new StringBuilder();
|
||||
|
||||
foreach (var character in text)
|
||||
{
|
||||
if (char.IsLetterOrDigit(character))
|
||||
{
|
||||
buffer.Append(char.ToLowerInvariant(character));
|
||||
}
|
||||
else if (buffer.Length > 0)
|
||||
{
|
||||
yield return buffer.ToString();
|
||||
buffer.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
if (buffer.Length > 0)
|
||||
{
|
||||
yield return buffer.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
private static void Normalize(float[] vector)
|
||||
{
|
||||
double sumOfSquares = vector.Sum(value => (double)value * value);
|
||||
var magnitude = Math.Sqrt(sumOfSquares);
|
||||
|
||||
if (magnitude <= 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
for (var i = 0; i < vector.Length; i++)
|
||||
{
|
||||
vector[i] = (float)(vector[i] / magnitude);
|
||||
}
|
||||
}
|
||||
|
||||
private static uint Fnv1a(string token)
|
||||
{
|
||||
const uint offset = 2166136261;
|
||||
const uint prime = 16777619;
|
||||
|
||||
var hash = offset;
|
||||
|
||||
foreach (var character in token)
|
||||
{
|
||||
hash ^= character;
|
||||
hash *= prime;
|
||||
}
|
||||
|
||||
return hash;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
namespace TarninaHub.Application.Matchmaking;
|
||||
|
||||
public interface IEmbeddingProvider
|
||||
{
|
||||
int Dimension { get; }
|
||||
|
||||
Task<float[]> EmbedAsync(string text, CancellationToken cancellationToken);
|
||||
|
||||
Task<IReadOnlyList<float[]>> EmbedBatchAsync(
|
||||
IReadOnlyList<string> texts,
|
||||
CancellationToken cancellationToken);
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
namespace TarninaHub.Application.Matchmaking;
|
||||
|
||||
public interface ILlmProvider
|
||||
{
|
||||
/// <summary>True when a real provider is configured (API key etc.).</summary>
|
||||
bool IsConfigured { get; }
|
||||
|
||||
/// <summary>Returns the completion text or null when unavailable.</summary>
|
||||
Task<string?> CompleteAsync(
|
||||
string systemPrompt,
|
||||
string userPrompt,
|
||||
CancellationToken cancellationToken);
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
namespace TarninaHub.Application.Matchmaking;
|
||||
|
||||
public interface ILlmRequestLimiter
|
||||
{
|
||||
/// <summary>
|
||||
/// Tries to reserve one LLM request within the current window.
|
||||
/// Returns false when the limit is exceeded (caller should fall back).
|
||||
/// </summary>
|
||||
bool TryAcquire();
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
namespace TarninaHub.Application.Matchmaking;
|
||||
|
||||
|
||||
public sealed record LlmPrompt(string System, string User);
|
||||
@@ -0,0 +1,32 @@
|
||||
namespace TarninaHub.Application.Matchmaking;
|
||||
|
||||
|
||||
public static class MatchPromptBuilder
|
||||
{
|
||||
private const string SystemPrompt =
|
||||
"Jesteś asystentem Małopolskiego Hubu Innowacji Społecznych. " +
|
||||
"Wyjaśnij po polsku, prostym językiem (maks. 2 zdania), dlaczego proponowana " +
|
||||
"innowacja pasuje do zgłoszonej potrzeby. Nie wymyślaj faktów.";
|
||||
|
||||
public static LlmPrompt BuildExplanation(
|
||||
string problemTitle,
|
||||
string problemDescription,
|
||||
string challengeId,
|
||||
string innovationTitle,
|
||||
string innovationSummary,
|
||||
string stage,
|
||||
double score)
|
||||
{
|
||||
var user =
|
||||
$"Potrzeba: {problemTitle}\n" +
|
||||
$"Opis: {problemDescription}\n" +
|
||||
$"Obszar: {challengeId}\n" +
|
||||
$"Proponowana innowacja: {innovationTitle}\n" +
|
||||
$"Streszczenie: {innovationSummary}\n" +
|
||||
$"Etap: {stage}\n" +
|
||||
$"Ocena dopasowania: {score:0.00}\n" +
|
||||
"Uzasadnij krótko dopasowanie.";
|
||||
|
||||
return new LlmPrompt(SystemPrompt, user);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
namespace TarninaHub.Application.Matchmaking;
|
||||
|
||||
/// <summary>
|
||||
/// Combines semantic similarity with context signals (area, location) and
|
||||
/// availability into a final 0..1 match score.
|
||||
/// </summary>
|
||||
public static class MatchScoring
|
||||
{
|
||||
private const double ChallengeBoost = 0.10;
|
||||
private const double LocationBoost = 0.05;
|
||||
|
||||
public static double Combine(
|
||||
double semanticScore,
|
||||
bool challengeMatch,
|
||||
bool locationMatch,
|
||||
bool available)
|
||||
{
|
||||
if (!available)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
var score = semanticScore;
|
||||
|
||||
if (challengeMatch)
|
||||
{
|
||||
score += ChallengeBoost;
|
||||
}
|
||||
|
||||
if (locationMatch)
|
||||
{
|
||||
score += LocationBoost;
|
||||
}
|
||||
|
||||
return Math.Clamp(score, 0, 1);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
namespace TarninaHub.Application.Matchmaking;
|
||||
|
||||
/// <summary>
|
||||
/// Default provider used when no external LLM is configured. Keeps the system
|
||||
/// fully offline/deterministic; matchmaking falls back to template rationales.
|
||||
/// </summary>
|
||||
public sealed class NullLlmProvider : ILlmProvider
|
||||
{
|
||||
public bool IsConfigured => false;
|
||||
|
||||
public Task<string?> CompleteAsync(
|
||||
string systemPrompt,
|
||||
string userPrompt,
|
||||
CancellationToken cancellationToken) => Task.FromResult<string?>(null);
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user