Compare commits
28
Commits
24c9e582c9
..
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 |
@@ -5,6 +5,12 @@
|
||||
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
|
||||
@@ -18,6 +24,15 @@ SurrealDb__DataDirectory=data
|
||||
# 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=
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
@@ -140,3 +150,4 @@ TarninaHub/
|
||||
- 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`).
|
||||
|
||||
+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,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);
|
||||
}
|
||||
@@ -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);
|
||||
@@ -1,25 +0,0 @@
|
||||
namespace TarninaHub.Application.Dtos;
|
||||
|
||||
public sealed class StorageProbe
|
||||
{
|
||||
public string Label { get; set; } = "probe";
|
||||
|
||||
public DateTimeOffset CreatedAt { get; set; }
|
||||
}
|
||||
|
||||
public sealed record StorageInfo(
|
||||
string Engine,
|
||||
string DataDirectory,
|
||||
string Namespace,
|
||||
string Database);
|
||||
|
||||
public sealed record StorageStatusResponse(
|
||||
string Engine,
|
||||
string DataDirectory,
|
||||
string Namespace,
|
||||
string Database,
|
||||
int ProbeCount);
|
||||
|
||||
public sealed record StorageProbeResponse(
|
||||
string Label,
|
||||
DateTimeOffset CreatedAt);
|
||||
@@ -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);
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
namespace TarninaHub.Application.Matchmaking;
|
||||
|
||||
|
||||
public sealed record RankedItem<T>(T Item, double Score);
|
||||
@@ -0,0 +1,29 @@
|
||||
namespace TarninaHub.Application.Matchmaking;
|
||||
|
||||
/// <summary>
|
||||
/// Deterministic, offline explanation of "why this fits". Used as the default
|
||||
/// and as a fallback when the LLM is unavailable or rate-limited.
|
||||
/// </summary>
|
||||
public static class RationaleBuilder
|
||||
{
|
||||
public static string Build(bool challengeMatch, bool locationMatch, double score)
|
||||
{
|
||||
var reasons = new List<string>();
|
||||
|
||||
if (challengeMatch)
|
||||
{
|
||||
reasons.Add("ten sam obszar problemowy");
|
||||
}
|
||||
|
||||
if (locationMatch)
|
||||
{
|
||||
reasons.Add("zbliżona lokalizacja");
|
||||
}
|
||||
|
||||
reasons.Add(score >= 0.3
|
||||
? "wysoka zbieżność opisu potrzeby"
|
||||
: "zbieżność semantyczna opisu");
|
||||
|
||||
return "Proponowane dopasowanie: " + string.Join(", ", reasons) + ".";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
namespace TarninaHub.Application.Matchmaking;
|
||||
|
||||
|
||||
public static class SemanticRanker
|
||||
{
|
||||
public static IReadOnlyList<RankedItem<T>> Rank<T>(
|
||||
IReadOnlyList<float> queryVector,
|
||||
IEnumerable<(T Item, IReadOnlyList<float> Vector)> candidates,
|
||||
int topK,
|
||||
double minScore = 0)
|
||||
{
|
||||
return candidates
|
||||
.Select(candidate => new RankedItem<T>(candidate.Item, VectorMath.CosineSimilarity(queryVector, candidate.Vector)))
|
||||
.Where(ranked => ranked.Score >= minScore)
|
||||
.OrderByDescending(ranked => ranked.Score)
|
||||
.Take(topK)
|
||||
.ToList();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
namespace TarninaHub.Application.Matchmaking;
|
||||
|
||||
public static class VectorMath
|
||||
{
|
||||
public static double CosineSimilarity(IReadOnlyList<float> a, IReadOnlyList<float> b)
|
||||
{
|
||||
if (a.Count == 0 || a.Count != b.Count)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
double dot = 0;
|
||||
double magnitudeA = 0;
|
||||
double magnitudeB = 0;
|
||||
|
||||
for (var i = 0; i < a.Count; i++)
|
||||
{
|
||||
dot += (double)a[i] * b[i];
|
||||
magnitudeA += (double)a[i] * a[i];
|
||||
magnitudeB += (double)b[i] * b[i];
|
||||
}
|
||||
|
||||
if (magnitudeA <= 0 || magnitudeB <= 0)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
return dot / (Math.Sqrt(magnitudeA) * Math.Sqrt(magnitudeB));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace TarninaHub.Application.Models;
|
||||
|
||||
public sealed record UserAccount(
|
||||
string Id,
|
||||
string Phone,
|
||||
string? Email,
|
||||
string Role,
|
||||
string? OrganizationId);
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user