Compare commits

..

14 Commits

Author SHA1 Message Date
OlgunR
e6b41f10c8 Merge branch 'feat/receiver-ui' of http://git.dd:3000/AppStd/EnvelopeGenerator into feat/receiver-ui 2025-12-10 11:16:27 +01:00
OlgunR
dc78ad4a24 Improve canvas mouse position accuracy; add settings.json
Refactored mouse event handling in pdfInterop.js to use a new getPos helper, ensuring accurate coordinate mapping on scaled or resized canvases. Updated start and move functions to use this helper. Added an empty settings.json file.
2025-12-10 11:16:13 +01:00
b282318298 Add launchSettings.json for ReceiverUIBlazor project
Configured development launch profile with browser launch, environment variable, and HTTP/HTTPS URLs for EnvelopeGenerator.ReceiverUIBlazor.
2025-12-10 09:55:32 +01:00
8d68ea8c57 Add IIS publish profile for EnvelopeGenerator.ReceiverUIBlazor
Introduced IISProfile.pubxml to enable packaging and deployment
of the EnvelopeGenerator.ReceiverUIBlazor project to IIS. The
profile configures publish method, build settings, output
package location, and IIS app path.
2025-12-10 09:55:24 +01:00
OlgunR
7cb8b02b1d Revamp UI colors and fonts for warmer, modern look
Switched to "Red Hat Text" and "Teko" fonts via Google Fonts. Updated color palette from cool blues to warm reds and yellows, including backgrounds, accents, and error states. Refreshed button, top bar, and card styles for improved visual hierarchy and softer appearance.
2025-12-09 10:58:53 +01:00
OlgunR
cc1d33462c Add drag-and-drop PDF support to Blazor app
Implement drag-and-drop PDF loading via JS interop and DotNetObjectReference. Refactor file loading logic and UI structure for clarity. Add IAsyncDisposable for resource cleanup. Update pdfInterop.js to handle drop events and send PDF data to Blazor.
2025-12-09 10:42:11 +01:00
OlgunR
cd85b4fffc Improve PDF reset logic and modernize UI styling
Refactor PDF reset to restore original document state using a new OriginalPdfBase64 variable and async logic. Redesign app.css with a lighter color palette, CSS variables, and updated styles for buttons, overlays, modals, and inputs for a cleaner, more accessible UI. Adjust signature and text overlay colors in pdfInterop.js for better contrast and consistency.
2025-12-09 10:26:01 +01:00
OlgunR
ab3e7fb4e9 Error while dragging fields fixed 2025-12-09 09:50:10 +01:00
OlgunR
4a7676765c Fixed error while loading file 2025-12-09 08:57:39 +01:00
OlgunR
562ceb9c3f First results converting receiver-ui-react into a Blazor Web App 2025-12-08 16:21:10 +01:00
OlgunR
751ea706df Created EnvelopeGenerator.ReceiverUIBlazor 2025-12-08 16:06:26 +01:00
OlgunR
490ad9f7cf Created EnvelopeGenerator.ReceiverUIBlazor 2025-12-08 16:04:51 +01:00
7aeaba7c12 Add EnvelopeGenerator.ReceiverUI project and UI components
Introduced a new Razor Components-based web application, `EnvelopeGenerator.ReceiverUI`, to the solution. This includes:

- Added `EnvelopeGenerator.ReceiverUI` project to `EnvelopeGenerator.sln` with Debug and Release configurations.
- Created `EnvelopeGenerator.ReceiverUI.csproj` targeting `.NET 8.0` with nullable and implicit usings enabled.
- Added Razor components (`App.razor`, `Routes.razor`, `MainLayout.razor`, `NavMenu.razor`, `Counter.razor`, `Error.razor`, `Home.razor`, `Weather.razor`) for layout, navigation, and pages.
- Included styles (`MainLayout.razor.css`, `NavMenu.razor.css`, `app.css`) and assets (`favicon.png`, `bootstrap.min.css`).
- Configured logging and allowed hosts in `appsettings.json` and `appsettings.Development.json`.
- Added `Program.cs` to configure and run the application.
- Added `launchSettings.json` for development and debugging profiles.

These changes enable a modern, interactive web interface with Bootstrap styling and development-friendly configurations.
2025-12-05 09:08:57 +01:00
2301a81a1c init receiver ui with react 2025-12-04 18:11:47 +01:00
673 changed files with 18420 additions and 52674 deletions

6
.gitignore vendored
View File

@@ -365,9 +365,3 @@ FodyWeavers.xsd
/EnvelopeGenerator.GeneratorAPI/ClientApp/envelope-generator-ui/.vscode /EnvelopeGenerator.GeneratorAPI/ClientApp/envelope-generator-ui/.vscode
/EnvelopeGenerator.Tests.Application/Services/BugFixTests.cs /EnvelopeGenerator.Tests.Application/Services/BugFixTests.cs
/EnvelopeGenerator.Tests.Application/annotations.json /EnvelopeGenerator.Tests.Application/annotations.json
/EnvelopeGenerator.Server/EnvelopeGenerator.Server/TekH - SoftHSM Test.md
/EnvelopeGenerator.Server/EnvelopeGenerator.Server/tekh_softHSM_test.md
/EnvelopeGenerator.Server/EnvelopeGenerator.Server/publish-output
/EnvelopeGenerator.Server/EnvelopeGenerator.Server/tekh_softHSM_test.md
/ignore/EnvelopeGenerator.Form
/EnvelopeGenerator.Server/EnvelopeGenerator.Server/appsettings.Secrets.json

1
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1 @@
{}

414
AGENTS.md
View File

@@ -1,414 +0,0 @@
# EnvelopeGenerator - Agent Guide
## Must Read First
- **`COPILOT_CONTEXT.md`** - Architecture, coordinate systems, migration status
- **`FORM_APPLICATION_CONTEXT.md`** - Legacy VB.NET features to migrate
---
## Documentation Catalog
This section explains every markdown file in the repo: what it contains, why it exists, and when you need it. Read this before deciding which file to open.
### Core Architecture Docs (Active — Always Relevant)
#### `AGENTS.md` ← you are here
**What:** Authoritative agent/copilot onboarding guide. Covers project structure, route table, render mode rules, coordinate system, API gaps, and common mistakes.
**Why it exists:** Single source of truth for agents so they don't re-derive architecture from code on every session.
**When you need it:** Always. Read first on every session.
**Maintenance:** Update whenever project structure, routes, render modes, or naming conventions change.
#### `COPILOT_CONTEXT.md`
**What:** Deep-dive architecture reference. Covers the full Blazor Auto hybrid setup, authentication flows (JWT + cookies, per-envelope receiver tokens), service registration patterns, coordinate system proof, and an explicit "prefer Server/Server.Client over old WebUI/ReceiverUI" rule.
**Why it exists:** Prevents agents from re-deriving or misinterpreting the hybrid SSR+WASM architecture. Also documents coordinate system evidence to prevent regressions (inches vs. points vs. pixels).
**When you need it:**
- Implementing any new page or service
- Debugging auth/cookie issues
- Any coordinate conversion question
- Unsure which project a new file should go in
**Maintenance:** Update when render mode assignments change, new auth schemes are added, or the coordinate system is touched.
#### `FORM_APPLICATION_CONTEXT.md`
**What:** Comprehensive reverse-engineering of the legacy VB.NET WinForms app (`EnvelopeGenerator.Form`). Documents every form (frmMain, frmEnvelopeEditor, frmFieldEditor, etc.), their UI layout, toolbar actions, data models, coordinate system, and a feature-by-feature migration mapping table to `EnvelopeGenerator.Server`.
**Why it exists:** The WinForms app is the functional specification for the sender-side web UI. It documents the full intended workflow that is being migrated to Blazor — especially the envelope creation, signature field placement, and dashboard features that are not yet implemented.
**When you need it:**
- Implementing any sender-side feature (`/sender`, `/sender/editor`, `/sender/envelope/{id}`)
- Understanding what a specific toolbar button or form should do
- Designing API endpoints for sender workflow
- Checking coordinate system behavior (INCHES proof is here)
**Maintenance:** Update the mapping table as features are implemented (change `❌ Not implemented` → `✅ Exists`). Do not rename `ReceiverUI` back — it was the old project name; current target is `EnvelopeGenerator.Server`.
#### `RECEIVER_PDF_VIEWER_CONTEXT.md`
**What:** Detailed technical reference for the receiver-side PDF viewer (`EnvelopeReceiverPage.razor`). Documents the PDF.js integration, signature button rendering, JS interop API (`window.pdfViewer`), signature capture flow, HiDPI/zoom/thumbnail config, and the full component lifecycle.
**Why it exists:** The receiver PDF viewer is the most complex component in the codebase. This doc prevents agents from misunderstanding the JS↔Blazor boundary, re-implementing already-solved problems (HiDPI, zoom, signature caching), or breaking the signing flow.
**When you need it:**
- Working on `EnvelopeReceiverPage.razor` or `pdf-viewer.js`
- Adding new signature field rendering logic
- Debugging signature rendering, zoom, or thumbnail issues
- Implementing sender-side PDF.js overlay (signature field placement) — the receiver side is the reference implementation
**Maintenance:** Update when `pdf-viewer.js` API changes, new JS interop functions are added, or the signing flow is modified.
#### `DATABASE_STRUCTURE_CONTEXT.md`
**What:** Deep DB documentation for the envelope domain. Includes table/column snapshot, entity-to-table mapping, EF relationship/constraint map, stored procedure behavior + call sites, and trigger behavior + wiring path.
**Why it exists:** DB is centralized and can evolve independently; this file preserves migration-critical data semantics when historical DB context is hard to recover.
**When you need it:**
- Planning or reviewing DB/schema changes
- Debugging status transitions/history side effects
- Tracing where `PRSIG_*` procedures or DB triggers affect app behavior
- Reconciling Domain entities with physical SQL schema
**Maintenance:** Refresh on each DB migration, trigger/procedure change, or significant Domain/EGDbContext model change.
---
### Deployment Docs
#### `EnvelopeGenerator.Server/EnvelopeGenerator.Server/README.md`
**What:** IIS publish and deployment guide for `EnvelopeGenerator.Server`. Covers why self-contained publish is required (WASM assembly version strictness), the exact `dotnet publish` command, IIS Application Pool settings (`No Managed Code`), directory structure after publish, and common deployment errors.
**Why it exists:** Blazor Auto (Server + WASM hybrid) has non-obvious deployment requirements that differ from a standard ASP.NET Core app. Forgetting self-contained publish or using the wrong app pool will break WASM loading in production.
**When you need it:**
- Deploying to IIS for the first time
- Troubleshooting 500 errors on production after deploy
- Setting up a new deployment environment
**Maintenance:** Update if the publish target, IIS config, or runtime version changes.
---
## Markdown Audit Snapshot (2026-10-05)
The following table summarizes repository markdown files by practical status: completed, partially complete, or removable candidates.
| File | Type | Status | Missing / Gap | Removal Recommendation |
|---|---|---|---|---|
| `AGENTS.md` | Agent onboarding | Partially complete | Route/migration statements can drift from real code over time. | Keep |
| `COPILOT_CONTEXT.md` | Architecture reference | Partially complete | Some route/render details may be out of sync with other docs; needs regular reconciliation. | Keep |
| `FORM_APPLICATION_CONTEXT.md` | Legacy-to-modern feature spec | Partially complete | Large sections still marked not implemented / needs implementation. | Keep |
| `RECEIVER_PDF_VIEWER_CONTEXT.md` | Receiver PDF viewer deep-dive | Partially complete | Long migration plan needs periodic sync with active implementation. | Keep |
| `SENDER_SAVE_EDIT_MIGRATION_PLAN.md` | Sender workflow implementation plan | Partially complete | No explicit implementation progress markers per phase. | Keep (until migration closes) |
| `SENDER_PDF_TOOLBAR_PLAN_EN.md` | Sender PDF toolbar plan | Incomplete | Explicit pending item remains (`Toolbar visual polish`). | Keep (until done) |
| `SENDER_EDITOR_WASM_PDF_EDITOR_IMPLEMENTATION_PLAN_en.md` | Sender editor WASM migration plan | Incomplete | Plan exists but completion/progress tracking is minimal. | Keep (until done) |
| `EnvelopeGenerator.Server/EnvelopeGenerator.Server/README.md` | Deployment guide | Complete (operational) | No critical gap identified. | Keep |
| `envelope-generator-ui/README.md` | Angular scaffold README | Complete but generic | Template text only; low project-specific value. | Removable or replace with project-specific summary |
| `EnvelopeGenerator.Web/wwwroot/lib/typed.js@2.1.0/README.md` | Third-party vendor README | Complete (vendor) | Not project workflow documentation. | Keep with vendor assets |
| `EnvelopeGenerator.Web/wwwroot/lib/jquery-validation/LICENSE.md` | Third-party license | Complete (legal) | No gap. | Keep |
| `EnvelopeGenerator.Server/EnvelopeGenerator.Server/wwwroot/css/open-iconic/README.md` | Third-party vendor README | Complete (vendor) | Not project-specific guidance. | Keep with vendor assets |
| `ignore/EnvelopeGenerator/EnvelopeGenerator.Web/wwwroot/README.md` | Legacy PSPDFKit note | Outdated | Mentions deprecated PSPDFKit-based flow; may mislead if reused. | Removable/archive |
| `ignore/EnvelopeGenerator/EnvelopeGenerator.Web/wwwroot/lib/typed.js@2.1.0/README.md` | Duplicate vendor README | Redundant | Duplicate copy under `ignore/`. | Removable/archive |
| `ignore/EnvelopeGenerator/EnvelopeGenerator.Web/wwwroot/lib/bootstrap-icons/README.md` | Vendor README in archive area | Secondary | Archive-only utility, not active docs. | Removable/archive |
| `ignore/EnvelopeGenerator/EnvelopeGenerator.Web/wwwroot/lib/jquery-validation/LICENSE.md` | Duplicate license in archive area | Redundant | Duplicate copy under `ignore/`. | Removable/archive (if legal policy allows) |
| `ignore/EnvelopeGenerator/EnvelopeGenerator.GeneratorAPI/ClientApp/envelope-generator-ui/README.md` | Duplicate Angular scaffold README | Redundant | Generic template + duplicate in archive path. | Removable/archive |
### Documentation Audit TODO
- [ ] Validate this markdown audit table against the current codebase and update statuses (`complete`, `partial`, `incomplete`, `removable`) when implementation changes land.
---
## Active Architecture (Post-Migration)
**Frontend:** Blazor Auto (Server+WASM hybrid)
- **EnvelopeGenerator.Server** (Server): `@rendermode InteractiveServer` - PDF viewers requiring DevExpress backend
- **EnvelopeGenerator.Server.Client** (WASM): `@rendermode InteractiveWebAssembly` - Login, dashboards, business logic
**Backend + API Host:** `EnvelopeGenerator.Server` (ASP.NET Core 8.0, merged Blazor + Web API host)
**Proxy:** YARP in `EnvelopeGenerator.Server` is used for external integrations (for example AuthHub forwarding), not for routing app APIs to `EnvelopeGenerator.API`.
### Deprecated Projects - DO NOT USE
- `EnvelopeGenerator.ReceiverUI` - Pure WASM (migrated to Server)
- `EnvelopeGenerator.Web` - Razor Pages (replaced by Server)
- **VB.NET projects** (`Form`, `Service`, `BBTests`) - Legacy, read-only for reference
## Development Commands
### Run Active Host
```powershell
# Terminal - Blazor + Web API host
cd EnvelopeGenerator.Server\EnvelopeGenerator.Server
dotnet run
```
**Critical:** `EnvelopeGenerator.Server` is the active runtime host for both UI and API controllers.
### Build
```powershell
dotnet build EnvelopeGenerator.sln
```
## Project Boundaries
```
EnvelopeGenerator.Domain/ # Entities (Envelope, Receiver, Document, etc.)
EnvelopeGenerator.Application/ # MediatR CQRS (Commands, Queries, Handlers)
EnvelopeGenerator.Infrastructure/ # EF Core, SQL executors, repositories
EnvelopeGenerator.Server/
EnvelopeGenerator.Server/ # Server-side Blazor + API controllers
├─ Components/Pages/ # @rendermode InteractiveServer
├─ Controllers/ # Active Web API endpoints
EnvelopeGenerator.Server.Client/ # Client-side WASM components
├─ Pages/ # @rendermode InteractiveWebAssembly
├─ Services/ # HTTP API clients
├─ Models/ # DTOs
```
## Route Structure (Critical)
| Route | File Location | Render Mode | Purpose |
|-------|--------------|-------------|---------|
| `/` | `Server.Client/Pages/Index.razor` | WASM | Landing page |
| `/sender/login` | `Server.Client/Pages/LoginSenderPage.razor` | WASM | Sender auth |
| `/sender` | `Server/Components/Pages/EnvelopeSenderPage.razor` | **Server** | Sender dashboard |
| `/sender/editor` | `Server/Components/Pages/EnvelopeSenderEditorPage.razor` | **Server** | Envelope editor + PDF viewer |
| `/envelope/login/{key}` | `Server.Client/Pages/LoginReceiverPage.razor` | WASM | Receiver auth |
| `/envelope/{key}` | `Server/Components/Pages/EnvelopeReceiverPage.razor` | **Server** | PDF viewer + signing |
**Rule:** PDF viewers MUST use `@rendermode InteractiveServer` (DevExpress backend requirement). Everything else uses WASM.
## Coordinate System (CRITICAL)
**Database stores INCHES** (GdPicture14 native). Origin: top-left, Y-axis down.
### Conversions
```csharp
// Database (INCHES) → PDF Points
float points = inches * 72;
// Database (INCHES) → DevExpress DX
float dx = inches * 100;
// PDF.js Pixels → Database (INCHES)
float inches = (pixelX / canvasWidth) * pageWidthInches;
```
**A4 Page:** 8.27" wide × 11.69" tall = 595pt × 842pt
**Signature Field Size:** 1.77" × 1.96" (FIXED, do not change)
**Evidence:** See `COPILOT_CONTEXT.md` lines 158-185, `EnvelopeGenerator.Form/frmFieldEditor.vb`
## API Architecture Quirks
### Monolithic Endpoint (Avoid for UI)
`POST /api/EnvelopeReceiver` - Creates envelope+document+receivers+fields atomically.
- **Use case:** External API consumers
- **Not suitable for:** Step-by-step UI workflow (no draft support, no partial updates)
### Sender Workflow Endpoint
```
POST /api/Envelope # Create/update/save/send envelope workflow payload
DELETE /api/Envelope/{id} # Delete with reason
POST /api/Envelope/{id}/document # Upload PDF
POST /api/Envelope/{id}/receivers # Add receiver
POST /api/Envelope/{id}/signature-fields # Place signature field
```
See `FORM_APPLICATION_CONTEXT.md` for detailed workflow requirements.
## Status Color Coding
Form app uses DevExpress `CustomDrawCell`. EnvelopeGenerator.Server needs CSS:
```css
.envelope-row.status-partly-signed { background-color: #81C784; } /* GREEN_300 */
.envelope-row.status-queued,
.envelope-row.status-sent { background-color: #FFB74D; } /* ORANGE_300 */
.envelope-row.status-completed { background-color: #81C784; }
.envelope-row.status-deleted,
.envelope-row.status-rejected { background-color: #E57373; } /* RED_300 */
```
## Configuration
### YARP Proxy (`EnvelopeGenerator.Server/yarp.json`)
Routes `/api/*`, `/swagger/*`, `/openapi/*`, `/scalar/*` → `https://localhost:8088`
### PDF.js Settings (`EnvelopeGenerator.Server/wwwroot/appsettings.json`)
```json
{
"PdfViewerOptions": {
"ThumbnailBaseScale": 0.75,
"ThumbnailEnableHiDPI": true,
"MainCanvasEnableHiDPI": true,
"ZoomStepPercentage": 5
}
}
```
### Server Host Config (`EnvelopeGenerator.Server/EnvelopeGenerator.Server/appsettings.json`)
- `ConnectionStrings:Default` - SQL Server DB
- `AllowedOrigins` - CORS (includes `http://localhost:5131`, `http://localhost:7192`)
- `Cache:SignatureCacheExpiration` - Signature persistence timeout
- `PSPDFKitLicenseKey` - **DEPRECATED** (use PDF.js instead)
## Migration Status
### Complete ✅
- Receiver login/authentication
- PDF viewing with PDF.js (HiDPI, zoom, thumbnails)
- Signature capture (draw/type/image)
- Signature caching (Redis/SQL)
- Sender login
### Missing (High Priority) ❌
- Sender dashboard (`/sender`) - Empty stub
- Envelope editor (`/sender/envelope/{id}`)
- Signature field placement tool (PDF.js + draggable overlays)
- Granular API endpoints (draft, receivers, fields)
- Master-detail grids for receivers/history
## Common Mistakes (DO NOT REPEAT)
| Mistake | Why Wrong |
|---------|-----------|
| Using iText7 in receiver pages | GPL license issue. Use PDF.js overlays. |
| Using PSPDFKit | Removed from architecture. Use PDF.js + DevExpress. |
| `@rendermode InteractiveWebAssembly` on PDF viewers | DevExpress DxPdfViewer requires server-side rendering. |
| Hardcoded quality in PDF.js | Use `appsettings.json` `PdfViewerOptions`. |
| Coordinates in points/pixels for DB | Database uses INCHES. Convert before save. |
| `BottomMarginBand` for signatures | Repeats on every page. Use `DetailBand`. |
## Testing
**No automated tests exist yet.**
Manual testing workflow:
1. Start EnvelopeGenerator.Server (`dotnet run` in `EnvelopeGenerator.Server\EnvelopeGenerator.Server`)
2. Navigate to `https://localhost:5131` (or check console output for port)
3. Test sender login at `/sender/login`
4. Test receiver flow at `/envelope/login/{envelopeKey}`
## Technical Debt TODOs
- **CRITICAL (Licensing / pre-commercial release):** Remove all `SixLabors.ImageSharp` and `QRCoder-ImageSharp` package dependencies from production/runtime projects before commercial release. Replace remaining QR/image paths with permissive-license alternatives (no copyleft/share-source obligations), run repo-wide package audit (`*.csproj`), and add CI guardrails to block reintroduction.
- Refactor `EnvelopeGenerator.Server/EnvelopeGenerator.Server/Components/Pages/EnvelopeSenderPage.razor` to reduce complexity.
- Move sender dashboard business/status logic into dedicated services and extension methods (for example effective status resolution, tab classification, receiver signed/rejected detection, and grid-layout filter sanitation).
- Keep `EnvelopeSenderPage.razor` focused on UI composition/state orchestration; avoid embedding heavy domain logic directly in the Razor component.
- Apply a modular page architecture across Server pages (starting with `EnvelopeGenerator.Server/EnvelopeGenerator.Server/Components/Pages/EnvelopeSenderPage.razor`): split large pages into smaller subcomponents/partial modules, eliminate duplicated code paths, and centralize shared UI/domain logic for maintainability.
- Perform a senior-level page performance review across sender/receiver flows before further UX changes: verify route transitions, component mount/unmount behavior, loading-state correctness, duplicate API calls, and perceived latency (first meaningful paint and return-navigation responsiveness). Treat loading logic defects as architecture/performance issues first, not styling problems.
- Audit `EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Services/` for unnecessary request/payload remapping and remove redundant mappings where direct request DTO usage is safe; review client HTTP methods end-to-end for similar avoidable transformation layers.
- Centralize dependency injection registrations into dedicated extension methods/modules (especially server + client service registrations in `Program.cs`) to reduce startup composition sprawl and improve maintainability.
- Refactor `EnvelopeGenerator.Server/EnvelopeGenerator.Server/Controllers/PdfRenderController.cs` + matching client service contract to remove JSON/base64 `byte[]` transport for PDF rendering; prefer binary streaming (`application/octet-stream` or multipart), avoid `data:` URL bloat where possible, and validate end-to-end memory/latency behavior with large PDFs.
- Establish a DevExpress toast-based notification architecture using `DxToastProvider` + `IToastNotificationService` (tab/page-scoped provider strategy, standardized `ToastOptions` factory, and severity-to-style mapping) so sender/receiver flows show consistent, non-duplicated, actionable feedback.
- Eliminate manual property-by-property request→domain mapping drift (example: `CreateEnvelopeCommandHandler`) by introducing a standardized mapping policy (AutoMapper/profile conventions or reflection/source-generator based mapper) and enforcing it across envelope workflows; this needs to be solved in the shared company Core library level so all services get the same guardrails by default.
- P1 security workstream: remove dynamic SQL string composition/interpolation from active save/create/update paths (`string.Format`, `$"...{input}..."`, `ToSqlParam`) and migrate to parameterized queries only; treat this as a potential SQL injection vulnerability (stability + security risk), execute repo-wide audit, and add CI guardrails to block reintroduction.
- Alternative UX design (Server component): in `EnvelopeGenerator.Server/EnvelopeGenerator.Server/Components/Pages/EnvelopeSenderPage.razor`, enforce deterministic row double-click behavior as exactly one action (preview OR edit), and keep row action buttons/tooltips aligned with the same rule to eliminate ambiguous navigation.
- Implement SPA-style language switching so the UI culture updates immediately without a full page reload.
- Implement sender-scoped caching for "recent receiver suggestions" used during envelope creation (wizard/editor flows): cache key should include sender identity, support short TTL + invalidation hooks after successful envelope receiver writes, and prevent cross-sender suggestion leakage.
## Database
**SQL Server** (DD_ECM)
- Connection string in `EnvelopeGenerator.Server/EnvelopeGenerator.Server/appsettings.json`
- EF Core migrations NOT used (manual SQL scripts)
- Stored procedures: `PRSIG_*` prefix
**Key Tables:**
- `TBSIG_ENVELOPE` - Envelope metadata
- `TBSIG_ENVELOPE_RECEIVER` - Receiver assignments
- `TBSIG_DOCUMENT_RECEIVER_ELEMENT` - Signature fields (X, Y in INCHES)
- `TBSIG_RECEIVER` - Receiver registry
- `TBSIG_ENVELOPE_DOCUMENT` - PDF binary data
- `TBSIG_ENVELOPE_HISTORY` - Audit trail
### Data Structure (Domain + EGDbContext)
Use this as the canonical quick map before touching sender/receiver workflow code.
#### Core Aggregate Shape
- `Envelope` (`TBSIG_ENVELOPE`) is the root aggregate for sending workflow.
- `Envelope -> Documents` is `1:N` via `Document.EnvelopeId` (`TBSIG_ENVELOPE_DOCUMENT`).
- `Envelope -> Histories` is `1:N` via `History.EnvelopeId` (`TBSIG_ENVELOPE_HISTORY`).
- `Envelope <-> Receiver` is `N:N` through `EnvelopeReceiver` (`TBSIG_ENVELOPE_RECEIVER`, composite PK `{EnvelopeId, ReceiverId}`).
- `Document -> DocReceiverElement` is `1:N` via `DocReceiverElement.DocumentId` (`TBSIG_DOCUMENT_RECEIVER_ELEMENT`).
- `DocReceiverElement -> ElementAnnotation` is `1:N` via `ElementAnnotation.ElementId` (`TBSIG_DOCUMENT_RECEIVER_ELEMENT_ANNOTATION`).
#### Status and Tracking Entities
- `DocumentStatus` (`TBSIG_DOCUMENT_STATUS`) links `EnvelopeId + ReceiverId` and stores receiver-level signing state.
- `History` stores timeline events (`EnvelopeStatus`) with polymorphic `UserReference` lookup semantics.
- `EnvelopeReceiverReadOnly` (`TBSIG_ENVELOPE_RECEIVER_READ_ONLY`) stores temporary read-only delegation with validity window (`DateValid`).
#### Important Constraints From `EGDbContext`
- `Envelope.Uuid` has a unique index.
- `Receiver.Signature` has a unique index.
- `Receiver.EmailAddress` has a unique index.
- `EnvelopeReceiver` uses composite primary key: `{ EnvelopeId, ReceiverId }`.
- `History.UserReference` is mapped conditionally (`IsMigration == false`) to:
- `Receiver.EmailAddress` (`History.Receiver`)
- `User.Email` from UserManager (`History.Sender`)
- `EnvelopeReceiverReadOnly.AddedWho` maps to `Receiver.EmailAddress` (principal key mapping, not receiver GUID).
#### Coordinate-Critical Fields
- Signature placement is persisted in `DocReceiverElement` as `X`, `Y`, `Width`, `Height`, `Page`.
- These coordinates are in INCHES (not PDF points, not pixels).
#### Cross-Module Tables in Same Context
- `EGDbContext` also includes UserManager entities: `User`, `Group`, `Module`, `GroupOfUser`, `ModuleOfUser`, `UserRep`, and `ClientUser`.
- Mail dispatcher entity `EmailOut` is also registered under `#if NET` and trigger-configurable.
#### Trigger-Aware Model Notes
- Trigger names are injected from `DbTriggerParams` in appsettings and applied via `ToTable(tb => tb.HasTrigger(...))`.
- Current trigger registration is explicit (`AddTrigger<T>()` calls), not attribute/reflection driven.
- Do not remove trigger mappings casually; this project depends on DB-side automation/audit behavior.
## DevExpress
**License:** Commercial (v25.2.3)
**Components Used:**
- `DxGrid` - Master-detail grids
- `DxPdfViewer` - Server-side PDF rendering
- `DxPopup` - Modal dialogs
- `DxToolbar` - Action bars
- `DxFormLayout` - Forms
**Theme:** Blazing Berry (default)
## JavaScript Interop
**PDF Viewer:** `wwwroot/js/pdf-viewer.js`
```javascript
window.pdfViewer = {
initialize(canvasId, pdfDataUrl, dotNetRef),
renderPage(num),
renderSignatureButtons(signatures, pageNum, dotNetRef),
applySignature(signatureId, dataUrl, fullName, position, place),
zoomIn(), zoomOut(), dispose()
}
```
**Signature Pad:** `wwwroot/js/receiver-signature.js`
```javascript
window.receiverSignature = {
initializeDrawPad(canvasId, dotNetRef),
getSignatureDataUrl(canvasId),
clearPad(canvasId)
}
```
## Multi-Envelope Support
Receivers can login to **multiple envelopes simultaneously** via per-envelope cookies:
```
AuthTokenSignFLOWReceiver.{envelopeKey}
```
Each envelope maintains independent authentication state.
## External Dependencies
**CDN:**
- PDF.js 3.11.174: `https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.min.js`
**NuGet (Server.Client):**
- `DevExpress.Blazor.*` 25.2.3
- `SkiaSharp.*` 3.119.1 (WASM rendering)
**External Services:**
- LDAP/AD authentication (optional)
- GTX Messaging (SMS 2FA)
- Email dispatcher (signFlow)
## Environment Variables
None required. All config in `appsettings.json`.
**Local dev ports:**
- App host (UI + API): `https://localhost:5131` or `http://localhost:7192`

View File

@@ -1,351 +0,0 @@
# EnvelopeGenerator — Current Workspace Context
## Purpose
Digital document signing system for senders and receivers.
- Senders authenticate, view envelope lists, and manage envelope workflows.
- Receivers authenticate per envelope, open PDFs, create signatures, and apply them in the viewer.
- The active UI stack is `Blazor Auto` with server-side and WebAssembly render modes.
- Primary UI/PDF libraries are `DevExpress` and `PDF.js`.
---
## Active Application Structure
### Main Host
**Primary active application:** `EnvelopeGenerator.Server`
`EnvelopeGenerator.Server` is the current runtime host and contains:
- Blazor server host
- WebAssembly host integration
- API controllers
- authentication/authorization setup
- Swagger/Scalar setup
- YARP reverse proxy configuration
- DevExpress server-side services
- SQL Server distributed cache setup
### Client Project
**Client UI project:** `EnvelopeGenerator.Server.Client`
This project contains:
- WebAssembly-rendered pages
- client-side services
- client models and options
- sender and receiver login flows
### Other Projects
- `EnvelopeGenerator.Application` — MediatR/CQRS handlers and business logic
- `EnvelopeGenerator.Domain` — domain models, constants, shared abstractions
- `EnvelopeGenerator.Infrastructure` — EF Core and infrastructure services
- `EnvelopeGenerator.PdfEditor` — PDF-related backend utilities
- `EnvelopeGenerator.API` — deprecated project (no longer part of the active solution/runtime path)
### Legacy / Do Not Touch
- `EnvelopeGenerator.Service`
- `EnvelopeGenerator.Form`
- `EnvelopeGenerator.BBTests`
- `EnvelopeGenerator.CommonServices`
---
## Current Hosting Model
`EnvelopeGenerator.Server/Program.cs` currently configures:
- `AddRazorComponents()` with both interactive server and interactive WebAssembly components
- `AddControllers()` and `MapControllers()`
- JWT authentication for sender and receiver flows
- cookie authentication
- authorization policies using `AuthScheme.Sender`, `AuthScheme.Receiver`, `AuthPolicy.Sender`, `AuthPolicy.Receiver`
- `AddReverseProxy()` with `yarp.json`
- Swagger / OpenAPI / Scalar
- distributed SQL Server cache
- DevExpress Blazor and DevExpress PDF Viewer server-side services
- request localization middleware
This means the active app is a **merged UI + API host**.
---
## Reverse Proxy
**Config file:** `EnvelopeGenerator.Server/EnvelopeGenerator.Server/yarp.json`
Current YARP usage is focused on **AuthHub forwarding**, not a general `/api/* -> EnvelopeGenerator.API` proxy.
Configured routes forward:
- `POST /api/auth` -> AuthHub `/api/auth/sign-flow`
- `POST /api/Auth/envelope-receiver/{key}` -> AuthHub `/api/auth/envelope-receiver/{key}?cookie=true`
---
## Active Routes and Files
### WebAssembly Pages (`EnvelopeGenerator.Server.Client`)
| Route | File | Render Mode | Purpose |
|---|---|---|---|
| `/` | `EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Pages/IndexPage.razor` | WebAssembly | Landing page |
| `/sender/login` | `EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Pages/LoginSenderPage.razor` | WebAssembly | Sender login |
| `/sender` | `EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Pages/EnvelopeSenderPage.razor` | WebAssembly (`prerender: false`) | Sender dashboard |
| `/envelope/login/{EnvelopeKey}` | `EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Pages/LoginReceiverPage.razor` | WebAssembly | Receiver login |
### Server Pages (`EnvelopeGenerator.Server`)
| Route | File | Render Mode | Purpose |
|---|---|---|---|
| `/envelope/{EnvelopeKey}` | `EnvelopeGenerator.Server/EnvelopeGenerator.Server/Components/Pages/EnvelopeReceiverPage.razor` | InteractiveServer | Main receiver PDF viewer and signing page |
| `/envelope/DxPdfViewer` | `EnvelopeGenerator.Server/EnvelopeGenerator.Server/Components/Pages/EnvelopeReceiverPage_DxPdfViewer.razor` | InteractiveServer | DevExpress PDF Viewer test page |
| `/envelope/{EnvelopeKey}/DxReportViewer` | `EnvelopeGenerator.Server/EnvelopeGenerator.Server/Components/Pages/EnvelopeReceiverPage_DxReportViewer.razor` | InteractiveServer | DevExpress report-based PDF rendering |
| `/envelope/Embed` | `EnvelopeGenerator.Server/EnvelopeGenerator.Server/Components/Pages/EnvelopeReceiverPage_embed.razor` | InteractiveServer | Embedded browser PDF view test page |
---
## Current API Location
The active application exposes controllers from:
`EnvelopeGenerator.Server/EnvelopeGenerator.Server/Controllers`
Current controller set includes:
- `AnnotationController`
- `AuthController`
- `CacheController`
- `ConfigController`
- `DocumentController`
- `EmailTemplateController`
- `EnvelopeController`
- `EnvelopeReceiverController`
- `EnvelopeTypeController`
- `HistoryController`
- `LocalizationController`
- `ReadOnlyController`
- `ReceiverController`
- `SignatureController`
- `TfaRegistrationController`
Do not assume API behavior lives in `EnvelopeGenerator.API`; the active merged host contains controller endpoints directly.
---
## Authentication Model
### Sender
Client login page uses `EnvelopeGenerator.Server.Client/Services/AuthService.cs`.
Key sender endpoints:
- `POST /api/auth?cookie=true` — login
- `GET /api/auth/check` — current sender access check
- `POST /api/auth/logout` — logout
### Receiver
Receiver authentication is **per envelope**.
Key receiver endpoints used by client services:
- `POST /api/Auth/envelope-receiver/{envelopeKey}` — submit access code
- `GET /api/auth/check/envelope/{envelopeKey}` — check access
- `POST /api/auth/logout/envelope/{envelopeKey}` — logout receiver for one envelope
Receiver cookie resolution in server auth uses an envelope-specific cookie name derived from:
- `AuthTokenSignFLOWReceiver.{envelopeKey}` pattern
### Receiver Server-Side Authorization
`EnvelopeReceiverPage.razor` does **not** rely on its own API access-check call for page authorization.
It uses:
- `EnvelopeGenerator.Server/EnvelopeGenerator.Server/Services/EnvelopeReceiverAuthorizationService.cs`
Behavior:
- tries the current `HttpContext.User`
- if needed, reads the per-envelope receiver cookie directly
- validates the JWT with the receiver auth scheme
- verifies the token subject matches the route envelope key
---
## Receiver Page Data Loading
Main server-side page data service:
- `EnvelopeGenerator.Server/EnvelopeGenerator.Server/Services/EnvelopeReceiverPageDataService.cs`
This service loads directly via MediatR and distributed cache:
- document bytes
- receiver envelope data
- signature placeholders
- cached signature data
For signature placeholders, the service:
- reads document receiver elements
- filters them for the authenticated receiver
- converts coordinates to `UnitOfLength.Point` before UI use
---
## Receiver PDF Viewer
**Main file:** `EnvelopeGenerator.Server/EnvelopeGenerator.Server/Components/Pages/EnvelopeReceiverPage.razor`
Current receiver viewer characteristics:
- route: `/envelope/{EnvelopeKey}`
- render mode: `InteractiveServer`
- PDF rendering: `PDF.js`
- toolbar: page navigation, zoom, thumbnail toggle, signature navigation, signature reset
- signature popup: `DxPopup`
- thumbnail sidebar: resizable and stored in `localStorage`
### JS Assets
- `EnvelopeGenerator.Server/EnvelopeGenerator.Server/wwwroot/js/pdf-viewer.js`
- `EnvelopeGenerator.Server/EnvelopeGenerator.Server/wwwroot/js/receiver-signature.js`
### CSS
- `EnvelopeGenerator.Server/EnvelopeGenerator.Server/wwwroot/css/envelope-viewer.css`
### PDF.js CDN
- `https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.min.js`
- `https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf_viewer.min.css`
---
## Signature Workflow
Receiver signatures are handled as a **viewer overlay workflow**.
### Current behavior
1. Server-side authorization validates receiver access.
2. The page loads document bytes, receiver data, signature placeholders, and cached signature state.
3. If no cached signature exists, the signature popup opens automatically.
4. Receiver creates signature using one of three tabs:
- draw
- text
- image
5. Required metadata:
- full name
- place
6. Optional metadata:
- position
7. Clicking a signature placeholder applies the signature as a client-side overlay in the PDF viewer.
### Important note
Although `itext` is referenced by the server project, the current receiver page signing flow is **not PDF stamping-based**. The active receiver UI uses client-side overlay behavior in the viewer.
### Signature DTO
`EnvelopeGenerator.Server.Client/Models/SignatureCaptureDto.cs`
```csharp
public sealed record SignatureCaptureDto {
public required string DataUrl { get; init; }
public required string FullName { get; init; }
public string Position { get; init; } = "";
public required string Place { get; init; }
}
```
---
## Signature Cache
### Active cache model
The current receiver page cache flow is handled directly in the server project through:
- `EnvelopeReceiverPageDataService`
- `IDistributedCache`
- SQL Server distributed cache configuration from `Program.cs`
### Cache key format
Current server-side key prefix:
- `envelope-generator.receiver-ui.signature:{receiverSignature}`
This is different from an envelope-key-only cache convention.
### Config
`EnvelopeGenerator.Server/EnvelopeGenerator.Server/Options/CacheOptions.cs`
- section name: `Cache`
- option: `SignatureCacheExpiration`
### Related controller
A cache API controller also exists in:
- `EnvelopeGenerator.Server/EnvelopeGenerator.Server/Controllers/CacheController.cs`
---
## Sender Dashboard
**Main file:** `EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Pages/EnvelopeSenderPage.razor`
Current behavior:
- checks sender access through `AuthService.CheckSenderAccessAsync()`
- redirects to `/sender/login` when unauthorized
- loads envelope list through client `EnvelopeService`
- separates envelopes into active/completed tabs
- uses `DevExpress DxGrid`
The sender page is active, but create/edit/delete actions are still marked with TODO behavior in the UI page.
---
## Localization
Current server host localization setup in `Program.cs`:
- supported cultures: `de-DE`, `en-US`
- request localization middleware is enabled
- `QueryStringRequestCultureProvider` is added
- cookie-based localization services are registered via `AddCookieBasedLocalizer()`
Do not assume the old ReceiverUI-only `localStorage` culture approach is the current source of truth for the active host.
---
## Coordinate System
### Source data
Database signature coordinates are still based on:
- **unit:** inches
- **origin:** top-left
- **axes:** X right, Y down
### Relevant conversions
- inches -> PDF points: `x_pt = x_inches * 72`
- inches -> DevExpress DX units: `x_dx = x_inches * 100`
### Current receiver page behavior
The server page data service converts signature placeholders to **points** before sending them into the viewer workflow.
### Unit systems to keep in mind
| System | Unit | Origin | Y-axis |
|---|---|---|---|
| Database | Inches | Top-left | Down |
| PDF.js display | Pixels | Top-left | Down |
| PDF points | Points | Depends on PDF model | Depends on consumer |
| DevExpress DX | 1/100 inch style coordinates | Top-left-oriented usage in this app | Down-oriented usage |
---
## Key Services and Files
### Client services
- `EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Services/AuthService.cs`
- `EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Services/EnvelopeService.cs`
- `EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Services/DocumentService.cs`
- `EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Services/SignatureCacheService.cs`
### Server services
- `EnvelopeGenerator.Server/EnvelopeGenerator.Server/Services/EnvelopeAuthService.cs`
- `EnvelopeGenerator.Server/EnvelopeGenerator.Server/Services/IEnvelopeAuthService.cs`
- `EnvelopeGenerator.Server/EnvelopeGenerator.Server/Services/EnvelopeReceiverAuthorizationService.cs`
- `EnvelopeGenerator.Server/EnvelopeGenerator.Server/Services/EnvelopeReceiverPageDataService.cs`
### Server config and host files
- `EnvelopeGenerator.Server/EnvelopeGenerator.Server/Program.cs`
- `EnvelopeGenerator.Server/EnvelopeGenerator.Server/yarp.json`
---
## Working Rules for This Workspace
- Treat `EnvelopeGenerator.Server` as the active main application host.
- Treat `EnvelopeGenerator.Server.Client` as the active client UI project.
- Prefer current `Server` / `Server.Client` paths over old `WebUI` / `ReceiverUI` references.
- Do not use `EnvelopeGenerator.Web` or `EnvelopeGenerator.ReceiverUI` as the primary implementation target unless explicitly asked.
- Do not modify the legacy VB.NET projects unless explicitly requested.
- For receiver PDF/signature work, prefer the current `PDF.js`-based flow in `EnvelopeReceiverPage.razor`.
- For DevExpress PDF viewer issues, remember server-side services are registered in `EnvelopeGenerator.Server`.
---
**Last Updated:** 2026-06-29

View File

@@ -1,384 +0,0 @@
# Database Structure Context
## Why this file exists
This is the deep-dive DB reference for migration work when schema can change and old DB knowledge may be lost.
- Scope: `EnvelopeGenerator.Domain` entities + `EnvelopeGenerator.Infrastructure/EGDbContext.cs` + live DB metadata snapshot from `DD_ECM`.
- Goal: make schema transitions predictable (what exists, how it is linked, where logic executes).
- Safety: all DB checks used read-only `SELECT` metadata queries; no data mutation.
## Snapshot metadata
- Source DB: `DD_ECM` on `SDD-VHP04-SQL19\DD_TESTING01`
- Snapshot date: `2026-10-20`
- App context: `EnvelopeGenerator.Infrastructure/EGDbContext.cs`
## Fast navigation
- High-level aggregate map: `AGENTS.md` -> `Database` section
- EF model config source: `EnvelopeGenerator.Infrastructure/EGDbContext.cs`
- Stored procedure wrappers:
- `EnvelopeGenerator.Application/Common/SQL/EnvelopeCreateReadSQL.cs`
- `EnvelopeGenerator.Application/Common/SQL/EnvelopeReceiverAddReadSQL.cs`
- `EnvelopeGenerator.Application/Common/SQL/DocumentCreateReadSQL.cs`
- Direct SQL execution in controller:
- `EnvelopeGenerator.Server/EnvelopeGenerator.Server/Controllers/EnvelopeReceiverController.cs`
## Core data model (ER-level)
- `Envelope` (`TBSIG_ENVELOPE`) is root aggregate.
- `Envelope (1) -> (N) Document` via `TBSIG_ENVELOPE_DOCUMENT.ENVELOPE_ID`.
- `Envelope (1) -> (N) History` via `TBSIG_ENVELOPE_HISTORY.ENVELOPE_ID`.
- `Envelope (N) <-> (N) Receiver` through `EnvelopeReceiver` (`TBSIG_ENVELOPE_RECEIVER`, composite PK `{ENVELOPE_ID, RECEIVER_ID}`).
- `Document (1) -> (N) DocReceiverElement` via `TBSIG_DOCUMENT_RECEIVER_ELEMENT.DOCUMENT_ID`.
- `DocReceiverElement (1) -> (N) ElementAnnotation` via `TBSIG_DOCUMENT_RECEIVER_ELEMENT_ANNOTATION.ELEMENT_ID`.
- `DocumentStatus` links `Envelope + Receiver` state in `TBSIG_DOCUMENT_STATUS`.
## Entity <-> table mapping (code-level)
| Entity | Table | PK in code | PK in DB | Important relation notes |
|---|---|---|---|---|
| `Envelope` | `TBSIG_ENVELOPE` | `Id` | `GUID` | Has `Documents`, `Histories`, `EnvelopeReceivers`; unique index on `Uuid` configured in EF |
| `EnvelopeReceiver` | `TBSIG_ENVELOPE_RECEIVER` | `{EnvelopeId, ReceiverId}` | `{ENVELOPE_ID, RECEIVER_ID}` | DB also has `GUID` identity (not modeled as entity PK) |
| `Document` | `TBSIG_ENVELOPE_DOCUMENT` | `Id` | `GUID` | FK `ENVELOPE_ID` |
| `DocReceiverElement` | `TBSIG_DOCUMENT_RECEIVER_ELEMENT` | `Id` | `GUID` | Coordinates persisted here (`POSITION_X/Y`, `WIDTH`, `HEIGHT`, `PAGE`) |
| `ElementAnnotation` | `TBSIG_DOCUMENT_RECEIVER_ELEMENT_ANNOTATION` | `Id` | `GUID` | FK `ELEMENT_ID` |
| `DocumentStatus` | `TBSIG_DOCUMENT_STATUS` | `Id` | `GUID` | FK `ENVELOPE_ID`, `RECEIVER_ID` |
| `History` | `TBSIG_ENVELOPE_HISTORY` | `Id` | `GUID` | `USER_REFERENCE` maps to `Receiver.EmailAddress` and `User.Email` when `IsMigration == false` |
| `Receiver` | `TBSIG_RECEIVER` | `Id` | `GUID` | Unique indexes in code: `Signature`, `EmailAddress` |
| `EnvelopeReceiverReadOnly` | `TBSIG_ENVELOPE_RECEIVER_READ_ONLY` | `Id` | `GUID` | `AddedWho` is FK-like mapping to `Receiver.EmailAddress` |
| `EnvelopeType` | `TBSIG_ENVELOPE_TYPE` | `Id` | `GUID` | Used as optional envelope defaults/template |
| `EmailTemplate` | `TBSIG_EMAIL_TEMPLATE` | `Id` | `GUID` | Trigger-managed `CHANGED_WHEN` |
| `EmailOut` (legacy/bridge) | `TBEMLP_EMAIL_OUT` | `Id` | `GUID` | Internal/obsolete domain class, still registered for DB access |
## Physical schema snapshot (table/column level)
Notes:
- Format per column: `COLUMN_NAME TYPE NULLABLE`.
- This section reflects live DB metadata at snapshot time.
### `TBSIG_ENVELOPE`
- `GUID int NOT NULL`
- `USER_ID int NOT NULL`
- `STATUS int NOT NULL`
- `ENVELOPE_UUID nvarchar(36) NOT NULL`
- `MESSAGE nvarchar(max) NOT NULL`
- `EXPIRES_WHEN datetime NULL`
- `EXPIRES_WARNING_WHEN datetime NULL`
- `VERIFICATION_TYPE tinyint NOT NULL`
- `TITLE nvarchar(128) NULL`
- `CONTRACT_TYPE int NULL`
- `LANGUAGE nvarchar(5) NULL`
- `SEND_REMINDER_EMAILS bit NULL`
- `FIRST_REMINDER_DAYS int NULL`
- `REMINDER_INTERVAL_DAYS int NULL`
- `ENVELOPE_TYPE int NULL`
- `CERTIFICATION_TYPE int NULL`
- `USE_ACCESS_CODE bit NULL`
- `FINAL_EMAIL_TO_CREATOR int NULL`
- `FINAL_EMAIL_TO_RECEIVERS int NULL`
- `EXPIRES_WHEN_DAYS int NULL`
- `EXPIRES_WARNING_WHEN_DAYS int NULL`
- `TFA_ENABLED bit NOT NULL`
- `DOC_RESULT varbinary(max) NULL`
- `LANG_CODE varchar(5) NOT NULL`
- `SOURCE varchar(10) NOT NULL`
- `COMMENT varchar(128) NULL`
- `ADDED_WHEN datetime NOT NULL`
- `CHANGED_WHEN datetime NULL`
### `TBSIG_ENVELOPE_RECEIVER`
- `ENVELOPE_ID int NOT NULL`
- `RECEIVER_ID int NOT NULL`
- `SEQUENCE int NOT NULL`
- `NAME nvarchar(128) NULL`
- `JOB_TITLE nvarchar(128) NULL`
- `COMPANY_NAME nvarchar(128) NULL`
- `PRIVATE_MESSAGE nvarchar(max) NULL`
- `ACCESS_CODE nvarchar(64) NULL`
- `ADDED_WHEN datetime NOT NULL`
- `CHANGED_WHEN datetime NULL`
- `GUID bigint NOT NULL`
- `PHONE_NUMBER varchar(20) NULL`
- `PARENT_ENVELOPE_RECEIVER_ID int NOT NULL`
### `TBSIG_ENVELOPE_DOCUMENT`
- `GUID int NOT NULL`
- `ENVELOPE_ID int NOT NULL`
- `FILENAME nvarchar(256) NOT NULL`
- `FILEPATH nvarchar(256) NOT NULL`
- `ADDED_WHEN datetime NOT NULL`
- `FILENAME_ORIGINAL nvarchar(256) NULL`
- `BYTE_DATA varbinary(max) NULL`
### `TBSIG_DOCUMENT_RECEIVER_ELEMENT`
- `GUID int NOT NULL`
- `DOCUMENT_ID int NOT NULL`
- `RECEIVER_ID int NOT NULL`
- `ELEMENT_TYPE int NOT NULL`
- `POSITION_X float NOT NULL`
- `POSITION_Y float NOT NULL`
- `WIDTH float NOT NULL`
- `HEIGHT float NOT NULL`
- `PAGE int NOT NULL`
- `REQUIRED bit NOT NULL`
- `TOOLTIP nvarchar(512) NULL`
- `READ_ONLY bit NOT NULL`
- `ANNOTATION_INDEX int NOT NULL`
- `ADDED_WHEN datetime NOT NULL`
- `CHANGED_WHEN datetime NULL`
- `CHANGED_WHO nvarchar(100) NULL`
- `FULL_NAME nvarchar(100) NULL`
- `POSITION nvarchar(100) NULL`
- `PLACE nvarchar(100) NULL`
- `INK varbinary(max) NULL`
### `TBSIG_DOCUMENT_RECEIVER_ELEMENT_ANNOTATION`
- `GUID bigint NOT NULL`
- `ELEMENT_ID int NOT NULL`
- `NAME nvarchar(100) NOT NULL`
- `VALUE nvarchar(max) NOT NULL`
- `ADDED_WHEN datetime NOT NULL`
- `CHANGED_WHEN datetime NULL`
- `CHANGED_WHO nvarchar(100) NULL`
- `TYPE nvarchar(50) NOT NULL`
- `POSITION_X float NULL`
- `POSITION_Y float NULL`
- `WIDTH float NULL`
- `HEIGHT float NULL`
### `TBSIG_DOCUMENT_STATUS`
- `GUID int NOT NULL`
- `ENVELOPE_ID int NOT NULL`
- `RECEIVER_ID int NOT NULL`
- `STATUS int NOT NULL`
- `STATUS_CHANGED_WHEN datetime NULL`
- `VALUE nvarchar(max) NULL`
- `ADDED_WHEN datetime NOT NULL`
- `CHANGED_WHEN datetime NULL`
### `TBSIG_ENVELOPE_HISTORY`
- `GUID bigint NOT NULL`
- `ENVELOPE_ID int NOT NULL`
- `USER_REFERENCE nvarchar(128) NOT NULL`
- `STATUS int NOT NULL`
- `ADDED_WHEN datetime NOT NULL`
- `ACTION_DATE datetime NULL`
- `COMMENT nvarchar(max) NULL`
### `TBSIG_RECEIVER`
- `GUID int NOT NULL`
- `EMAIL_ADDRESS nvarchar(128) NOT NULL`
- `SIGNATURE nvarchar(64) NOT NULL`
- `ADDED_WHEN datetime NOT NULL`
- `TOTP_SECRET_KEY nvarchar(max) NULL`
- `TFA_REG_DEADLINE datetime NULL`
- `CULTURE nvarchar(10) NULL`
### `TBSIG_ENVELOPE_RECEIVER_READ_ONLY`
- `GUID bigint NOT NULL`
- `ENVELOPE_ID bigint NOT NULL`
- `RECEIVER_MAIL nvarchar(250) NOT NULL`
- `DATE_VALID date NOT NULL`
- `ADDED_WHO varchar(100) NOT NULL`
- `ADDED_WHEN datetime NULL`
- `CHANGED_WHO varchar(100) NULL`
- `CHANGED_WHEN datetime NULL`
### `TBSIG_ENVELOPE_TYPE`
- `GUID int NOT NULL`
- `TITLE nvarchar(128) NOT NULL`
- `LANGUAGE nvarchar(5) NOT NULL`
- `EXPIRES_DAYS int NULL`
- `CERTIFICATION_TYPE int NULL`
- `USE_ACCESS_CODE bit NULL`
- `FINAL_EMAIL_TO_CREATOR int NULL`
- `FINAL_EMAIL_TO_RECEIVERS int NULL`
- `ADDED_WHEN datetime NOT NULL`
- `CHANGED_WHEN datetime NULL`
- `EXPIRES_WARNING_DAYS int NULL`
- `SEND_REMINDER_EMAILS bit NULL`
- `FIRST_REMINDER_DAYS int NULL`
- `REMINDER_INTERVAL_DAYS int NULL`
- `CONTRACT_TYPE int NULL`
### `TBSIG_EMAIL_TEMPLATE`
- `GUID int NOT NULL`
- `NAME nvarchar(64) NULL`
- `BODY nvarchar(max) NULL`
- `SUBJECT nvarchar(512) NULL`
- `ADDED_WHEN datetime NOT NULL`
- `CHANGED_WHEN datetime NULL`
- `LANG_CODE varchar(5) NOT NULL`
### `TBEMLP_EMAIL_OUT`
- `GUID int NOT NULL`
- `REMINDER_TYPE_ID int NOT NULL`
- `SENDING_PROFILE int NOT NULL`
- `REFERENCE_ID int NOT NULL`
- `REFERENCE_STRING varchar(200) NULL`
- `ENTITY_ID int NULL`
- `WF_ID int NOT NULL`
- `WF_REFERENCE varchar(200) NULL`
- `EMAIL_ADRESS varchar(1000) NOT NULL`
- `EMAIL_SUBJ varchar(500) NOT NULL`
- `EMAIL_BODY varchar(max) NOT NULL`
- `EMAIL_ATTMT1 varchar(512) NULL`
- `EMAIL_SENT datetime NULL`
- `COMMENT varchar(500) NULL`
- `ADDED_WHO varchar(50) NOT NULL`
- `ADDED_WHEN datetime NULL`
- `CHANGED_WHO varchar(50) NULL`
- `CHANGED_WHEN datetime NULL`
- `ERROR_TIMESTAMP datetime NULL`
- `ERROR_MSG varchar(900) NULL`
- `ATTACHMENT1_BYTES varbinary(max) NULL`
- `ATT1_RELATED_ID bigint NULL`
- `ATT1_REL_TYPE varchar(20) NULL`
## Constraints and indexes (relevant)
From EF + DB metadata snapshot:
- `TBSIG_ENVELOPE`: PK `PK_TBSIG_ENVELOPE (GUID)`.
- `TBSIG_ENVELOPE_RECEIVER`: composite PK `PK_TBSIG_ENVELOPE_RECEIVER (ENVELOPE_ID, RECEIVER_ID)`.
- `TBSIG_DOCUMENT_RECEIVER_ELEMENT`: PK `GUID`, plus unique key `UQTBSIG_DOCUMENT_RECEIVER_ELEMENT_DocID_PosX_PosY (DOCUMENT_ID, POSITION_X, POSITION_Y)`.
- `TBSIG_RECEIVER`: PK `GUID`, unique key `UQTBSIG_RECEIVER (EMAIL_ADDRESS)`.
- `TBEMLP_EMAIL_OUT`: PK `GUID`, FK `FK_TBEMLP_EMAIL_OUT_SENDING_PROFILE`.
## Stored procedures: behavior and code call sites
### Active in .NET code path
1) `PRSIG_API_CREATE_ENVELOPE`
- Purpose: creates envelope row (`STATUS=1002`, `SOURCE='API'`, defaults) and returns `ENVELOPE_UUID` via output.
- DB writes: `TBSIG_ENVELOPE`.
- Called by:
- SQL template `EnvelopeGenerator.Application/Common/SQL/EnvelopeCreateReadSQL.cs`
- executor `EnvelopeGenerator.Infrastructure/Executor/EnvelopeExecutor.cs`
- used in `EnvelopeReceiverController.CreateAsync` and `CreateEnvelopeReceiverCommandHandler`.
2) `PRSIG_API_CREATE_RECEIVER`
- Purpose: upserts `TBSIG_RECEIVER` by email, adds envelope assignment row with generated access code.
- DB writes: `TBSIG_RECEIVER`, `TBSIG_ENVELOPE_RECEIVER`.
- Called by:
- SQL template `EnvelopeGenerator.Application/Common/SQL/EnvelopeReceiverAddReadSQL.cs`
- executor `EnvelopeGenerator.Infrastructure/Executor/EnvelopeReceiverExecutor.cs`
- used in `EnvelopeReceiverController.CreateAsync` and `CreateEnvelopeReceiverCommandHandler`.
3) `PRSIG_API_ADD_DOC`
- Purpose: stores one document for envelope if absent; returns doc id.
- DB writes: `TBSIG_ENVELOPE_DOCUMENT`.
- Called by:
- SQL template `EnvelopeGenerator.Application/Common/SQL/DocumentCreateReadSQL.cs`
- executor `EnvelopeGenerator.Infrastructure/Executor/DocumentExecutor.cs`
- used in `EnvelopeReceiverController.CreateAsync`.
4) `PRSIG_API_ADD_DOC_RECEIVER_ELEM`
- Purpose: inserts signature element record if `(DOC_ID, RECEIVER_ID)` pair does not exist.
- DB writes: `TBSIG_DOCUMENT_RECEIVER_ELEMENT`.
- Called directly by:
- `EnvelopeGenerator.Server/EnvelopeGenerator.Server/Controllers/EnvelopeReceiverController.cs`
5) `PRSIG_API_ADD_HISTORY_STATE`
- Purpose: appends history row with status and user reference.
- DB writes: `TBSIG_ENVELOPE_HISTORY`.
- Called directly by:
- `EnvelopeGenerator.Server/EnvelopeGenerator.Server/Controllers/EnvelopeReceiverController.cs`
### Legacy/other path references in repo
- `PRSIG_GET_ENVELOPES_FOR_USER` and `PRSIG_GET_CLOSED_ENVELOPES_FOR_USER` are called in legacy VB path:
- `EnvelopeGenerator.CommonServices/Models/EnvelopeModel.vb`
- `PRSIG_API_WITHDRAW_ENVELOPE` exists in DB snapshot but no active `.cs` call site found in current server flow.
## Trigger behavior and where they are wired
## How trigger names are wired in app
- Config source: `EnvelopeGenerator.Server/EnvelopeGenerator.Server/appsettings.json` -> `DbTriggerParams`.
- DI binding: `EnvelopeGenerator.Infrastructure/DependencyInjection.cs` (`AddDbTriggerParams`).
- Model binding: `EnvelopeGenerator.Infrastructure/EGDbContext.cs` -> `AddTrigger<T>()` -> `ToTable(tb => tb.HasTrigger(triggerName))`.
### Trigger definitions observed in DB snapshot
1) `TBSIG_DOCUMENT_RECEIVER_ELEMENT_AFT_INS`
- Fires on insert of `TBSIG_DOCUMENT_RECEIVER_ELEMENT`.
- Normalizes/rewrites `POSITION_X`, `POSITION_Y` and sets `CHANGED_WHO='TBSIG_DOC_REC_EL_AFT_INS'`.
2) `TBSIG_DOCUMENT_RECEIVER_ELEMENT_AFT_UPD`
- Fires on update of `TBSIG_DOCUMENT_RECEIVER_ELEMENT`.
- Rewrites coordinates and `CHANGED_WHO='TBSIG_DOC_REC_EL_AFT_UPD'` except when `CHANGED_WHO='OFFSET_Y_POS'`.
3) `TBSIG_ENVELOPE_HISTORY_AFT_INS`
- Fires on history insert.
- Central state machine side-effects:
- syncs `TBSIG_ENVELOPE.STATUS` for selected `100x` states,
- on `1003` applies Y-offset update to receiver elements,
- on `2005` calculates partial/fully signed and may insert follow-up `1006` history,
- on `2007` (type-dependent reject path) writes rejection emails into `TBEMLP_EMAIL_OUT` and updates envelope rejected state,
- on `3001` invitation-message counting can move envelope to `1004`.
4) `TBSIG_EMAIL_TEMPLATE_AFT_UPD`
- Fires on update of `TBSIG_EMAIL_TEMPLATE`.
- Sets `CHANGED_WHEN = GETDATE()`.
5) `TBSIG_ENVELOPE_RECEIVER_READ_ONLY_UPD`
- Fires on update of `TBSIG_ENVELOPE_RECEIVER_READ_ONLY`.
- Updates config timestamp row (`TBSIG_CONFIG.GUID = 1`).
6) `TBEMLP_EMAIL_OUT_AFT_UPD`
- Fires on update of `TBEMLP_EMAIL_OUT`.
- When `EMAIL_SENT` changes and `WF_ID in (3001..3005)`, writes corresponding history entries to `TBSIG_ENVELOPE_HISTORY`.
### Trigger mismatch alert (important)
- `DbTriggerParams` contains `TBEMLP_EMAIL_OUT_AFT_INS`, but this trigger was not found in the snapshot DB (`ExistsInDb = 0`).
- `TBEMLP_EMAIL_OUT_AFT_UPD` exists and is active.
This should be treated as environment drift and validated before migrations.
## Known model-vs-DB drift risks
These are not necessarily bugs, but they are migration risk hotspots:
- `Receiver.EmailAddress` is `[StringLength(250)]` in entity, DB column is `nvarchar(128)`.
- `EnvelopeReceiverReadOnly.AddedWho` entity expects `nvarchar(250)`, DB column is `varchar(100)`.
- `EnvelopeReceiver` entity does not model DB columns `GUID` and `PARENT_ENVELOPE_RECEIVER_ID`.
- `Envelope` entity does not explicitly model some DB columns (`VERIFICATION_TYPE`, `LANG_CODE`, `SOURCE`).
- `EmailOut` entity has `[NotMapped] AddedWhen`, while DB has `ADDED_WHEN` column.
Before schema changes, reconcile these deltas intentionally (either mapping update or documented accepted divergence).
## Migration-safe operating rules
- Do not rely on implicit trigger behavior; document expected side effects in feature code touching history/status flows.
- Any change to `TBSIG_ENVELOPE_HISTORY` semantics requires trigger impact review first.
- Keep `DbTriggerParams` synchronized with actual DB trigger inventory per environment.
- For stored procedure changes, update both:
- SQL wrapper templates (`Application/Common/SQL/*`)
- direct SQL callers in controllers/executors.
## Refresh checklist (read-only)
Run these metadata checks before/after DB migration:
1. Column inventory per table (`INFORMATION_SCHEMA.COLUMNS`).
2. Constraint inventory (`INFORMATION_SCHEMA.TABLE_CONSTRAINTS` + `KEY_COLUMN_USAGE`).
3. Trigger inventory (`sys.triggers`) and definitions (`OBJECT_DEFINITION`).
4. Stored procedure inventory (`sys.procedures`) and definitions (`OBJECT_DEFINITION`).
5. Repo call-site grep for `PRSIG_` names to detect dead or missing integration points.

View File

@@ -1,40 +0,0 @@
using DigitalData.Core.Exceptions;
using FluentValidation;
using MediatR;
namespace EnvelopeGenerator.Application.Common.Behaviors;
public class ValidationBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
where TRequest : notnull
{
private readonly IEnumerable<IValidator<TRequest>> _validators;
public ValidationBehavior(IEnumerable<IValidator<TRequest>> validators)
{
_validators = validators;
}
public async Task<TResponse> Handle(
TRequest request,
RequestHandlerDelegate<TResponse> next,
CancellationToken cancellationToken)
{
if (_validators.Any())
{
var context = new ValidationContext<TRequest>(request);
var failures = _validators
.Select(v => v.Validate(context))
.SelectMany(r => r.Errors)
.Where(f => f is not null)
.ToList();
if (failures.Count > 0)
{
var message = string.Join("; ", failures.Select(f => f.ErrorMessage).Distinct());
throw new BadRequestException(message);
}
}
return await next();
}
}

View File

@@ -1,36 +0,0 @@
using DigitalData.Core.Abstraction.Application.Repository;
using MediatR;
namespace EnvelopeGenerator.Application.Common.Commands;
/// <summary>
///
/// </summary>
/// <typeparam name="TCommand"></typeparam>
/// <typeparam name="TEntity"></typeparam>
public class CreateCommandHandler<TCommand, TEntity> : IRequestHandler<TCommand, TEntity>
where TCommand : class, IRequest<TEntity>
where TEntity : class
{
/// <summary>
///
/// </summary>
protected readonly IRepository<TEntity> Repository;
/// <summary>
///
/// </summary>
/// <param name="repository"></param>
public CreateCommandHandler(IRepository<TEntity> repository)
{
Repository = repository;
}
/// <summary>
///
/// </summary>
/// <param name="request"></param>
/// <param name="cancel"></param>
/// <returns></returns>
public Task<TEntity> Handle(TCommand request, CancellationToken cancel) => Repository.CreateAsync(request, cancel);
}

View File

@@ -1,59 +0,0 @@
using DigitalData.Core.Abstraction.Application.Repository;
using MediatR;
using System.Linq.Expressions;
namespace EnvelopeGenerator.Application.Common.Commands;
/// <summary>
///
/// </summary>
/// <typeparam name="TUpdateDto"></typeparam>
/// <typeparam name="TEntity"></typeparam>
public abstract record UpdateCommand<TUpdateDto, TEntity> : IRequest where TUpdateDto : class where TEntity : class
{
/// <summary>
///
/// </summary>
public TUpdateDto Update { get; init; } = null!;
/// <summary>
///
/// </summary>
/// <returns></returns>
public abstract Expression<Func<TEntity, bool>> BuildQueryExpression();
}
/// <summary>
///
/// </summary>
/// <typeparam name="TCommand"></typeparam>
/// <typeparam name="TUpdateDto"></typeparam>
/// <typeparam name="TEntity"></typeparam>
public class UpdateCommandHandler<TCommand, TUpdateDto, TEntity> : IRequestHandler<TCommand>
where TUpdateDto : class
where TEntity : class
where TCommand : UpdateCommand<TUpdateDto, TEntity>
{
/// <summary>
///
/// </summary>
protected readonly IRepository<TEntity> Repository;
/// <summary>
///
/// </summary>
/// <param name="repository"></param>
public UpdateCommandHandler(IRepository<TEntity> repository)
{
Repository = repository;
}
/// <summary>
///
/// </summary>
/// <param name="request"></param>
/// <param name="cancel"></param>
/// <returns></returns>
public Task Handle(TCommand request, CancellationToken cancel)
=> Repository.UpdateAsync(request.Update, request.BuildQueryExpression(), cancel);
}

View File

@@ -1,17 +0,0 @@
namespace EnvelopeGenerator.Application.Common.Configurations;
/// <summary>
///
/// </summary>
public class ESignParams
{
/// <summary>
///
/// </summary>
public required string ApplicationName { get; set; }
/// <summary>
///
/// </summary>
public string DefaultReason { get; set; } = string.Empty;
}

View File

@@ -1,17 +0,0 @@
namespace EnvelopeGenerator.Application.Common.Configurations;
/// <summary>
///
/// </summary>
public class SignatureFieldParams
{
/// <summary>
///
/// </summary>
public double Width { get; init; } = 1.77;
/// <summary>
///
/// </summary>
public double Height { get; init; } = 1.96;
}

View File

@@ -8,77 +8,42 @@ public record AnnotationCreateDto
/// <summary> /// <summary>
/// ///
/// </summary> /// </summary>
public long Id { get; set; } public int ElementId { get; init; }
/// <summary> /// <summary>
/// ///
/// </summary> /// </summary>
[Obsolete("Not required for DevExpress")] public string Name { get; init; } = null!;
public int ElementId { get; init; } = -1;
/// <summary> /// <summary>
/// ///
/// </summary> /// </summary>
[Obsolete("Not required for DevExpress")] public string Value { get; init; } = null!;
public string Name { get; init; } = string.Empty;
/// <summary> /// <summary>
/// ///
/// </summary> /// </summary>
[Obsolete("Not required for DevExpress")] public string Type { get; init; } = null!;
public string Value { get; init; } = string.Empty;
/// <summary> /// <summary>
/// ///
/// </summary> /// </summary>
[Obsolete("Not required for DevExpress")]
public string Type { get; init; } = string.Empty;
/// <summary>
/// Horizontal position of the signature field on the page.
/// <br/><br/>
/// <b>Unit:</b> INCHES (GdPicture14 native), origin at the <b>top-left</b> corner of the page, X increases to the right.
/// <br/>
/// <b>Conversion to DevExpress:</b> Multiply by 100 (DX uses 1/100 inch).
/// Convert: <c>xDX = xInches * 100.0</c>
/// <br/>
/// <b>Conversion to PDF Points:</b> Multiply by 72 (PSPDFKit, iText7 use 1/72 inch).
/// Convert: <c>xPt = xInches * 72.0</c>
/// </summary>
public double? X { get; init; } public double? X { get; init; }
/// <summary> /// <summary>
/// Vertical position of the signature field on the page. ///
/// <br/><br/>
/// <b>Unit:</b> INCHES (GdPicture14 native), origin at the <b>top-left</b> corner of the page, Y increases downward.
/// <br/>
/// <b>Conversion to DevExpress:</b> Multiply by 100 (DX uses 1/100 inch).
/// Convert: <c>yDX = yInches * 100.0</c>
/// <br/>
/// <b>Conversion to PDF Points (top-left origin):</b> Multiply by 72.
/// Convert: <c>yPt = yInches * 72.0</c>
/// <br/>
/// <b>Conversion to PDF Points (bottom-left origin - iText7):</b> Y-flip required.
/// Convert: <c>yPt = (pageHeightInches - yInches - elemHeightInches) * 72.0</c>
/// </summary> /// </summary>
public double? Y { get; init; } public double? Y { get; init; }
/// <summary> /// <summary>
/// ///
/// </summary> /// </summary>
[Obsolete("Not required for DevExpress")]
public double? Width { get; init; } public double? Width { get; init; }
/// <summary> /// <summary>
/// ///
/// </summary> /// </summary>
[Obsolete("Not required for DevExpress")]
public double? Height { get; init; } public double? Height { get; init; }
/// <summary>
/// Added to eliminate the need for SignatureDto in DevExpress
/// </summary>
public int? Page { get; init; }
} }
/// <summary> /// <summary>

View File

@@ -1,8 +1,11 @@
namespace EnvelopeGenerator.Application.Common.Dto; using Microsoft.AspNetCore.Mvc;
namespace EnvelopeGenerator.Application.Common.Dto;
/// <summary> /// <summary>
/// Data Transfer Object representing configuration settings. /// Data Transfer Object representing configuration settings.
/// </summary> /// </summary>
[ApiExplorerSettings(IgnoreApi = true)]
public class ConfigDto public class ConfigDto
{ {
/// <summary> /// <summary>

View File

@@ -1,27 +0,0 @@
namespace EnvelopeGenerator.Application.Common.Dto;
/// <summary>
/// Data Transfer Object representing document metadata and elements without binary payload.
/// </summary>
public record DocumentDetailsDto
{
/// <summary>
/// Gets or sets the unique identifier of the document.
/// </summary>
public int Id { get; init; }
/// <summary>
/// Gets or sets the envelope ID to which the document belongs.
/// </summary>
public int EnvelopeId { get; init; }
/// <summary>
/// Gets or sets the date and time when the document was added.
/// </summary>
public DateTime AddedWhen { get; init; }
/// <summary>
/// Gets or sets the collection of elements associated with the document for receiver interactions, if any.
/// </summary>
public IEnumerable<DocReceiverElementDto>? Elements { get; init; }
}

View File

@@ -1,27 +1,35 @@
namespace EnvelopeGenerator.Application.Common.Dto; using Microsoft.AspNetCore.Mvc;
namespace EnvelopeGenerator.Application.Common.Dto;
/// <summary> /// <summary>
/// Data Transfer Object representing a document within an envelope, including optional binary data and form elements. /// Data Transfer Object representing a document within an envelope, including optional binary data and form elements.
/// </summary> /// </summary>
public record DocumentDto : DocumentDetailsDto [ApiExplorerSettings(IgnoreApi = true)]
public class DocumentDto
{ {
/// <summary> /// <summary>
/// /// Gets or sets the unique identifier of the document.
/// </summary> /// </summary>
public DocumentDto() { } public int Id { get; set; }
/// <summary> /// <summary>
/// /// Gets or sets the envelope ID to which the document belongs.
/// </summary> /// </summary>
/// <param name="byteData"></param> public int EnvelopeId { get; set; }
/// <param name="details"></param>
public DocumentDto(byte[] byteData, DocumentDetailsDto details) : base(details) /// <summary>
{ /// Gets or sets the date and time when the document was added.
ByteData = byteData; /// </summary>
} public DateTime AddedWhen { get; set; }
/// <summary> /// <summary>
/// Gets or sets the binary data of the document, if available. /// Gets or sets the binary data of the document, if available.
/// </summary> /// </summary>
public byte[]? ByteData { get; set; } public byte[]? ByteData { get; set; }
/// <summary>
/// Gets or sets the collection of elements associated with the document for receiver interactions, if any.
/// </summary>
public IEnumerable<SignatureDto>? Elements { get; set; }
} }

View File

@@ -1,10 +1,12 @@
using EnvelopeGenerator.Domain.Constants; using EnvelopeGenerator.Domain.Constants;
using Microsoft.AspNetCore.Mvc;
namespace EnvelopeGenerator.Application.Common.Dto; namespace EnvelopeGenerator.Application.Common.Dto;
/// <summary> /// <summary>
/// Data Transfer Object representing the status of a document for a specific receiver. /// Data Transfer Object representing the status of a document for a specific receiver.
/// </summary> /// </summary>
[ApiExplorerSettings(IgnoreApi = true)]
public class DocumentStatusDto public class DocumentStatusDto
{ {
/// <summary> /// <summary>

View File

@@ -1,8 +1,11 @@
namespace EnvelopeGenerator.Application.Common.Dto; using Microsoft.AspNetCore.Mvc;
namespace EnvelopeGenerator.Application.Common.Dto
{
/// <summary> /// <summary>
/// ///
/// </summary> /// </summary>
[ApiExplorerSettings(IgnoreApi = true)]
public record EmailTemplateDto public record EmailTemplateDto
{ {
/// <summary> /// <summary>
@@ -13,30 +16,16 @@ public record EmailTemplateDto
/// <summary> /// <summary>
/// ///
/// </summary> /// </summary>
public string Name { get; set; } = null!; public required string Name { get; init; }
/// <summary> /// <summary>
/// Das Datum und die Uhrzeit, wann die Vorlage hinzugefügt wurde. ///
/// </summary> /// </summary>
public DateTime AddedWhen { get; set; } public required string Body { get; set; }
/// <summary> /// <summary>
/// Der Inhalt (Body) der E-Mail-Vorlage. Kann null sein. ///
/// </summary> /// </summary>
public string? Body { get; set; } public required string Subject { get; set; }
/// <summary>
/// Der Betreff der E-Mail-Vorlage. Kann null sein.
/// </summary>
public string? Subject { get; set; }
/// <summary>
/// Der Sprachcode der E-Mail-Vorlage.
/// </summary>
public string LangCode { get; set; } = null!;
/// <summary>
/// Das Datum und die Uhrzeit, wann die Vorlage zuletzt geändert wurde. Kann null sein.
/// </summary>
public DateTime? ChangedWhen { get; set; }
}; };
}

View File

@@ -1,16 +1,16 @@
using DigitalData.EmailProfilerDispatcher.Abstraction.Attributes; using DigitalData.EmailProfilerDispatcher.Abstraction.Attributes;
using DigitalData.UserManager.Application.DTOs.User; using DigitalData.UserManager.Application.DTOs.User;
using EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiver;
using EnvelopeGenerator.Application.Common.Dto.History;
using EnvelopeGenerator.Domain.Constants; using EnvelopeGenerator.Domain.Constants;
using EnvelopeGenerator.Domain.Entities; using EnvelopeGenerator.Domain.Entities;
using EnvelopeGenerator.Domain.Interfaces; using Microsoft.AspNetCore.Mvc;
namespace EnvelopeGenerator.Application.Common.Dto; namespace EnvelopeGenerator.Application.Common.Dto;
/// <summary> /// <summary>
/// ///
/// </summary> /// </summary>
public record EnvelopeDto : IEnvelope [ApiExplorerSettings(IgnoreApi = true)]
public record EnvelopeDto
{ {
/// <summary> /// <summary>
/// ///
@@ -62,7 +62,7 @@ public record EnvelopeDto : IEnvelope
/// <summary> /// <summary>
/// ///
/// </summary> /// </summary>
public ContractType? ContractType { get; set; } public int? ContractType { get; set; }
/// <summary> /// <summary>
/// Default value is 'de-DE' /// Default value is 'de-DE'
@@ -74,52 +74,10 @@ public record EnvelopeDto : IEnvelope
/// </summary> /// </summary>
public int? EnvelopeTypeId { get; set; } public int? EnvelopeTypeId { get; set; }
// TODO: use ReadAndConfirm property name
/// <summary> /// <summary>
/// ///
/// </summary> /// </summary>
[Obsolete("Use EnvelopeExtensions.IsReadAndConfirm extension metot instead.")] public int? CertificationType { get; set; }
public bool ReadOnly => this.IsReadAndConfirm();
/// <summary>
///
/// </summary>
public CertificationType? CertificationType { get; set; }
/// <summary>
///
/// </summary>
public int? FinalEmailToCreator { get; set; }
/// <summary>
///
/// </summary>
public int? FinalEmailToReceivers { get; set; }
/// <summary>
///
/// </summary>
public bool? SendReminderEmails { get; set; }
/// <summary>
///
/// </summary>
public int? FirstReminderDays { get; set; }
/// <summary>
///
/// </summary>
public int? ReminderIntervalDays { get; set; }
/// <summary>
///
/// </summary>
public int? ExpiresWhenDays { get; set; }
/// <summary>
///
/// </summary>
public int? ExpiresWarningWhenDays { get; set; }
/// <summary> /// <summary>
/// ///
@@ -160,14 +118,4 @@ public record EnvelopeDto : IEnvelope
/// ///
/// </summary> /// </summary>
public IEnumerable<DocumentDto>? Documents { get; set; } public IEnumerable<DocumentDto>? Documents { get; set; }
/// <summary>
///
/// </summary>
public IEnumerable<EnvelopeReceiverDto>? EnvelopeReceivers { get; set; }
/// <summary>
/// Envelope history entries tracking actions like DocumentSigned, EnvelopeOpened, etc.
/// </summary>
public IEnumerable<HistoryDto>? Histories { get; set; }
} }

View File

@@ -1,12 +1,13 @@
using DigitalData.EmailProfilerDispatcher.Abstraction.Attributes; using DigitalData.EmailProfilerDispatcher.Abstraction.Attributes;
using EnvelopeGenerator.Application.Common.Dto.Receiver; using EnvelopeGenerator.Application.Common.Dto.Receiver;
using EnvelopeGenerator.Domain.Constants; using Microsoft.AspNetCore.Mvc;
namespace EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiver; namespace EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiver;
/// <summary> /// <summary>
/// ///
/// </summary> /// </summary>
[ApiExplorerSettings(IgnoreApi = true)]
public record EnvelopeReceiverDto public record EnvelopeReceiverDto
{ {
/// <summary> /// <summary>
@@ -60,11 +61,6 @@ public record EnvelopeReceiverDto
/// </summary> /// </summary>
public string? PrivateMessage { get; init; } public string? PrivateMessage { get; init; }
/// <summary>
///
/// </summary>
public string? AccessCode { get; init; }
/// <summary> /// <summary>
/// ///
/// </summary> /// </summary>
@@ -79,13 +75,4 @@ public record EnvelopeReceiverDto
/// ///
/// </summary> /// </summary>
public bool HasPhoneNumber { get; init; } public bool HasPhoneNumber { get; init; }
/// <summary>
/// Indicates whether this receiver has signed the envelope.
/// Checks if there is a DocumentSigned history entry for this receiver in the envelope's history.
/// </summary>
public bool Signed => Envelope?.Histories?.Any(h =>
h.Receiver?.Id == ReceiverId &&
h.Status == EnvelopeStatus.DocumentSigned
) ?? false;
} }

View File

@@ -1,8 +1,11 @@
namespace EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiver; using Microsoft.AspNetCore.Mvc;
namespace EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiver;
/// <summary> /// <summary>
/// ///
/// </summary> /// </summary>
[ApiExplorerSettings(IgnoreApi = true)]
public record EnvelopeReceiverSecretDto : EnvelopeReceiverDto public record EnvelopeReceiverSecretDto : EnvelopeReceiverDto
{ {
/// <summary> /// <summary>

View File

@@ -1,4 +1,5 @@
using System.ComponentModel.DataAnnotations; using Microsoft.AspNetCore.Mvc;
using System.ComponentModel.DataAnnotations;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
namespace EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiverReadOnly; namespace EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiverReadOnly;
@@ -7,6 +8,7 @@ namespace EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiverReadOnly;
/// ///
/// </summary> /// </summary>
/// <param name="DateValid"></param> /// <param name="DateValid"></param>
[ApiExplorerSettings(IgnoreApi = true)]
public record EnvelopeReceiverReadOnlyCreateDto( public record EnvelopeReceiverReadOnlyCreateDto(
DateTime DateValid) DateTime DateValid)
{ {

View File

@@ -1,4 +1,6 @@
using EnvelopeGenerator.Application.Common.Dto.Receiver; using EnvelopeGenerator.Application.Common.Dto;
using EnvelopeGenerator.Application.Common.Dto.Receiver;
using Microsoft.AspNetCore.Mvc;
namespace EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiverReadOnly; namespace EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiverReadOnly;
@@ -6,6 +8,7 @@ namespace EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiverReadOnly;
/// Represents a read-only Data Transfer Object (DTO) for an envelope receiver. /// Represents a read-only Data Transfer Object (DTO) for an envelope receiver.
/// Contains information about the receiver, associated envelope, and audit details. /// Contains information about the receiver, associated envelope, and audit details.
/// </summary> /// </summary>
[ApiExplorerSettings(IgnoreApi = true)]
public class EnvelopeReceiverReadOnlyDto public class EnvelopeReceiverReadOnlyDto
{ {
/// <summary> /// <summary>

View File

@@ -1,8 +1,11 @@
namespace EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiverReadOnly; using Microsoft.AspNetCore.Mvc;
namespace EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiverReadOnly;
/// <summary> /// <summary>
/// Data Transfer Object for updating a read-only envelope receiver. /// Data Transfer Object for updating a read-only envelope receiver.
/// </summary> /// </summary>
[ApiExplorerSettings(IgnoreApi = true)]
public class EnvelopeReceiverReadOnlyUpdateDto public class EnvelopeReceiverReadOnlyUpdateDto
{ {
/// <summary> /// <summary>

View File

@@ -1,10 +1,11 @@
using EnvelopeGenerator.Domain.Constants; using Microsoft.AspNetCore.Mvc;
namespace EnvelopeGenerator.Application.Common.Dto; namespace EnvelopeGenerator.Application.Common.Dto;
/// <summary> /// <summary>
/// Data Transfer Object representing a type of envelope with its configuration settings. /// Data Transfer Object representing a type of envelope with its configuration settings.
/// </summary> /// </summary>
[ApiExplorerSettings(IgnoreApi = true)]
public class EnvelopeTypeDto public class EnvelopeTypeDto
{ {
/// <summary> /// <summary>
@@ -30,7 +31,7 @@ public class EnvelopeTypeDto
/// <summary> /// <summary>
/// Gets or sets the certification type identifier. /// Gets or sets the certification type identifier.
/// </summary> /// </summary>
public CertificationType? CertificationType { get; set; } public int? CertificationType { get; set; }
/// <summary> /// <summary>
/// Gets or sets a value indicating whether an access code is required. /// Gets or sets a value indicating whether an access code is required.
@@ -80,5 +81,5 @@ public class EnvelopeTypeDto
/// <summary> /// <summary>
/// Gets or sets the contract type associated with the envelope type. /// Gets or sets the contract type associated with the envelope type.
/// </summary> /// </summary>
public ContractType? ContractType { get; set; } public int? ContractType { get; set; }
} }

View File

@@ -7,9 +7,7 @@ using EnvelopeGenerator.Application.Common.Dto.Receiver;
using EnvelopeGenerator.Application.Common.Extensions; using EnvelopeGenerator.Application.Common.Extensions;
using EnvelopeGenerator.Domain.Entities; using EnvelopeGenerator.Domain.Entities;
using EnvelopeGenerator.Application.Common.Dto; namespace EnvelopeGenerator.Application.Common.Dto;
namespace EnvelopeGenerator.Application.Common;
/// <summary> /// <summary>
/// Represents the AutoMapper profile configuration for mapping between /// Represents the AutoMapper profile configuration for mapping between
@@ -25,42 +23,36 @@ public class MappingProfile : Profile
{ {
// Entity to DTO mappings // Entity to DTO mappings
CreateMap<Config, ConfigDto>(); CreateMap<Config, ConfigDto>();
CreateMap<DocReceiverElement, DocReceiverElementDto>(); CreateMap<Signature, SignatureDto>();
CreateMap<DocumentStatus, DocumentStatusDto>(); CreateMap<DocumentStatus, DocumentStatusDto>();
CreateMap<EmailTemplate, EmailTemplateDto>(); CreateMap<EmailTemplate, EmailTemplateDto>();
CreateMap<Envelope, EnvelopeDto>() CreateMap<Envelope, EnvelopeDto>();
.ForMember(dest => dest.EnvelopeTypeTitle,
opt => opt.MapFrom(src => src.Type != null ? src.Type.Title : null));
CreateMap<Document, DocumentDto>(); CreateMap<Document, DocumentDto>();
CreateMap<Document, DocumentDetailsDto>(); CreateMap<Domain.Entities.History, HistoryDto>();
CreateMap<History, HistoryDto>().ForMember(dest => dest.ActionDate, opt => opt.MapFrom(src => src.ChangedWhen)); CreateMap<Domain.Entities.History, HistoryCreateDto>();
CreateMap<History, HistoryCreateDto>().ForMember(dest => dest.ActionDate, opt => opt.MapFrom(src => src.ChangedWhen)); CreateMap<Domain.Entities.EnvelopeReceiver, EnvelopeReceiverDto>();
CreateMap<EnvelopeReceiver, EnvelopeReceiverDto>(); CreateMap<Domain.Entities.EnvelopeReceiver, EnvelopeReceiverSecretDto>();
CreateMap<EnvelopeReceiver, EnvelopeReceiverSecretDto>();
CreateMap<EnvelopeType, EnvelopeTypeDto>(); CreateMap<EnvelopeType, EnvelopeTypeDto>();
CreateMap<Receiver, ReceiverDto>(); CreateMap<Domain.Entities.Receiver, ReceiverDto>();
CreateMap<EnvelopeReceiverReadOnly, EnvelopeReceiverReadOnlyDto>(); CreateMap<Domain.Entities.EnvelopeReceiverReadOnly, EnvelopeReceiverReadOnlyDto>();
CreateMap<ElementAnnotation, AnnotationDto>(); CreateMap<ElementAnnotation, AnnotationDto>();
// DTO to Entity mappings // DTO to Entity mappings
CreateMap<ConfigDto, Config>(); CreateMap<ConfigDto, Config>();
CreateMap<DocReceiverElementDto, DocReceiverElement>(); CreateMap<SignatureDto, Signature>();
CreateMap<Signature, DocReceiverElement>()
.ForMember(dest => dest.Ink, opt => opt.MapFrom(src => src.DataUrl.MapDataUrlToRequiredBytes()))
.MapChangedWhen();
CreateMap<DocumentStatusDto, DocumentStatus>(); CreateMap<DocumentStatusDto, DocumentStatus>();
CreateMap<EmailTemplateDto, EmailTemplate>(); CreateMap<EmailTemplateDto, EmailTemplate>();
CreateMap<EnvelopeDto, Envelope>(); CreateMap<EnvelopeDto, Envelope>();
CreateMap<DocumentDto, Document>(); CreateMap<DocumentDto, Document>();
CreateMap<HistoryDto, History>().ForMember(dest => dest.ChangedWhen, opt => opt.MapFrom(src => src.ActionDate)); CreateMap<HistoryDto, Domain.Entities.History>();
CreateMap<HistoryCreateDto, History>().ForMember(dest => dest.ChangedWhen, opt => opt.MapFrom(src => src.ActionDate)); CreateMap<HistoryCreateDto, Domain.Entities.History>();
CreateMap<EnvelopeReceiverDto, EnvelopeReceiver>(); CreateMap<EnvelopeReceiverDto, Domain.Entities.EnvelopeReceiver>();
CreateMap<EnvelopeTypeDto, EnvelopeType>(); CreateMap<EnvelopeTypeDto, EnvelopeType>();
CreateMap<ReceiverDto, Receiver>().ForMember(rcv => rcv.EnvelopeReceivers, rcvReadDto => rcvReadDto.Ignore()); CreateMap<ReceiverDto, Domain.Entities.Receiver>().ForMember(rcv => rcv.EnvelopeReceivers, rcvReadDto => rcvReadDto.Ignore());
CreateMap<EnvelopeReceiverReadOnlyCreateDto, EnvelopeReceiverReadOnly>(); CreateMap<EnvelopeReceiverReadOnlyCreateDto, Domain.Entities.EnvelopeReceiverReadOnly>();
CreateMap<EnvelopeReceiverReadOnlyUpdateDto, EnvelopeReceiverReadOnly>(); CreateMap<EnvelopeReceiverReadOnlyUpdateDto, Domain.Entities.EnvelopeReceiverReadOnly>();
CreateMap<AnnotationCreateDto, ElementAnnotation>() CreateMap<AnnotationCreateDto, ElementAnnotation>()
.MapAddedWhen(); .ForMember(dest => dest.AddedWhen, opt => opt.MapFrom(_ => DateTime.UtcNow));
// Messaging mappings // Messaging mappings
// for GTX messaging // for GTX messaging

View File

@@ -1,6 +1,9 @@
namespace EnvelopeGenerator.Application.Common.Dto.Messaging; using Microsoft.AspNetCore.Mvc;
namespace EnvelopeGenerator.Application.Common.Dto.Messaging;
/// <summary> /// <summary>
/// ///
/// </summary> /// </summary>
[ApiExplorerSettings(IgnoreApi = true)]
public class GtxMessagingResponse : Dictionary<string, object?> { } public class GtxMessagingResponse : Dictionary<string, object?> { }

View File

@@ -1,8 +1,11 @@
namespace EnvelopeGenerator.Application.Common.Dto.Messaging; using Microsoft.AspNetCore.Mvc;
namespace EnvelopeGenerator.Application.Common.Dto.Messaging;
/// <summary> /// <summary>
/// ///
/// </summary> /// </summary>
[ApiExplorerSettings(IgnoreApi = true)]
public record SmsResponse public record SmsResponse
{ {
/// <summary> /// <summary>

View File

@@ -1,16 +0,0 @@
namespace EnvelopeGenerator.Application.Common.Dto;
public sealed class PdfPageImageDto
{
public int PageNumber { get; init; }
public double WidthPt { get; init; }
public double HeightPt { get; init; }
public int WidthPx { get; init; }
public int HeightPx { get; init; }
public string ImageDataUrl { get; init; } = string.Empty;
}

View File

@@ -1,6 +0,0 @@
namespace EnvelopeGenerator.Application.Common.Dto;
public sealed class PdfRenderRequestDto
{
public byte[]? PdfBytes { get; init; }
}

View File

@@ -1,54 +0,0 @@
using EnvelopeGenerator.Application.Common.Enums;
namespace EnvelopeGenerator.Application.Common.Dto;
/// <summary>
///
/// </summary>
public sealed record PdfSignOptionsDto
{
/// <summary>
///
/// </summary>
public long DocumentElementId { get; set; }
/// <summary>
///
/// </summary>
public string Name { get; set; } = null!;
/// <summary>
///
/// </summary>
public string Reason { get; set; } = null!;
/// <summary>
///
/// </summary>
public string Location { get; set; } = null!;
/// <summary>
///
/// </summary>
public string? Email { get; set; }
/// <summary>
///
/// </summary>
public string? Phone { get; set; }
/// <summary>
///
/// </summary>
public string ApplicationName { get; set; } = null!;
/// <summary>
///
/// </summary>
public PdfSignatureCertificationLevel CertificationLevel { get; set; }
/// <summary>
///
/// </summary>
public PdfSignatureAppearanceDto? Appearance { get; set; }
}

View File

@@ -1,96 +0,0 @@
namespace EnvelopeGenerator.Application.Common.Dto;
/// <summary>
///
/// </summary>
public enum PdfSignatureCoordinateUnit
{
/// <summary>
///
/// </summary>
Point = 0,
/// <summary>
///
/// </summary>
Inch = 1
}
/// <summary>
///
/// </summary>
public enum PdfSignatureCoordinateOrigin
{
/// <summary>
///
/// </summary>
BottomLeft = 0,
/// <summary>
///
/// </summary>
TopLeft = 1
}
/// <summary>
///
/// </summary>
/// <param name="PageNumber"></param>
/// <param name="Left"></param>
/// <param name="Bottom"></param>
/// <param name="Width"></param>
/// <param name="Height"></param>
/// <param name="ShowLabels"></param>
/// <param name="ShowName"></param>
/// <param name="ShowDate"></param>
/// <param name="ShowReason"></param>
/// <param name="ShowLocation"></param>
/// <param name="ShowDistinguishedName"></param>
/// <param name="DateTimeFormat"></param>
/// <param name="CoordinateUnit"></param>
/// <param name="CoordinateOrigin"></param>
public sealed record PdfSignatureAppearanceDto(
int PageNumber,
double Left,
double Bottom,
double Width,
double Height,
bool ShowLabels = false,
bool ShowName = false,
bool ShowDate = false,
bool ShowReason = false,
bool ShowLocation = false,
bool ShowDistinguishedName = false,
string DateTimeFormat = "dd.MM.yyyy HH:mm:ss zzz",
PdfSignatureCoordinateUnit CoordinateUnit = PdfSignatureCoordinateUnit.Point,
PdfSignatureCoordinateOrigin CoordinateOrigin = PdfSignatureCoordinateOrigin.BottomLeft)
{
/// <summary>
///
/// </summary>
/// <param name="FamilyName"></param>
/// <param name="Size"></param>
/// <param name="IsBold"></param>
/// <param name="IsItalic"></param>
/// <param name="IsUnderline"></param>
public sealed record FontDto(string FamilyName = "Helvetica", float Size = 10, bool IsBold = false, bool IsItalic = false, bool IsUnderline = false)
{
/// <summary>
///
/// </summary>
public RgbaColorDto Color { get; init; } = new();
}
/// <summary>
///
/// </summary>
/// <param name="R"></param>
/// <param name="G"></param>
/// <param name="B"></param>
public sealed record RgbaColorDto(double R = 0, double G = 0, double B = 0);
/// <summary>
///
/// </summary>
public FontDto Font { get; init; } = new();
}

View File

@@ -1,57 +0,0 @@
namespace EnvelopeGenerator.Application.Common.Dto;
public sealed record PdfSignaturePrincipalDto(
string? CommonName,
string? Organization,
string? Country,
string? Full);
public sealed record PdfSignatureCertificateInfoDto(
PdfSignaturePrincipalDto IssuedTo,
PdfSignaturePrincipalDto IssuedBy,
string? SerialNumber,
DateTimeOffset? ValidFrom,
DateTimeOffset? ValidTo,
bool SelfSigned,
bool Expired,
bool NotYetValid,
bool IsSigner,
int? Version,
string? KeyAlgorithm,
string? SignatureAlgorithm,
string? Pem,
int? KeySizeBits);
public sealed record PdfSignatureCheckItemDto(
bool CoversWholeDoc,
string? Reason,
string? BusinessReason,
string? Location,
string? DeclaredName,
string? ContactInfo,
string? Email,
string? Phone,
string? DigestAlgorithm,
string? SignatureAlgorithm,
string? SignerName,
string? Issuer,
string? SerialNumber,
DateTimeOffset? ValidFrom,
DateTimeOffset? ValidTo,
string ExpiryCheckedAgainst,
IReadOnlyList<PdfSignatureCertificateInfoDto> Certificates,
bool Unchanged,
string? FieldName,
bool IsCryptographicallyValid,
string? SigningTime,
string? Filter,
string? SubFilter,
string? ApplicationName,
long? DocumentElementId,
string? OriginalDocumentSha256,
string? MessageDigestHex,
string? Error);
public sealed record PdfSignatureCheckResultDto(
bool Signed,
IReadOnlyList<PdfSignatureCheckItemDto> Signatures);

View File

@@ -1,11 +0,0 @@
using System.Dynamic;
namespace EnvelopeGenerator.Application.Common.Dto;
/// <summary>
/// Represents PSPDFKit annotation data.
/// </summary>
/// <param name="Instant">Instant annotation data.</param>
/// <param name="Structured">Structured annotation data.</param>
[Obsolete("The PSPDFKit library is deprecated.")]
public record PsPdfKitAnnotation(ExpandoObject Instant, IEnumerable<AnnotationCreateDto> Structured);

View File

@@ -1,4 +1,5 @@
using EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiver; using EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiver;
using Microsoft.AspNetCore.Mvc;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
namespace EnvelopeGenerator.Application.Common.Dto.Receiver; namespace EnvelopeGenerator.Application.Common.Dto.Receiver;
@@ -6,6 +7,7 @@ namespace EnvelopeGenerator.Application.Common.Dto.Receiver;
/// <summary> /// <summary>
/// ///
/// </summary> /// </summary>
[ApiExplorerSettings(IgnoreApi = true)]
public class ReceiverDto public class ReceiverDto
{ {
/// <summary> /// <summary>

View File

@@ -1,10 +0,0 @@
namespace EnvelopeGenerator.Application.Common.Dto.Receiver;
public class RecentReceiverSuggestionDto
{
public required string FullName { get; init; }
public required string EmailAddress { get; init; }
public required DateTime LastUsedAt { get; init; }
}

View File

@@ -1,65 +0,0 @@
namespace EnvelopeGenerator.Application.Common.Dto;
/// <summary>
/// Represents a captured signature with metadata created by the receiver in the signature popup.
/// This model holds the signature image (as base64 data URL) along with signer information
/// used for rendering applied signatures on the PDF canvas.
/// </summary>
/// <remarks>
/// <b>Used in:</b> EnvelopeViewer.razor signature popup workflow
/// <br/>
/// <b>Creation:</b> User draws/types/uploads signature and fills required fields
/// </remarks>
public sealed record Signature
{
/// <summary>
/// TBDD_DOCUMENT_RECEIVER_ELEMENT.ID - identifies the specific signature field on the PDF page.
/// </summary>
public required int Id { get; init; }
/// <summary>
/// Base64-encoded data URL of the signature image.
/// <br/>
/// <b>Format:</b> <c>data:image/png;base64,iVBORw0KG...</c>
/// <br/>
/// <b>Source:</b> Canvas.toDataURL() from signature pad (draw/text/image tabs)
/// <br/>
/// <b>Usage:</b> Set as <c>img.src</c> in applied signature overlay
/// </summary>
public required string DataUrl { get; init; }
/// <summary>
/// Full name of the signer (first and last name).
/// <br/>
/// <b>Required:</b> Yes (validated in popup)
/// <br/>
/// <b>Example:</b> "Max Mustermann"
/// </summary>
public required string FullName { get; init; }
private readonly string? _position = null;
/// <summary>
/// Job title or position of the signer.
/// <br/>
/// <b>Required:</b> No (optional field)
/// <br/>
/// <b>Example:</b> "Geschäftsführer" or empty string
/// </summary>
public string? Position
{
get => _position;
init => _position = string.IsNullOrWhiteSpace(value) ? null : value;
}
/// <summary>
/// Location/place where the signature was created.
/// <br/>
/// <b>Required:</b> Yes (validated in popup)
/// <br/>
/// <b>Display:</b> Shown with current date in German format (dd.MM.yyyy)
/// <br/>
/// <b>Example:</b> "Berlin" ? rendered as "Berlin, 26.01.2025"
/// </summary>
public required string Place { get; init; }
}

View File

@@ -1,14 +1,13 @@
using EnvelopeGenerator.Application.Common.Dto.Receiver; using EnvelopeGenerator.Domain.Interfaces;
using EnvelopeGenerator.Domain.Constants; using Microsoft.AspNetCore.Mvc;
using EnvelopeGenerator.Domain.Interfaces;
namespace EnvelopeGenerator.Application.Common.Dto; namespace EnvelopeGenerator.Application.Common.Dto;
// TODO: Add the “Reason” property and integrate with UI and the digital signature process.
/// <summary> /// <summary>
/// Data Transfer Object representing a positioned element assigned to a document receiver. /// Data Transfer Object representing a positioned element assigned to a document receiver.
/// </summary> /// </summary>
public class DocReceiverElementDto : IDocReceiverElement, ISignatureInkPayload [ApiExplorerSettings(IgnoreApi = true)]
public class SignatureDto : ISignature
{ {
/// <summary> /// <summary>
/// Gets or sets the unique identifier of the element. /// Gets or sets the unique identifier of the element.
@@ -20,21 +19,11 @@ public class DocReceiverElementDto : IDocReceiverElement, ISignatureInkPayload
/// </summary> /// </summary>
public int DocumentId { get; set; } public int DocumentId { get; set; }
/// <summary>
///
/// </summary>
public virtual DocumentDto? Document { get; set; }
/// <summary> /// <summary>
/// Gets or sets the identifier of the receiver. /// Gets or sets the identifier of the receiver.
/// </summary> /// </summary>
public int ReceiverId { get; set; } public int ReceiverId { get; set; }
/// <summary>
///
/// </summary>
public virtual ReceiverDto? Receiver { get; set; }
/// <summary> /// <summary>
/// Gets or sets the type of the element. /// Gets or sets the type of the element.
/// </summary> /// </summary>
@@ -104,71 +93,4 @@ public class DocReceiverElementDto : IDocReceiverElement, ISignatureInkPayload
/// Gets or sets the left position of the element (in layout terms). /// Gets or sets the left position of the element (in layout terms).
/// </summary> /// </summary>
public double Left => X; public double Left => X;
/// <summary>
///
/// </summary>
public IEnumerable<AnnotationDto>? Annotations { get; set; }
/// <summary>
///
/// </summary>
public SenderAppType SenderAppType { get; set; } = SenderAppType.LegacyFormApp;
/// <summary>
///
/// </summary>
public string? FullName { get; set; }
/// <summary>
///
/// </summary>
public string? Position { get; set; }
/// <summary>
///
/// </summary>
public string? Place { get; set; }
/// <summary>
///
/// </summary>
public byte[]? Ink { get; set; }
/// <summary>
/// Returns the stored signature bytes as an image payload with detected MIME type.
/// </summary>
/// <exception cref="InvalidOperationException">Thrown when signature image bytes do not exist.</exception>
public (byte[] Bytes, string ContentType, string FileExtension) ToImage()
{
if (Ink is not { Length: > 0 })
throw new InvalidOperationException("Signature image (Ink) does not exist.");
if (HasPrefix(Ink, 0x89, 0x50, 0x4E, 0x47))
return (Ink, "image/png", ".png");
if (HasPrefix(Ink, 0xFF, 0xD8, 0xFF))
return (Ink, "image/jpeg", ".jpg");
if (HasPrefix(Ink, 0x52, 0x49, 0x46, 0x46) &&
Ink.Length >= 12 &&
Ink[8] == 0x57 && Ink[9] == 0x45 && Ink[10] == 0x42 && Ink[11] == 0x50)
return (Ink, "image/webp", ".webp");
return (Ink, "application/octet-stream", ".bin");
}
private static bool HasPrefix(byte[] data, params byte[] prefix)
{
if (data.Length < prefix.Length)
return false;
for (var i = 0; i < prefix.Length; i++)
{
if (data[i] != prefix[i])
return false;
}
return true;
}
} }

View File

@@ -1,39 +0,0 @@
namespace EnvelopeGenerator.Application.Common.Dto;
/// <summary>
/// Represents a user's UI preferences stored in the distributed cache (dbo.TBDD_CACHE).
/// </summary>
public sealed record UserPreferencesDto
{
/// <summary>
/// The active DevExpress theme name.
/// Allowed values: "blazing-berry", "fluent", "purple"
/// </summary>
public string ThemeName { get; set; } = "blazing-berry";
/// <summary>
/// Whether dark mode is active.
/// Default: false (light mode).
/// </summary>
public bool IsDarkMode { get; set; } = false;
/// <summary>
/// Persisted DxGrid layout states, keyed by a stable grid identifier.
/// Example keys: "sender.active-envelopes", "sender.completed-envelopes"
/// Values are opaque JSON strings produced by DxGrid.SaveLayoutAsync().
/// </summary>
public Dictionary<string, string> GridLayouts { get; set; } = new();
/// <summary>
/// The authenticated user's display name ("{GivenName} {Surname}").
/// Populated by the server on GET responses from JWT claims.
/// NOT persisted in cache — injected at read time by UserPreferencesController.
/// Ignored on PUT requests.
/// </summary>
public string? DisplayName { get; set; }
/// <summary>
///
/// </summary>
public static UserPreferencesDto Default { get; } = new UserPreferencesDto();
}

View File

@@ -1,27 +0,0 @@
namespace EnvelopeGenerator.Application.Common.Enums;
/// <summary>
///
/// </summary>
public enum PdfSignatureCertificationLevel
{
/// <summary>
/// All changes are allowed
/// </summary>
NoCertification = 0,
/// <summary>
/// No changes are allowed
/// </summary>
NoChangesAllowed,
/// <summary>
/// Allows users to fill in forms and sign the document.
/// </summary>
FillForms,
/// <summary>
/// Allows users to fill in forms, create, modify and delete annotations, and sign the document.
/// </summary>
FillFormsAndAnnotate
}

View File

@@ -1,42 +0,0 @@
using AutoMapper;
using EnvelopeGenerator.Domain.Interfaces.Auditing;
using System;
namespace EnvelopeGenerator.Application.Common.Extensions;
/// <summary>
/// Extension methods for applying auditing timestamps during AutoMapper mappings.
/// </summary>
public static class AutoMapperAuditingExtensions
{
/// <summary>
/// Maps <see cref="IHasAddedWhen.AddedWhen"/> to the current UTC time.
/// </summary>
public static IMappingExpression<TSource, TDestination> MapAddedWhen<TSource, TDestination>(this IMappingExpression<TSource, TDestination> expression)
where TDestination : IHasAddedWhen
=> expression.ForMember(dest => dest.AddedWhen, opt => opt.MapFrom(_ => DateTime.Now));
/// <summary>
/// Maps <see cref="IHasChangedWhen.ChangedWhen"/> to the current UTC time.
/// </summary>
public static IMappingExpression<TSource, TDestination> MapChangedWhen<TSource, TDestination>(this IMappingExpression<TSource, TDestination> expression)
where TDestination : IHasChangedWhen
=> expression.ForMember(dest => dest.ChangedWhen, opt => opt.MapFrom(_ => DateTime.Now));
/// <summary>
/// Converts a base64 data URL string to a byte array.
/// Handles data URLs in the format: "data:image/png;base64,iVBORw0KG..."
/// </summary>
/// <param name="dataUrl">The base64 data URL string from Canvas.toDataURL()</param>
/// <returns>The decoded byte array, or null if the input is null or empty</returns>
public static byte[]? MapDataUrlToRequiredBytes(this string dataUrl)
{
// Remove data URL prefix (e.g., "data:image/png;base64,")
var base64Index = dataUrl.IndexOf(',', StringComparison.Ordinal);
if (base64Index == -1)
throw new ArgumentException("Invalid data URL format. Unable to extract base64 data.", nameof(dataUrl));
var base64Data = dataUrl[(base64Index + 1)..];
return Convert.FromBase64String(base64Data);
}
}

View File

@@ -3,19 +3,8 @@ using System.Text;
namespace EnvelopeGenerator.Application.Common.Extensions namespace EnvelopeGenerator.Application.Common.Extensions
{ {
/// <summary>
///
/// </summary>
public static class LoggerExtensions public static class LoggerExtensions
{ {
/// <summary>
///
/// </summary>
/// <param name="logger"></param>
/// <param name="envelopeReceiverId"></param>
/// <param name="exception"></param>
/// <param name="message"></param>
/// <param name="args"></param>
public static void LogEnvelopeError(this ILogger logger, string envelopeReceiverId, Exception? exception = null, string? message = null, params object?[] args) public static void LogEnvelopeError(this ILogger logger, string envelopeReceiverId, Exception? exception = null, string? message = null, params object?[] args)
{ {
var sb = new StringBuilder().AppendLine(envelopeReceiverId.DecodeEnvelopeReceiverId().ToTitle()); var sb = new StringBuilder().AppendLine(envelopeReceiverId.DecodeEnvelopeReceiverId().ToTitle());
@@ -29,15 +18,6 @@ namespace EnvelopeGenerator.Application.Common.Extensions
logger.Log(LogLevel.Error, exception, sb.AppendLine(exception.Message).ToString(), args); logger.Log(LogLevel.Error, exception, sb.AppendLine(exception.Message).ToString(), args);
} }
/// <summary>
///
/// </summary>
/// <param name="logger"></param>
/// <param name="uuid"></param>
/// <param name="signature"></param>
/// <param name="exception"></param>
/// <param name="message"></param>
/// <param name="args"></param>
public static void LogEnvelopeError(this ILogger logger, string? uuid, string? signature = null, Exception? exception = null, string? message = null, params object?[] args) public static void LogEnvelopeError(this ILogger logger, string? uuid, string? signature = null, Exception? exception = null, string? message = null, params object?[] args)
{ {
var sb = new StringBuilder($"Envelope Uuid: {uuid}"); var sb = new StringBuilder($"Envelope Uuid: {uuid}");
@@ -54,11 +34,6 @@ namespace EnvelopeGenerator.Application.Common.Extensions
logger.Log(LogLevel.Error, exception, sb.ToString(), args); logger.Log(LogLevel.Error, exception, sb.ToString(), args);
} }
/// <summary>
///
/// </summary>
/// <param name="envelopeReceiverTuple"></param>
/// <returns></returns>
public static string ToTitle(this (string? UUID, string? Signature) envelopeReceiverTuple) public static string ToTitle(this (string? UUID, string? Signature) envelopeReceiverTuple)
{ {
return $"UUID is {envelopeReceiverTuple.UUID} and signature is {envelopeReceiverTuple.Signature}"; return $"UUID is {envelopeReceiverTuple.UUID} and signature is {envelopeReceiverTuple.Signature}";

View File

@@ -5,7 +5,6 @@ namespace EnvelopeGenerator.Application.Common.Extensions;
/// <summary> /// <summary>
/// Extension methods for tasks /// Extension methods for tasks
/// </summary> /// </summary>
[Obsolete("Implement Mediator behaviors in the Osolete .NET project.")]
public static class TaskExtensions public static class TaskExtensions
{ {
/// <summary> /// <summary>
@@ -18,7 +17,6 @@ public static class TaskExtensions
/// <param name="factory">Exception provider</param> /// <param name="factory">Exception provider</param>
/// <returns>The awaited result if not <c>null</c>.</returns> /// <returns>The awaited result if not <c>null</c>.</returns>
/// <exception>Thrown if the result is <c>null</c>.</exception> /// <exception>Thrown if the result is <c>null</c>.</exception>
[Obsolete("Implement Mediator behaviors in the Osolete .NET project.")]
public static async Task<T> ThrowIfNull<T, TException>(this Task<T?> task, Func<TException> factory) where TException : Exception public static async Task<T> ThrowIfNull<T, TException>(this Task<T?> task, Func<TException> factory) where TException : Exception
{ {
var result = await task; var result = await task;
@@ -35,7 +33,6 @@ public static class TaskExtensions
/// <param name="factory">Exception provider</param> /// <param name="factory">Exception provider</param>
/// <returns>The awaited collection if it is not <c>null</c> or empty.</returns> /// <returns>The awaited collection if it is not <c>null</c> or empty.</returns>
/// <exception cref="NotFoundException">Thrown if the result is <c>null</c> or empty.</exception> /// <exception cref="NotFoundException">Thrown if the result is <c>null</c> or empty.</exception>
[Obsolete("Implement Mediator behaviors in the Osolete .NET project.")]
public static async Task<IEnumerable<T>> ThrowIfEmpty<T, TException>(this Task<IEnumerable<T>> task, Func<TException> factory) where TException : Exception public static async Task<IEnumerable<T>> ThrowIfEmpty<T, TException>(this Task<IEnumerable<T>> task, Func<TException> factory) where TException : Exception
{ {
var result = await task; var result = await task;
@@ -50,33 +47,11 @@ public static class TaskExtensions
/// <param name="task"></param> /// <param name="task"></param>
/// <param name="act"></param> /// <param name="act"></param>
/// <returns></returns> /// <returns></returns>
[Obsolete("Implement Mediator behaviors in the Osolete .NET project.")]
public static async Task<I> Then<T, I>(this Task<T> task, Func<T, I> act) public static async Task<I> Then<T, I>(this Task<T> task, Func<T, I> act)
{ {
var res = await task; var res = await task;
return act(res); return act(res);
} }
/// <summary>
///
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="task"></param>
/// <returns></returns>
[Obsolete("Implement Mediator behaviors in the Osolete .NET project.")]
public static Task<T?> FirstOrDefaultAsync<T>(this Task<IEnumerable<T>> task) => task.Then(t => t.FirstOrDefault());
/// <summary>
///
/// </summary>
/// <typeparam name="T"></typeparam>
/// <typeparam name="TException"></typeparam>
/// <param name="task"></param>
/// <param name="factory"></param>
/// <returns></returns>
public static Task<T> FirstAsync<T, TException>(this Task<IEnumerable<T>> task, Func<TException> factory)
where TException : Exception
=> task.Then(t => t.FirstOrDefault() ?? throw factory());
} }
/// <summary> /// <summary>
@@ -93,13 +68,11 @@ public static class Exceptions
/// ///
/// </summary> /// </summary>
/// <returns></returns> /// <returns></returns>
[Obsolete("Implement Mediator behaviors in the Osolete .NET project.")]
public static BadRequestException BadRequest() => new(); public static BadRequestException BadRequest() => new();
/// <summary> /// <summary>
/// ///
/// </summary> /// </summary>
/// <returns></returns> /// <returns></returns>
[Obsolete("Implement Mediator behaviors in the Osolete .NET project.")]
public static ForbiddenException Forbidden() => new(); public static ForbiddenException Forbidden() => new();
} }

View File

@@ -2,42 +2,16 @@
using Microsoft.Extensions.Localization; using Microsoft.Extensions.Localization;
using System.Text.Encodings.Web; using System.Text.Encodings.Web;
namespace EnvelopeGenerator.Application.Common.Extensions; namespace EnvelopeGenerator.Application.Common.Extensions
{
/// <summary>
///
/// </summary>
public static class XSSExtensions public static class XSSExtensions
{ {
/// <summary>
///
/// </summary>
/// <param name="value"></param>
/// <param name="encoder"></param>
/// <returns></returns>
public static string? TryEncode(this string? value, UrlEncoder encoder) => value is null ? value : encoder.Encode(value); public static string? TryEncode(this string? value, UrlEncoder encoder) => value is null ? value : encoder.Encode(value);
/// <summary>
///
/// </summary>
/// <param name="value"></param>
/// <param name="encoder"></param>
/// <returns></returns>
public static string? TryEncode(this LocalizedString? value, UrlEncoder encoder) => value is null ? null : encoder.Encode(value); public static string? TryEncode(this LocalizedString? value, UrlEncoder encoder) => value is null ? null : encoder.Encode(value);
/// <summary>
///
/// </summary>
/// <param name="html"></param>
/// <param name="sanitizer"></param>
/// <returns></returns>
public static string? TrySanitize(this string? html, HtmlSanitizer sanitizer) => html is null ? html : sanitizer.Sanitize(html); public static string? TrySanitize(this string? html, HtmlSanitizer sanitizer) => html is null ? html : sanitizer.Sanitize(html);
/// <summary>
///
/// </summary>
/// <param name="html"></param>
/// <param name="sanitizer"></param>
/// <returns></returns>
public static string? TrySanitize(this LocalizedString? html, HtmlSanitizer sanitizer) => html is null ? null : sanitizer.Sanitize(html); public static string? TrySanitize(this LocalizedString? html, HtmlSanitizer sanitizer) => html is null ? null : sanitizer.Sanitize(html);
} }
}

View File

@@ -1,66 +0,0 @@
using DigitalData.Core.Exceptions;
using EnvelopeGenerator.Application.Common.Dto;
using EnvelopeGenerator.Domain.Interfaces;
namespace EnvelopeGenerator.Application.Common.Interfaces.Doc;
/// <summary>
///
/// </summary>
public interface IPdfPageImageRenderer
{
/// <summary>
///
/// </summary>
/// <param name="pdfBytes"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
Task<IReadOnlyList<PdfPageImageDto>> RenderPagesAsImagesAsync(byte[] pdfBytes, CancellationToken cancellationToken = default);
/// <summary>
///
/// </summary>
/// <param name="element"></param>
/// <returns></returns>
Stream ComposeSignaturePreviewPng(ISignatureInkPayload element);
/// <summary>
///
/// </summary>
/// <param name="bytes"></param>
/// <returns></returns>
public bool IsSupportedImageFormat(byte[] bytes);
}
/// <summary>
///
/// </summary>
public static class PdfPageImageRendererExtensions
{
/// <summary>
///
/// </summary>
/// <param name="renderer"></param>
/// <param name="inkBytes"></param>
/// <returns></returns>
/// <exception cref="BadRequestException"></exception>
public static byte[] EnsureSupportedImageBytes(this IPdfPageImageRenderer renderer, byte[] inkBytes)
{
if (!renderer.IsSupportedImageFormat(inkBytes))
throw new BadRequestException("Only PNG and JPEG signature formats are supported.");
return inkBytes;
}
public static byte[] ComposeSignaturePreviewPngBytes(this IPdfPageImageRenderer renderer, ISignatureInkPayload element)
{
using var stream = renderer.ComposeSignaturePreviewPng(element);
if (stream is MemoryStream memoryStream)
return memoryStream.ToArray();
using var copy = new MemoryStream();
stream.CopyTo(copy);
return copy.ToArray();
}
}

View File

@@ -1,12 +0,0 @@
namespace EnvelopeGenerator.Application.Common.Interfaces;
/// <summary>
///
/// </summary>
public interface IRequestContext
{
/// <summary>
///
/// </summary>
string? Origin { get; }
}

View File

@@ -6,6 +6,6 @@ namespace EnvelopeGenerator.Application.Common.Interfaces.Repositories;
/// ///
/// </summary> /// </summary>
[Obsolete("Use IRepository")] [Obsolete("Use IRepository")]
public interface IDocumentReceiverElementRepository : ICRUDRepository<DocReceiverElement, int> public interface IDocumentReceiverElementRepository : ICRUDRepository<Signature, int>
{ {
} }

View File

@@ -8,6 +8,6 @@ namespace EnvelopeGenerator.Application.Common.Interfaces.Services;
/// ///
/// </summary> /// </summary>
[Obsolete("Use MediatR")] [Obsolete("Use MediatR")]
public interface IDocumentReceiverElementService : IBasicCRUDService<DocReceiverElementDto, DocReceiverElement, int> public interface IDocumentReceiverElementService : IBasicCRUDService<SignatureDto, Signature, int>
{ {
} }

View File

@@ -1,29 +0,0 @@
using EnvelopeGenerator.Application.Common.Dto;
namespace EnvelopeGenerator.Application.Common.Interfaces.Services;
/// <summary>
/// Defines PDF digital-signing operations for the application boundary.
/// </summary>
public interface IPdfDigitalSignatureService
{
/// <summary>
/// Creates a digital signature for the provided PDF bytes and returns
/// a new signed PDF document.
/// </summary>
/// <param name="inputPdf">Source PDF content stream.</param>
/// <param name="outputPdf">Destination stream for signed PDF output.</param>
/// <param name="options">Optional signing options that map to PDF signature metadata fields.</param>
/// <param name="image">Optional image stream for signature appearance.</param>
/// <param name="cancellationToken">Cancellation token for cooperative cancellation.</param>
Task SignPdfAsync(Stream inputPdf, Stream outputPdf, PdfSignOptionsDto? options = null, Stream? image = null, CancellationToken cancellationToken = default);
/// <summary>
/// Checks whether the provided PDF contains signature field(s) and whether
/// those signatures are cryptographically valid.
/// </summary>
/// <param name="inputPdf">PDF content stream to inspect.</param>
/// <param name="cancellationToken">Cancellation token for cooperative cancellation.</param>
/// <returns>A structured result containing signature presence and per-signature details.</returns>
Task<PdfSignatureCheckResultDto> CheckPdfSignatureAsync(Stream inputPdf, CancellationToken cancellationToken = default);
}

View File

@@ -1,29 +0,0 @@
using EnvelopeGenerator.Application.Common.Dto;
namespace EnvelopeGenerator.Application.Common.Interfaces.Services;
/// <summary>
/// Manages per-user UI preferences (theme, dark mode, grid layouts).
/// Preferences are stored in the distributed cache (dbo.TBDD_CACHE) as JSON.
/// Cache key schema: "signflow.user-preferences:{username}"
/// </summary>
public interface IUserPreferencesService
{
/// <summary>
/// Retrieves preferences for the given user.
/// Always returns a valid object — never throws, never returns null.
/// Returns defaults if no entry exists in cache.
/// </summary>
/// <param name="id"></param>
/// <param name="ct">Cancellation token.</param>
Task<UserPreferencesDto> GetOrCreateAsync(string id, CancellationToken ct = default);
/// <summary>
/// Persists the given preferences for the user in the distributed cache.
/// Failures are non-fatal and silently ignored.
/// </summary>
/// <param name="id"></param>
/// <param name="preferences">The preferences to persist.</param>
/// <param name="ct">Cancellation token.</param>
Task SaveAsync(string id, UserPreferencesDto preferences, CancellationToken ct = default);
}

View File

@@ -1,98 +0,0 @@
using EnvelopeGenerator.Application.Common.Extensions;
using EnvelopeGenerator.Domain.Constants;
namespace EnvelopeGenerator.Application.Common.Interfaces.Services.Notification;
/// <summary>
///
/// </summary>
public record DocumentReceivedNotification : EnvelopeNotificationBase
{
/// <summary>
///
/// </summary>
public required string DocumentTitle { get; init; }
/// <summary>
///
/// </summary>
public required string NameReceiver { get; init; }
/// <summary>
///
/// </summary>
public required string NameSender { get; init; }
/// <summary>
/// [SIGNATURE_TYPE]
/// </summary>
public required bool IsReadAndConfirm { private get; init; }
/// <summary>
///
/// </summary>
public string SignatureType => IsReadAndConfirm ? "Signieren" : "Lesen und bestätigen";
/// <summary>
///
/// </summary>
public required bool AdvancedSignature { private get; init; }
/// <summary>
///
/// </summary>
public string DocumentProcess => AdvancedSignature ? " und elektronisch unterschreiben" : string.Empty;
/// <summary>
///
/// </summary>
public required Uri Origin { private get; init; }
/// <summary>
///
/// </summary>
public required string EnvelopeUuid { private get; init; }
/// <summary>
///
/// </summary>
public required string ReceiverSignature { private get; init; }
private Uri EnvelopeURL => new(Origin, $"Envelope/{(EnvelopeUuid, ReceiverSignature).ToEnvelopeKey()}");
/// <summary>
///
/// </summary>
public string LinkToDocument => EnvelopeURL.AbsoluteUri;
/// <summary>
///
/// </summary>
public string LinkToDocumentText
{
get
{
var urlText = EnvelopeURL.ToString();
return $"{urlText[..Math.Min(urlText.Length, 40)]}..";
}
}
/// <summary>
///
/// </summary>
public required string Message { get; init; }
/// <summary>
///
/// </summary>
public string NamePortal { get; set; } = "signFLOW Portal";
/// <summary>
///
/// </summary>
/// <returns></returns>
public override EmailTemplateType GetTemplateType()
{
return EmailTemplateType.DocumentReceived;
}
}

View File

@@ -1,59 +0,0 @@
using EnvelopeGenerator.Domain.Constants;
namespace EnvelopeGenerator.Application.Common.Interfaces.Services.Notification;
/// <summary>
///
/// </summary>
public record DocumentSignedNotification : EnvelopeNotificationBase
{
/// <summary>
///
/// </summary>
public required string DocumentTitle { get; init; }
/// <summary>
///
/// </summary>
public required string NameReceiver { get; init; }
/// <summary>
///
/// </summary>
public string NamePortal { get; init; } = "signFLOW Portal";
/// <summary>
///
/// </summary>
public required bool IsReadAndConfirm { private get; init; }
/// <summary>
///
/// </summary>
public string ReceiverAction => IsReadAndConfirm ? "bestätigt" : "unterschrieben";
/// <summary>
///
/// </summary>
public string FinalStatus => IsReadAndConfirm ? "Lesebestätigung" : "Signatur";
/// <summary>
///
/// </summary>
public string FinalAction => IsReadAndConfirm ? "Empfänger bestätigt" : "Vertragspartner unterzeichnet";
/// <summary>
///
/// </summary>
public override EmailTemplateType GetTemplateType() => EmailTemplateType.DocumentSigned;
}
/*
Umschlag abgeschlossen: '[DOCUMENT_TITLE]'
---
Guten Tag [NAME_RECEIVER],<br /> <br /> Der Vorgang <B><I>'[DOCUMENT_TITLE]'</I></B> wurde erfolgreich abgeschlossen.<br /> <br /> Sie erhalten das Dokument mit einem detaillierten Ergebnisbericht als Anhang zu dieser Email.<br /> <br /> Mit freundlichen Grüßen<br /> <br /> [NAME_PORTAL]
*/

View File

@@ -1,19 +0,0 @@
using EnvelopeGenerator.Domain.Constants;
namespace EnvelopeGenerator.Application.Common.Interfaces.Services.Notification;
/// <summary>
///
/// </summary>
public abstract record EnvelopeNotificationBase
{
/// <summary>
///
/// </summary>
public abstract EmailTemplateType GetTemplateType();
/// <summary>
///
/// </summary>
public IReadOnlyList<(string FileName, byte[] Content)>? Attachments { get; init; }
}

View File

@@ -1,18 +0,0 @@
namespace EnvelopeGenerator.Application.Common.Interfaces.Services.Notification;
/// <summary>
///
/// </summary>
public interface IEnvelopeNotificationService
{
/// <summary>
///
/// </summary>
/// <typeparam name="TEnvelopeNotificationBase"></typeparam>
/// <param name="receiverEmail"></param>
/// <param name="notification"></param>
/// <param name="cancel"></param>
/// <returns></returns>
public Task Notify<TEnvelopeNotificationBase>(string receiverEmail, TEnvelopeNotificationBase notification, CancellationToken cancel = default)
where TEnvelopeNotificationBase : EnvelopeNotificationBase;
}

View File

@@ -1,37 +1,88 @@
using EnvelopeGenerator.Application.Common.Dto; using EnvelopeGenerator.Application.Common.Dto;
using EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiver; using EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiver;
using EnvelopeGenerator.Application.Common.Extensions; using EnvelopeGenerator.Application.Common.Extensions;
using EnvelopeGenerator.Application.Common.Notifications.RemoveSignature;
using EnvelopeGenerator.Domain.Constants; using EnvelopeGenerator.Domain.Constants;
using MediatR; using MediatR;
using System.Dynamic;
namespace EnvelopeGenerator.Application.Common.Notifications.DocSigned; namespace EnvelopeGenerator.Application.Common.Notifications.DocSigned;
/// <summary> /// <summary>
/// Notification raised when a document is signed by a receiver. ///
/// </summary> /// </summary>
[Obsolete("This notification is deprecated. Use Signature.Commands.SignCommand instead.")] /// <param name="Instant"></param>
public record DocSignedNotification : INotification, ISendMailNotification /// <param name="Structured"></param>
public record PsPdfKitAnnotation(ExpandoObject Instant, IEnumerable<AnnotationCreateDto> Structured);
/// <summary>
///
/// </summary>
/// <param name="Original"></param>
public record DocSignedNotification(EnvelopeReceiverDto Original) : EnvelopeReceiverDto(Original), INotification, ISendMailNotification
{ {
/// <summary> /// <summary>
/// The envelope receiver information. ///
/// </summary> /// </summary>
public required EnvelopeReceiverDto EnvelopeReceiver { get; init; } public PsPdfKitAnnotation PsPdfKitAnnotation { get; init; } = null!;
/// <summary> /// <summary>
/// The PSPDFKit annotation data. ///
/// </summary>
[Obsolete("The PSPDFKit library is deprecated.")]
public PsPdfKitAnnotation? PsPdfKitAnnotation { get; init; }
/// <summary>
/// Gets the email template type.
/// </summary> /// </summary>
public EmailTemplateType TemplateType => EmailTemplateType.DocumentSigned; public EmailTemplateType TemplateType => EmailTemplateType.DocumentSigned;
/// <summary> /// <summary>
/// Gets the email address of the receiver. ///
/// </summary> /// </summary>
public string EmailAddress => EnvelopeReceiver.Receiver?.EmailAddress public string EmailAddress => Receiver?.EmailAddress
?? throw new InvalidOperationException($"Receiver is null." + ?? throw new InvalidOperationException($"Receiver is null." +
$"DocSignedNotification:\n{this.ToJson(Format.Json.ForDiagnostics)}"); $"DocSignedNotification:\n{this.ToJson(Format.Json.ForDiagnostics)}");
} }
/// <summary>
///
/// </summary>
public static class DocSignedNotificationExtensions
{
/// <summary>
/// Converts an <see cref="EnvelopeReceiverDto"/> to a <see cref="DocSignedNotification"/>.
/// </summary>
/// <param name="dto">The DTO to convert.</param>
/// <param name="psPdfKitAnnotation"></param>
/// <returns>A new <see cref="DocSignedNotification"/> instance.</returns>
public static DocSignedNotification ToDocSignedNotification(this EnvelopeReceiverDto dto, PsPdfKitAnnotation psPdfKitAnnotation)
=> new(dto) { PsPdfKitAnnotation = psPdfKitAnnotation };
/// <summary>
///
/// </summary>
/// <param name="dtoTask"></param>
/// <param name="psPdfKitAnnotation"></param>
/// <returns></returns>
public static async Task<DocSignedNotification?> ToDocSignedNotification(this Task<EnvelopeReceiverDto?> dtoTask, PsPdfKitAnnotation psPdfKitAnnotation)
=> await dtoTask is EnvelopeReceiverDto dto ? new(dto) { PsPdfKitAnnotation = psPdfKitAnnotation } : null;
/// <summary>
///
/// </summary>
/// <param name="publisher"></param>
/// <param name="notification"></param>
/// <param name="cancel"></param>
/// <returns></returns>
public static async Task PublishSafely(this IPublisher publisher, DocSignedNotification notification, CancellationToken cancel = default)
{
try
{
await publisher.Publish(notification, cancel);
}
catch (Exception)
{
await publisher.Publish(new RemoveSignatureNotification()
{
EnvelopeId = notification.EnvelopeId,
ReceiverId = notification.ReceiverId
}, cancel);
throw;
}
}
}

View File

@@ -1,6 +1,5 @@
using DigitalData.Core.Abstraction.Application.Repository; using DigitalData.Core.Abstraction.Application.Repository;
using EnvelopeGenerator.Domain.Entities; using EnvelopeGenerator.Domain.Entities;
using EnvelopeGenerator.Application.Common.Dto;
using MediatR; using MediatR;
namespace EnvelopeGenerator.Application.Common.Notifications.DocSigned.Handlers; namespace EnvelopeGenerator.Application.Common.Notifications.DocSigned.Handlers;
@@ -8,7 +7,6 @@ namespace EnvelopeGenerator.Application.Common.Notifications.DocSigned.Handlers;
/// <summary> /// <summary>
/// ///
/// </summary> /// </summary>
[Obsolete("The PSPDFKit library is deprecated.")]
public class AnnotationHandler : INotificationHandler<DocSignedNotification> public class AnnotationHandler : INotificationHandler<DocSignedNotification>
{ {
/// <summary> /// <summary>
@@ -31,9 +29,6 @@ public class AnnotationHandler : INotificationHandler<DocSignedNotification>
/// <param name="notification"></param> /// <param name="notification"></param>
/// <param name="cancel"></param> /// <param name="cancel"></param>
/// <returns></returns> /// <returns></returns>
public async Task Handle(DocSignedNotification notification, CancellationToken cancel) public Task Handle(DocSignedNotification notification, CancellationToken cancel)
{ => _repo.CreateAsync(notification.PsPdfKitAnnotation.Structured, cancel);
if (notification.PsPdfKitAnnotation is PsPdfKitAnnotation annot)
await _repo.CreateAsync(annot.Structured, cancel);
}
} }

View File

@@ -1,5 +1,4 @@
using EnvelopeGenerator.Application.DocStatus.Commands; using EnvelopeGenerator.Application.DocStatus.Commands;
using EnvelopeGenerator.Application.Common.Dto;
using EnvelopeGenerator.Domain.Constants; using EnvelopeGenerator.Domain.Constants;
using MediatR; using MediatR;
using System.Text.Json; using System.Text.Json;
@@ -9,11 +8,8 @@ namespace EnvelopeGenerator.Application.Common.Notifications.DocSigned.Handlers;
/// <summary> /// <summary>
/// ///
/// </summary> /// </summary>
[Obsolete("This notification is deprecated. Use Signature.Commands.SignCommand instead.")]
public class DocStatusHandler : INotificationHandler<DocSignedNotification> public class DocStatusHandler : INotificationHandler<DocSignedNotification>
{ {
private const string BlankAnnotationJson = "{}";
private readonly ISender _sender; private readonly ISender _sender;
/// <summary> /// <summary>
@@ -31,13 +27,13 @@ public class DocStatusHandler : INotificationHandler<DocSignedNotification>
/// <param name="notification"></param> /// <param name="notification"></param>
/// <param name="cancel"></param> /// <param name="cancel"></param>
/// <returns></returns> /// <returns></returns>
[Obsolete("This notification is deprecated. Use Signature.Commands.SignCommand instead.")] public async Task Handle(DocSignedNotification notification, CancellationToken cancel)
public Task Handle(DocSignedNotification notification, CancellationToken cancel) => _sender.Send(new CreateDocStatusCommand()
{ {
EnvelopeId = notification.EnvelopeReceiver.EnvelopeId, await _sender.Send(new SaveDocStatusCommand()
ReceiverId = notification.EnvelopeReceiver.ReceiverId, {
Value = notification.PsPdfKitAnnotation is PsPdfKitAnnotation annot Envelope = new() { Id = notification.EnvelopeId },
? JsonSerializer.Serialize(annot.Instant, Format.Json.ForAnnotations) Receiver = new() { Id = notification.ReceiverId},
: BlankAnnotationJson Value = JsonSerializer.Serialize(notification.PsPdfKitAnnotation.Instant, Format.Json.ForAnnotations)
}, cancel); }, cancel);
} }
}

View File

@@ -29,13 +29,13 @@ public class HistoryHandler : INotificationHandler<DocSignedNotification>
/// <returns></returns> /// <returns></returns>
public async Task Handle(DocSignedNotification notification, CancellationToken cancel) public async Task Handle(DocSignedNotification notification, CancellationToken cancel)
{ {
if (notification.EnvelopeReceiver.Receiver is null) if (notification.Receiver is null)
throw new InvalidOperationException($"Receiver information is missing in the notification. DocSignedNotification:\n {notification.ToJson(Format.Json.ForDiagnostics)}"); throw new InvalidOperationException($"Receiver information is missing in the notification. DocSignedNotification:\n {notification.ToJson(Format.Json.ForDiagnostics)}");
await _sender.Send(new CreateHistoryCommand() await _sender.Send(new CreateHistoryCommand()
{ {
EnvelopeId = notification.EnvelopeReceiver.EnvelopeId, EnvelopeId = notification.EnvelopeId,
UserReference = notification.EnvelopeReceiver.Receiver.EmailAddress, UserReference = notification.Receiver.EmailAddress,
Status = EnvelopeStatus.DocumentSigned, Status = EnvelopeStatus.DocumentSigned,
}, cancel); }, cancel);
} }

View File

@@ -3,7 +3,6 @@ using DigitalData.EmailProfilerDispatcher.Abstraction.Entities;
using EnvelopeGenerator.Application.Common.Configurations; using EnvelopeGenerator.Application.Common.Configurations;
using EnvelopeGenerator.Domain.Entities; using EnvelopeGenerator.Domain.Entities;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using EnvelopeGenerator.Domain.Interfaces;
namespace EnvelopeGenerator.Application.Common.Notifications.DocSigned.Handlers; namespace EnvelopeGenerator.Application.Common.Notifications.DocSigned.Handlers;
@@ -31,7 +30,7 @@ public class SendSignedMailHandler : SendMailHandler<DocSignedNotification>
protected override void ConfigureEmailOut(DocSignedNotification notification, EmailOut emailOut) protected override void ConfigureEmailOut(DocSignedNotification notification, EmailOut emailOut)
{ {
emailOut.ReferenceString = notification.EmailAddress; emailOut.ReferenceString = notification.EmailAddress;
emailOut.ReferenceId = notification.EnvelopeReceiver.ReceiverId; emailOut.ReferenceId = notification.ReceiverId;
} }
/// <summary> /// <summary>
@@ -42,29 +41,10 @@ public class SendSignedMailHandler : SendMailHandler<DocSignedNotification>
{ {
var placeHolders = new Dictionary<string, string>() var placeHolders = new Dictionary<string, string>()
{ {
{ "[NAME_RECEIVER]", notification.EnvelopeReceiver.Name ?? string.Empty }, { "[NAME_RECEIVER]", notification.Name ?? string.Empty },
{ "[DOCUMENT_TITLE]", notification.EnvelopeReceiver.Envelope?.Title ?? string.Empty }, { "[DOCUMENT_TITLE]", notification.Envelope?.Title ?? string.Empty },
}; };
if (notification.EnvelopeReceiver.Envelope.IsReadAndConfirm())
{
placeHolders["[SIGNATURE_TYPE]"] = "Lesen und bestätigen";
placeHolders["[DOCUMENT_PROCESS]"] = string.Empty;
placeHolders["[FINAL_STATUS]"] = "Lesebestätigung";
placeHolders["[FINAL_ACTION]"] = "Empfänger bestätigt";
placeHolders["[REJECTED_BY_OTHERS]"] = "anderen Empfänger abgelehnt!";
placeHolders["[RECEIVER_ACTION]"] = "bestätigt";
}
else
{
placeHolders["[SIGNATURE_TYPE]"] = "Signieren";
placeHolders["[DOCUMENT_PROCESS]"] = " und elektronisch unterschreiben";
placeHolders["[FINAL_STATUS]"] = "Signatur";
placeHolders["[FINAL_ACTION]"] = "Vertragspartner unterzeichnet";
placeHolders["[REJECTED_BY_OTHERS]"] = "anderen Vertragspartner abgelehnt! Ihre notwendige Unterzeichnung wurde verworfen.";
placeHolders["[RECEIVER_ACTION]"] = "unterschrieben";
}
return placeHolders; return placeHolders;
} }
} }

View File

@@ -1,21 +0,0 @@
using EnvelopeGenerator.Domain.Constants;
using EnvelopeGenerator.Domain.Entities;
using MediatR;
namespace EnvelopeGenerator.Application.Common.Notifications.EnvelopeDeleted;
public record EnvelopeDeletedNotification : INotification, ISendMailNotification
{
public required Envelope Envelope { get; init; }
public required EnvelopeReceiver EnvelopeReceiver { get; init; }
public required string Reason { get; init; }
public required string SenderReference { get; init; }
public EmailTemplateType TemplateType => EmailTemplateType.DocumentDeleted;
public string EmailAddress => EnvelopeReceiver.Receiver?.EmailAddress
?? throw new InvalidOperationException("Receiver email is missing for EnvelopeDeletedNotification.");
}

View File

@@ -1,42 +0,0 @@
using DigitalData.Core.Abstraction.Application.Repository;
using DigitalData.EmailProfilerDispatcher.Abstraction.Entities;
using EnvelopeGenerator.Application.Common.Configurations;
using EnvelopeGenerator.Domain.Constants;
using EnvelopeGenerator.Domain.Entities;
using Microsoft.Extensions.Options;
namespace EnvelopeGenerator.Application.Common.Notifications.EnvelopeDeleted.Handlers;
public class SendDeletedMailHandler : SendMailHandler<EnvelopeDeletedNotification>
{
public SendDeletedMailHandler(
IRepository<EmailTemplate> tempRepo,
IRepository<EmailOut> emailOutRepo,
IOptions<MailParams> mailParamsOptions,
IOptions<DispatcherParams> dispatcherParamsOptions)
: base(tempRepo, emailOutRepo, mailParamsOptions, dispatcherParamsOptions)
{
}
protected override void ConfigureEmailOut(EnvelopeDeletedNotification notification, EmailOut emailOut)
{
emailOut.ReferenceString = notification.EmailAddress;
emailOut.ReferenceId = notification.EnvelopeReceiver.ReceiverId;
emailOut.WfId = (int)EnvelopeStatus.MessageDeletionSent;
}
protected override Dictionary<string, string> CreatePlaceHolders(EnvelopeDeletedNotification notification)
{
var senderName = notification.Envelope.User?.Email;
if (string.IsNullOrWhiteSpace(senderName))
senderName = notification.SenderReference;
return new Dictionary<string, string>
{
["[NAME_RECEIVER]"] = notification.EnvelopeReceiver.Name ?? string.Empty,
["[NAME_SENDER]"] = senderName ?? string.Empty,
["[DOCUMENT_TITLE]"] = notification.Envelope.Title ?? string.Empty,
["[REASON]"] = notification.Reason
};
}
}

View File

@@ -2,7 +2,6 @@
using DigitalData.EmailProfilerDispatcher.Abstraction.Entities; using DigitalData.EmailProfilerDispatcher.Abstraction.Entities;
using EnvelopeGenerator.Application.Common.Configurations; using EnvelopeGenerator.Application.Common.Configurations;
using EnvelopeGenerator.Application.Common.Extensions; using EnvelopeGenerator.Application.Common.Extensions;
using EnvelopeGenerator.Application.Common.Notifications.EnvelopeDeleted;
using EnvelopeGenerator.Domain.Constants; using EnvelopeGenerator.Domain.Constants;
using EnvelopeGenerator.Domain.Entities; using EnvelopeGenerator.Domain.Entities;
using MediatR; using MediatR;
@@ -97,10 +96,6 @@ public abstract class SendMailHandler<TNotification> : INotificationHandler<TNot
/// <exception cref="NotImplementedException"></exception> /// <exception cref="NotImplementedException"></exception>
public virtual async Task Handle(TNotification notification, CancellationToken cancel) public virtual async Task Handle(TNotification notification, CancellationToken cancel)
{ {
// do not send email if the envelope is deleted and the status is less than or equal to EnvelopeSaved
if (notification is EnvelopeDeletedNotification delNot && delNot.Envelope.Status <= EnvelopeStatus.EnvelopeSaved)
return;
var placeHolders = CreatePlaceHolders(notification); var placeHolders = CreatePlaceHolders(notification);
var temp = await TempRepo var temp = await TempRepo
@@ -118,7 +113,7 @@ public abstract class SendMailHandler<TNotification> : INotificationHandler<TNot
EmailAddress = notification.EmailAddress, EmailAddress = notification.EmailAddress,
EmailBody = temp.Body, EmailBody = temp.Body,
EmailSubj = temp.Subject, EmailSubj = temp.Subject,
AddedWhen = DateTime.Now, AddedWhen = DateTime.UtcNow,
AddedWho = DispatcherParams.AddedWho, AddedWho = DispatcherParams.AddedWho,
SendingProfile = DispatcherParams.SendingProfile, SendingProfile = DispatcherParams.SendingProfile,
ReminderTypeId = DispatcherParams.ReminderTypeId, ReminderTypeId = DispatcherParams.ReminderTypeId,

View File

@@ -1,6 +1,4 @@
using System.ComponentModel.DataAnnotations.Schema; namespace EnvelopeGenerator.Application.Common.Query;
namespace EnvelopeGenerator.Application.Common.Query;
/// <summary> /// <summary>
/// Stellt eine Abfrage dar, um die Details eines Empfängers zu lesen. /// Stellt eine Abfrage dar, um die Details eines Empfängers zu lesen.
@@ -31,6 +29,5 @@ public record ReceiverQueryBase
/// <see cref="Id"/>, <see cref="EmailAddress"/>, or <see cref="Signature"/> is not null. /// <see cref="Id"/>, <see cref="EmailAddress"/>, or <see cref="Signature"/> is not null.
/// <para>Usage example: The query can be executed only if at least one criterion is specified.</para> /// <para>Usage example: The query can be executed only if at least one criterion is specified.</para>
/// </remarks> /// </remarks>
[NotMapped] public bool HasAnyCriteria => Id is not null || EmailAddress is not null || Signature is not null;
public virtual bool HasAnyCriteria => Id is not null || EmailAddress is not null || Signature is not null;
} }

View File

@@ -13,7 +13,7 @@ public class EnvelopeReceiverAddReadSQL : ISQL<Envelope>
/// ENV_UID, EMAIL_ADRESS, SALUTATION, PHONE, /// ENV_UID, EMAIL_ADRESS, SALUTATION, PHONE,
/// </summary> /// </summary>
public string Raw => @" public string Raw => @"
DECLARE @OUT_RECEIVER_ID bigint DECLARE @OUT_RECEIVER_ID int
EXEC [dbo].[PRSIG_API_CREATE_RECEIVER] EXEC [dbo].[PRSIG_API_CREATE_RECEIVER]
{0}, {0},

View File

@@ -7,11 +7,6 @@ using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.DependencyInjection.Extensions;
using QRCoder; using QRCoder;
using System.Reflection; using System.Reflection;
using MediatR;
using EnvelopeGenerator.Application.DocReceiverElements.Commands;
using EnvelopeGenerator.Application.DocReceiverElements.Behaviors;
using EnvelopeGenerator.Application.Common.Behaviors;
using FluentValidation;
namespace EnvelopeGenerator.Application; namespace EnvelopeGenerator.Application;
@@ -49,46 +44,20 @@ public static class DependencyInjection
services.Configure<DispatcherParams>(config.GetSection(nameof(DispatcherParams))); services.Configure<DispatcherParams>(config.GetSection(nameof(DispatcherParams)));
services.Configure<MailParams>(config.GetSection(nameof(MailParams))); services.Configure<MailParams>(config.GetSection(nameof(MailParams)));
services.Configure<SignatureFieldParams>(config.GetSection(nameof(SignatureFieldParams)));
services.Configure<AuthenticatorParams>(config.GetSection(nameof(AuthenticatorParams))); services.Configure<AuthenticatorParams>(config.GetSection(nameof(AuthenticatorParams)));
services.Configure<TotpSmsParams>(config.GetSection(nameof(TotpSmsParams))); services.Configure<TotpSmsParams>(config.GetSection(nameof(TotpSmsParams)));
services.Configure<ESignParams>(config.GetSection(nameof(ESignParams)));
services.AddHttpClientService<GtxMessagingParams>(config.GetSection(nameof(GtxMessagingParams))); services.AddHttpClientService<GtxMessagingParams>(config.GetSection(nameof(GtxMessagingParams)));
services.TryAddScoped<ISmsSender, GTXSmsSender>(); // Changed: Singleton → Scoped services.TryAddSingleton<ISmsSender, GTXSmsSender>();
services.TryAddScoped<IEnvelopeSmsHandler, EnvelopeSmsHandler>(); // Changed: Singleton → Scoped services.TryAddSingleton<IEnvelopeSmsHandler, EnvelopeSmsHandler>();
services.TryAddSingleton<IAuthenticator, Authenticator>(); services.TryAddSingleton<IAuthenticator, Authenticator>();
services.TryAddSingleton<QRCodeGenerator>(); services.TryAddSingleton<QRCodeGenerator>();
services.AddMediatR(cfg => services.AddMediatR(cfg =>
{ {
cfg.RegisterServicesFromAssembly(Assembly.GetExecutingAssembly()); cfg.RegisterServicesFromAssembly(Assembly.GetExecutingAssembly());
// Generic request validation
cfg.AddOpenBehavior(typeof(ValidationBehavior<,>));
// Register SignCommand pipeline behaviors in execution order
// 0. EnvelopeReceiverResolutionBehavior - Resolves EnvelopeReceiver from query parameters (executes FIRST)
cfg.AddBehavior<IPipelineBehavior<SigningCommand, Unit>, EnvelopeReceiverResolutionBehavior>();
// 1. AnnotationBehavior - Saves annotations (executes second)
cfg.AddBehavior<IPipelineBehavior<SigningCommand, Unit>, SaveSignatureBehavior>();
// 2. AnnotationBehavior - Saves annotations for legacy web flow
cfg.AddBehavior<IPipelineBehavior<SigningCommand, Unit>, AnnotationBehavior>();
// 3. DocStatusBehavior - Creates document status
cfg.AddBehavior<IPipelineBehavior<SigningCommand, Unit>, DocStatusBehavior>();
// 4. HistoryBehavior - Records history
cfg.AddBehavior<IPipelineBehavior<SigningCommand, Unit>, HistoryBehavior>();
// 5. SendSignedMailBehavior - Sends notification email (executes LAST)
cfg.AddBehavior<IPipelineBehavior<SigningCommand, Unit>, SendSignedMailBehavior>();
}); });
services.AddValidatorsFromAssembly(Assembly.GetExecutingAssembly());
return services; return services;
} }
} }

View File

@@ -1,50 +0,0 @@
using DigitalData.Core.Abstraction.Application.Repository;
using DigitalData.Core.Exceptions;
using EnvelopeGenerator.Application.Common.Dto;
using EnvelopeGenerator.Application.DocReceiverElements.Commands;
using EnvelopeGenerator.Domain.Entities;
using MediatR;
namespace EnvelopeGenerator.Application.DocReceiverElements.Behaviors;
/// <summary>
/// Pipeline behavior that saves annotations.
/// Executes first in the signing process.
/// </summary>
[Obsolete("The PSPDFKit library is deprecated.")]
public class AnnotationBehavior : IPipelineBehavior<SigningCommand, Unit>
{
private readonly IRepository<ElementAnnotation> _repo;
/// <summary>
/// Initializes a new instance of the <see cref="AnnotationBehavior"/> class.
/// </summary>
/// <param name="repository"></param>
public AnnotationBehavior(IRepository<ElementAnnotation> repository)
{
_repo = repository;
}
/// <summary>
///
/// </summary>
/// <param name="request"></param>
/// <param name="next"></param>
/// <param name="cancel"></param>
/// <returns></returns>
public async Task<Unit> Handle(SigningCommand request, RequestHandlerDelegate<Unit> next, CancellationToken cancel)
{
if(request.ReceiverAppType != ReceiverAppType.LegacyWeb)
if(request.PsPdfKitAnnotation is null)
return await next(cancel);
else
throw new BadRequestException("PsPdfKit Annotation are only supported for the legacy web receiver type.");
if (request.PsPdfKitAnnotation is PsPdfKitAnnotation annot)
await _repo.CreateAsync(annot.Structured, cancel);
else
throw new BadRequestException("Annotation data is missing or invalid.");
return await next(cancel);
}
}

View File

@@ -1,50 +0,0 @@
using EnvelopeGenerator.Application.Common.Dto;
using EnvelopeGenerator.Application.DocStatus.Commands;
using EnvelopeGenerator.Application.DocReceiverElements.Commands;
using EnvelopeGenerator.Domain.Constants;
using MediatR;
using System.Text.Json;
namespace EnvelopeGenerator.Application.DocReceiverElements.Behaviors;
/// <summary>
/// Pipeline behavior that creates document status.
/// Executes second in the signing process.
/// </summary>
public class DocStatusBehavior : IPipelineBehavior<SigningCommand, Unit>
{
private const string BlankAnnotationJson = "{}";
private readonly ISender _sender;
/// <summary>
///
/// </summary>
/// <param name="sender"></param>
public DocStatusBehavior(ISender sender)
{
_sender = sender;
}
/// <summary>
///
/// </summary>
/// <param name="request"></param>
/// <param name="next"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
[Obsolete("This notification is deprecated. Use Signature.Commands.SignCommand instead.")]
public async Task<Unit> Handle(SigningCommand request, RequestHandlerDelegate<Unit> next, CancellationToken cancellationToken)
{
await _sender.Send(new CreateDocStatusCommand()
{
EnvelopeId = request.EnvelopeReceiver.EnvelopeId,
ReceiverId = request.EnvelopeReceiver.ReceiverId,
Value = request.PsPdfKitAnnotation is PsPdfKitAnnotation annot
? JsonSerializer.Serialize(annot.Instant, Format.Json.ForAnnotations)
: BlankAnnotationJson
}, cancellationToken);
return await next(cancellationToken);
}
}

View File

@@ -1,58 +0,0 @@
using AutoMapper;
using DigitalData.Core.Abstraction.Application.Repository;
using DigitalData.Core.Exceptions;
using EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiver;
using EnvelopeGenerator.Application.Common.Extensions;
using EnvelopeGenerator.Application.DocReceiverElements.Commands;
using EnvelopeGenerator.Domain.Entities;
using MediatR;
using Microsoft.EntityFrameworkCore;
namespace EnvelopeGenerator.Application.DocReceiverElements.Behaviors;
/// <summary>
/// Pipeline behavior that resolves and validates EnvelopeReceiver.
/// Executes FIRST in the signing process - before all other behaviors.
/// If EnvelopeReceiver is not provided, it queries the database using EnvelopeReceiverQueryBase parameters.
/// </summary>
public class EnvelopeReceiverResolutionBehavior : IPipelineBehavior<SigningCommand, Unit>
{
private readonly IRepository<EnvelopeReceiver> _erRepo;
private readonly IMapper _mapper;
/// <summary>
///
/// </summary>
/// <param name="erRepo"></param>
/// <param name="mapper"></param>
public EnvelopeReceiverResolutionBehavior(IRepository<EnvelopeReceiver> erRepo, IMapper mapper)
{
_erRepo = erRepo;
_mapper = mapper;
}
/// <summary>
///
/// </summary>
/// <param name="request"></param>
/// <param name="next"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
public async Task<Unit> Handle(SigningCommand request, RequestHandlerDelegate<Unit> next, CancellationToken cancellationToken)
{
// If EnvelopeReceiver is not provided, query it from database
if (request.EnvelopeReceiver is null)
{
var er = await _erRepo.Query
.Where(request, notnull: true)
.Include(x => x.Envelope)
.Include(x => x.Receiver)
.SingleOrDefaultAsync(cancellationToken)
?? throw new NotFoundException("EnvelopeReceiver not found");
request.SetEnvelopeReceiver(_mapper.Map<EnvelopeReceiverDto>(er));
}
return await next(cancellationToken);
}
}

View File

@@ -1,47 +0,0 @@
using EnvelopeGenerator.Application.Common.Extensions;
using EnvelopeGenerator.Application.Histories.Commands;
using EnvelopeGenerator.Application.DocReceiverElements.Commands;
using EnvelopeGenerator.Domain.Constants;
using MediatR;
namespace EnvelopeGenerator.Application.DocReceiverElements.Behaviors;
/// <summary>
/// Pipeline behavior that records history.
/// Executes third in the signing process.
/// </summary>
public class HistoryBehavior : IPipelineBehavior<SigningCommand, Unit>
{
private readonly ISender _sender;
/// <summary>
///
/// </summary>
/// <param name="sender"></param>
public HistoryBehavior(ISender sender)
{
_sender = sender;
}
/// <summary>
///
/// </summary>
/// <param name="request"></param>
/// <param name="next"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
public async Task<Unit> Handle(SigningCommand request, RequestHandlerDelegate<Unit> next, CancellationToken cancellationToken)
{
if (request.EnvelopeReceiver.Receiver is null)
throw new InvalidOperationException($"Receiver information is missing in the notification. SignCommand:\n {request.ToJson(Format.Json.ForDiagnostics)}");
await _sender.Send(new CreateHistoryCommand()
{
EnvelopeId = request.EnvelopeReceiver.EnvelopeId,
UserReference = request.EnvelopeReceiver.Receiver.EmailAddress,
Status = EnvelopeStatus.DocumentSigned,
}, cancellationToken);
return await next(cancellationToken);
}
}

View File

@@ -1,70 +0,0 @@
using AutoMapper;
using DigitalData.Core.Abstraction.Application.Repository;
using DigitalData.Core.Exceptions;
using EnvelopeGenerator.Application.Common.Dto;
using EnvelopeGenerator.Application.DocReceiverElements.Commands;
using EnvelopeGenerator.Domain.Entities;
using MediatR;
using Microsoft.EntityFrameworkCore;
namespace EnvelopeGenerator.Application.DocReceiverElements.Behaviors;
/// <summary>
/// Pipeline behavior that creates document status.
/// Executes second in the signing process.
/// </summary>
public class SaveSignatureBehavior : IPipelineBehavior<SigningCommand, Unit>
{
private readonly ISender _sender;
private readonly IRepository<DocReceiverElement> _elementRepo;
private readonly IMapper _mapper;
/// <summary>
///
/// </summary>
/// <param name="sender"></param>
/// <param name="elementRepo"></param>
/// <param name="mapper"></param>
public SaveSignatureBehavior(ISender sender, IRepository<DocReceiverElement> elementRepo, IMapper mapper)
{
_sender = sender;
_elementRepo = elementRepo;
_elementRepo = elementRepo;
_mapper = mapper;
}
/// <summary>
///
/// </summary>
/// <param name="request"></param>
/// <param name="next"></param>
/// <param name="cancel"></param>
/// <returns></returns>
public async Task<Unit> Handle(SigningCommand request, RequestHandlerDelegate<Unit> next, CancellationToken cancel)
{
if (request.ReceiverAppType == ReceiverAppType.LegacyWeb)
return await next(cancel);
else if(request.Signatures is not IEnumerable<Signature> signatures)
throw new BadRequestException($"Signatures are required for saving signature behavior.");
var elements = await _elementRepo
.Where(e => e.Document.EnvelopeId == request.EnvelopeReceiver.EnvelopeId)
.Where(e => e.ReceiverId == request.EnvelopeReceiver.ReceiverId)
.ToListAsync(cancel);
foreach (var element in elements)
{
var signatures = request.Signatures.Where(s => s.Id == element.Id).ToList();
if(signatures.Count == 0)
throw new BadRequestException("No signature found for element with id {element.Id}.");
else if(signatures.Count > 1)
throw new BadRequestException("Multiple signatures found for element with id {element.Id}.");
await _elementRepo.UpdateAsync(signatures.First(), e => e.Id == element.Id, cancel);
}
return await next(cancel);
}
}

View File

@@ -1,122 +0,0 @@
using DigitalData.Core.Abstraction.Application.Repository;
using DigitalData.EmailProfilerDispatcher.Abstraction.Entities;
using EnvelopeGenerator.Application.Common.Configurations;
using EnvelopeGenerator.Application.Common.Extensions;
using EnvelopeGenerator.Application.DocReceiverElements.Commands;
using EnvelopeGenerator.Domain.Constants;
using EnvelopeGenerator.Domain.Entities;
using EnvelopeGenerator.Domain.Interfaces;
using MediatR;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Options;
using Newtonsoft.Json;
namespace EnvelopeGenerator.Application.DocReceiverElements.Behaviors;
/// <summary>
/// Pipeline behavior that sends signed mail notification.
/// Executes LAST in the signing process - only if all previous behaviors succeed.
/// </summary>
public class SendSignedMailBehavior : IPipelineBehavior<SigningCommand, Unit>
{
private readonly IRepository<EmailTemplate> _tempRepo;
private readonly IRepository<EmailOut> _emailOutRepo;
private readonly MailParams _mailParams;
private readonly DispatcherParams _dispatcherParams;
/// <summary>
///
/// </summary>
/// <param name="tempRepo"></param>
/// <param name="emailOutRepo"></param>
/// <param name="mailParamsOptions"></param>
/// <param name="dispatcherParamsOptions"></param>
public SendSignedMailBehavior(
IRepository<EmailTemplate> tempRepo,
IRepository<EmailOut> emailOutRepo,
IOptions<MailParams> mailParamsOptions,
IOptions<DispatcherParams> dispatcherParamsOptions)
{
_tempRepo = tempRepo;
_emailOutRepo = emailOutRepo;
_mailParams = mailParamsOptions.Value;
_dispatcherParams = dispatcherParamsOptions.Value;
}
/// <summary>
///
/// </summary>
/// <param name="request"></param>
/// <param name="next"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
public async Task<Unit> Handle(SigningCommand request, RequestHandlerDelegate<Unit> next, CancellationToken cancellationToken)
{
var placeHolders = CreatePlaceHolders(request);
var temp = await _tempRepo
.Where(x => x.Name == EmailTemplateType.DocumentSigned.ToString())
.SingleOrDefaultAsync(cancellationToken)
?? throw new InvalidOperationException($"Email template not found. SignCommand:\n {request.ToJson(Format.Json.ForDiagnostics)}");
temp.Subject = ReplacePlaceHolders(temp.Subject, placeHolders, _mailParams.Placeholders);
temp.Body = ReplacePlaceHolders(temp.Body, placeHolders, _mailParams.Placeholders);
var emailOut = new EmailOut
{
EmailAddress = request.EnvelopeReceiver.Receiver!.EmailAddress,
EmailBody = temp.Body,
EmailSubj = temp.Subject,
AddedWhen = DateTime.Now,
AddedWho = _dispatcherParams.AddedWho,
SendingProfile = _dispatcherParams.SendingProfile,
ReminderTypeId = _dispatcherParams.ReminderTypeId,
EmailAttmt1 = _dispatcherParams.EmailAttmt1,
WfId = (int)EnvelopeStatus.MessageConfirmationSent,
ReferenceString = request.EnvelopeReceiver.Receiver!.EmailAddress,
ReferenceId = request.EnvelopeReceiver.ReceiverId
};
await _emailOutRepo.CreateAsync(emailOut, cancellationToken);
return await next(cancellationToken);
}
private Dictionary<string, string> CreatePlaceHolders(SigningCommand request)
{
var placeHolders = new Dictionary<string, string>()
{
{ "[NAME_RECEIVER]", request.EnvelopeReceiver.Name ?? string.Empty },
{ "[DOCUMENT_TITLE]", request.EnvelopeReceiver.Envelope?.Title ?? string.Empty },
};
if (request.EnvelopeReceiver.Envelope.IsReadAndConfirm())
{
placeHolders["[SIGNATURE_TYPE]"] = "Lesen und bestätigen";
placeHolders["[DOCUMENT_PROCESS]"] = string.Empty;
placeHolders["[FINAL_STATUS]"] = "Lesebestätigung";
placeHolders["[FINAL_ACTION]"] = "Empfänger bestätigt";
placeHolders["[REJECTED_BY_OTHERS]"] = "anderen Empfänger abgelehnt!";
placeHolders["[RECEIVER_ACTION]"] = "bestätigt";
}
else
{
placeHolders["[SIGNATURE_TYPE]"] = "Signieren";
placeHolders["[DOCUMENT_PROCESS]"] = " und elektronisch unterschreiben";
placeHolders["[FINAL_STATUS]"] = "Signatur";
placeHolders["[FINAL_ACTION]"] = "Vertragspartner unterzeichnet";
placeHolders["[REJECTED_BY_OTHERS]"] = "anderen Vertragspartner abgelehnt! Ihre notwendige Unterzeichnung wurde verworfen.";
placeHolders["[RECEIVER_ACTION]"] = "unterschrieben";
}
return placeHolders;
}
private static string ReplacePlaceHolders(string text, params Dictionary<string, string>[] placeHoldersList)
{
foreach (var placeHolders in placeHoldersList)
foreach (var ph in placeHolders)
text = text.Replace(ph.Key, ph.Value);
return text;
}
}

View File

@@ -1,78 +0,0 @@
using MediatR;
using EnvelopeGenerator.Application.Common.Dto;
using EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiver;
using EnvelopeGenerator.Application.Common.Query;
namespace EnvelopeGenerator.Application.DocReceiverElements.Commands;
/// <summary>
/// Command to sign a document by a receiver.
/// </summary>
public record SigningCommand : EnvelopeReceiverQueryBase, IRequest
{
private EnvelopeReceiverDto? _envelopeReceiver;
internal void SetEnvelopeReceiver(EnvelopeReceiverDto envelopeReceiver)
{
_envelopeReceiver = envelopeReceiver;
}
/// <summary>
/// The envelope receiver information.
/// </summary>
public EnvelopeReceiverDto EnvelopeReceiver
{
get => _envelopeReceiver!;
init => _envelopeReceiver = value;
}
/// <summary>
/// The PSPDFKit annotation data.
/// </summary>
[Obsolete("This notification is deprecated. Use Signature.Commands.SignCommand instead.")]
public PsPdfKitAnnotation? PsPdfKitAnnotation { get; init; }
/// <summary>
///
/// </summary>
public IEnumerable<Signature>? Signatures { get; init; }
/// <summary>
///
/// </summary>
public ReceiverAppType ReceiverAppType { get; init; } = ReceiverAppType.ReceiverUI;
}
/// <summary>
/// Handles the sign command. All work is done by pipeline behaviors.
/// This handler is intentionally empty - behaviors handle all the processing.
/// </summary>
public class SignCommandHandler : IRequestHandler<SigningCommand>
{
/// <summary>
/// Executes the signing command. Pipeline behaviors handle all processing.
/// </summary>
/// <param name="request"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
public Task Handle(SigningCommand request, CancellationToken cancellationToken = default)
{
return Task.CompletedTask;
}
}
/// <summary>
///
/// </summary>
public enum ReceiverAppType
{
/// <summary>
///
/// </summary>
ReceiverUI = 0,
/// <summary>
///
/// </summary>
LegacyWeb = 1,
}

View File

@@ -1,81 +0,0 @@
using AutoMapper;
using EnvelopeGenerator.Application.Common.Dto;
using EnvelopeGenerator.Application.Common.Query;
using EnvelopeGenerator.Domain.Entities;
using MediatR;
using EnvelopeGenerator.Application.Common.Extensions;
using DigitalData.Core.Abstraction.Application.Repository;
using Microsoft.EntityFrameworkCore;
namespace EnvelopeGenerator.Application.DocReceiverElements.Queries;
/// <summary>
///
/// </summary>
public record ReadDocReceiverElementQuery : EnvelopeReceiverQueryBase, IRequest<IEnumerable<DocReceiverElementDto>>
{
/// <summary>
///
/// </summary>
public int? Id { get; set; } = null;
/// <summary>
///
/// </summary>
public bool IncludeDocument { get; set; } = false;
}
/// <summary>
///
/// </summary>
public class ReadDocReceiverElementQueryHandler : IRequestHandler<ReadDocReceiverElementQuery, IEnumerable<DocReceiverElementDto>>
{
private readonly IRepository<DocReceiverElement> _repository;
private readonly IMapper _mapper;
/// <summary>
///
/// </summary>
/// <param name="repository"></param>
/// <param name="mapper"></param>
public ReadDocReceiverElementQueryHandler(IRepository<DocReceiverElement> repository, IMapper mapper)
{
_repository = repository;
_mapper = mapper;
}
/// <summary>
///
/// </summary>
/// <param name="request"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
/// <exception cref="NotImplementedException"></exception>
public async Task<IEnumerable<DocReceiverElementDto>> Handle(ReadDocReceiverElementQuery request, CancellationToken cancellationToken)
{
var q = _repository.Query;
if (request.Id is not null)
q = q.Where(e => e.Id == request.Id);
if (request.Envelope.Id is int envelopeId)
q = q.Where(e => e.Document.EnvelopeId == envelopeId);
if (request.Envelope.Uuid is string envelopeUuid)
q = q.Where(e => e.Document.Envelope.Uuid == envelopeUuid);
if (request.Receiver.Id is int receiverId)
q = q.Where(e => e.ReceiverId == receiverId);
if (request.Receiver.Signature is string signature)
q = q.Where(e => e.Receiver.Signature == signature);
if( request.IncludeDocument)
q = q.Include(e => e.Document);
var elements = await q.ToListAsync(cancellationToken);
return _mapper.Map<IEnumerable<DocReceiverElementDto>>(elements);
}
}

View File

@@ -1,41 +1,12 @@
using DigitalData.Core.Abstraction.Application.Repository; namespace EnvelopeGenerator.Application.DocStatus.Commands;
using EnvelopeGenerator.Application.Common.Commands;
using EnvelopeGenerator.Domain.Entities;
using MediatR;
namespace EnvelopeGenerator.Application.DocStatus.Commands;
/// <summary> /// <summary>
/// ///
/// </summary> /// </summary>
public record CreateDocStatusCommand : IRequest<DocumentStatus> public record CreateDocStatusCommand : ModifyDocStatusCommandBase
{ {
/// <summary> /// <summary>
/// /// Gets timestamp when this record was added. Returns the StatusChangedWhen value.
/// </summary> /// </summary>
public int EnvelopeId { get; set; } public DateTime AddedWhen => StatusChangedWhen;
/// <summary>
///
/// </summary>
public int ReceiverId { get; set; }
/// <summary>
/// Gets or sets the display value associated with the status.
/// </summary>
public string? Value { get; set; }
}
/// <summary>
///
/// </summary>
public class CreateDocStatusCommandHandler : CreateCommandHandler<CreateDocStatusCommand, DocumentStatus>
{
/// <summary>
///
/// </summary>
/// <param name="repository"></param>
public CreateDocStatusCommandHandler(IRepository<DocumentStatus> repository) : base(repository)
{
}
} }

View File

@@ -0,0 +1,54 @@
using EnvelopeGenerator.Application.Common.Query;
using EnvelopeGenerator.Domain.Constants;
namespace EnvelopeGenerator.Application.DocStatus.Commands;
/// <summary>
///
/// </summary>
public record ModifyDocStatusCommandBase : EnvelopeReceiverQueryBase
{
/// <summary>
///
/// </summary>
public int? EnvelopeId => Envelope.Id;
/// <summary>
///
/// </summary>
public int? ReceiverId => Receiver.Id;
/// <summary>
///
/// </summary>
public override ReceiverQueryBase Receiver { get => base.Receiver; set => base.Receiver = value; }
/// <summary>
/// Gets the current status code.
/// </summary>
public DocumentStatus Status => Value is null ? DocumentStatus.Created : DocumentStatus.Signed;
/// <summary>
/// Gets or sets the display value associated with the status.
/// </summary>
public string? Value { get; set; }
/// <summary>
/// Gets timestamp when this record was added.
/// </summary>
public DateTime StatusChangedWhen { get; } = DateTime.Now;
/// <summary>
/// Maps the current command to a new instance of the specified type.
/// </summary>
/// <typeparam name="TDest"></typeparam>
/// <returns></returns>
public TDest To<TDest>() where TDest : ModifyDocStatusCommandBase, new()
=> new()
{
Key = Key,
Envelope = Envelope,
Receiver = Receiver,
Value = Value
};
}

View File

@@ -0,0 +1,77 @@
using DigitalData.Core.Abstraction.Application.Repository;
using EnvelopeGenerator.Domain.Entities;
using MediatR;
using Microsoft.EntityFrameworkCore;
using AutoMapper;
using EnvelopeGenerator.Application.Common.Dto;
using EnvelopeGenerator.Application.Common.Extensions;
namespace EnvelopeGenerator.Application.DocStatus.Commands;
/// <summary>
/// Represents a command to save the status of a document, either by creating a new status or updating an existing one based on the provided envelope and receiver identifiers.
/// It returns the identifier of the saved document status.
/// </summary>
public record SaveDocStatusCommand : ModifyDocStatusCommandBase, IRequest<DocumentStatusDto?>;
/// <summary>
///
/// </summary>
public class SaveDocStatusCommandHandler : IRequestHandler<SaveDocStatusCommand, DocumentStatusDto?>
{
private readonly IMapper _mapper;
private readonly IRepository<DocumentStatus> _repo;
private readonly IRepository<Envelope> _envRepo;
private readonly IRepository<Receiver> _rcvRepo;
/// <summary>
///
/// </summary>
/// <param name="mapper"></param>
/// <param name="repo"></param>
/// <param name="rcvRepo"></param>
/// <param name="envRepo"></param>
public SaveDocStatusCommandHandler(IMapper mapper, IRepository<DocumentStatus> repo, IRepository<Receiver> rcvRepo, IRepository<Envelope> envRepo)
{
_mapper = mapper;
_repo = repo;
_rcvRepo = rcvRepo;
_envRepo = envRepo;
}
/// <summary>
///
/// </summary>
/// <param name="request"></param>
/// <param name="cancel"></param>
/// <returns></returns>
public async Task<DocumentStatusDto?> Handle(SaveDocStatusCommand request, CancellationToken cancel)
{
// ceck if exists
bool isExists = await _repo.ReadOnly().Where(request).AnyAsync(cancel);
var env = await _envRepo.ReadOnly().Where(request.Envelope).FirstAsync(cancel);
var rcv = await _rcvRepo.ReadOnly().Where(request.Receiver).FirstAsync(cancel);
request.Envelope.Id = env.Id;
request.Receiver.Id = rcv.Id;
if (isExists)
{
var uReq = request.To<UpdateDocStatusCommand>();
await _repo.UpdateAsync(uReq, q => q.Where(request), cancel);
}
else
{
var cReq = request.To<CreateDocStatusCommand>();
await _repo.CreateAsync(cReq, cancel);
}
var docStatus = await _repo.ReadOnly().Where(request).SingleOrDefaultAsync(cancel);
return _mapper.Map<DocumentStatusDto>(docStatus);
}
}

View File

@@ -1,41 +1,14 @@
using EnvelopeGenerator.Application.Common.Commands; using EnvelopeGenerator.Domain;
using EnvelopeGenerator.Domain.Entities;
using System.Linq.Expressions;
namespace EnvelopeGenerator.Application.DocStatus.Commands; namespace EnvelopeGenerator.Application.DocStatus.Commands;
/// <summary> /// <summary>
/// ///
/// </summary> /// </summary>
/// <param name="Value"></param> public record UpdateDocStatusCommand : ModifyDocStatusCommandBase
public record DocStatusUpdateDto(string? Value);
/// <summary>
///
/// </summary>
public record UpdateDocStatusCommand : UpdateCommand<DocStatusUpdateDto, DocumentStatus>
{ {
/// <summary> /// <summary>
/// /// Gets timestamp when this record was added. Returns the StatusChangedWhen value.
/// </summary> /// </summary>
public int EnvelopeId { get; set; } public DateTime? ChangedWhen => StatusChangedWhen;
/// <summary>
///
/// </summary>
public int ReceiverId { get; set; }
/// <summary>
/// Gets or sets the display value associated with the status.
/// </summary>
public string? Value { get; set; }
/// <summary>
///
/// </summary>
/// <returns></returns>
public override Expression<Func<DocumentStatus, bool>> BuildQueryExpression()
{
return ds => ds.EnvelopeId == EnvelopeId && ds.ReceiverId == ReceiverId;
}
} }

View File

@@ -1,5 +1,4 @@
using AutoMapper; using AutoMapper;
using EnvelopeGenerator.Application.Common.Extensions;
using EnvelopeGenerator.Application.DocStatus.Commands; using EnvelopeGenerator.Application.DocStatus.Commands;
using EnvelopeGenerator.Domain.Entities; using EnvelopeGenerator.Domain.Entities;
@@ -17,17 +16,10 @@ public class MappingProfile : Profile
{ {
CreateMap<CreateDocStatusCommand, DocumentStatus>() CreateMap<CreateDocStatusCommand, DocumentStatus>()
.ForMember(dest => dest.Envelope, opt => opt.Ignore()) .ForMember(dest => dest.Envelope, opt => opt.Ignore())
.ForMember(dest => dest.Receiver, opt => opt.Ignore()) .ForMember(dest => dest.Receiver, opt => opt.Ignore());
.ForMember(dest => dest.Status, opt => opt.MapFrom(
src => src.Value == null
? Domain.Constants.DocumentStatus.Created
: Domain.Constants.DocumentStatus.Signed))
.MapAddedWhen();
CreateMap<UpdateDocStatusCommand, DocumentStatus>() CreateMap<UpdateDocStatusCommand, DocumentStatus>()
.ForMember(dest => dest.Envelope, opt => opt.Ignore()) .ForMember(dest => dest.Envelope, opt => opt.Ignore())
.ForMember(dest => dest.Receiver, opt => opt.Ignore()) .ForMember(dest => dest.Receiver, opt => opt.Ignore());
.ForMember(dest => dest.StatusChangedWhen, opt => opt.MapFrom(src => DateTime.Now))
.MapChangedWhen();
} }
} }

View File

@@ -1,102 +0,0 @@
using DigitalData.Core.Abstraction.Application.Repository;
using EnvelopeGenerator.Application.Common.Dto;
using EnvelopeGenerator.Domain.Entities;
using MediatR;
using Microsoft.EntityFrameworkCore;
namespace EnvelopeGenerator.Application.Documents.Queries;
/// <summary>
/// Represents a query to read document metadata/details without binary payload.
/// </summary>
/// <param name="EnvelopeId">The identifier of the envelope associated with the document.</param>
/// <param name="UserId">The unique identifier of the sending user. Optional.</param>
public record ReadDocumentDetailsQuery(int EnvelopeId, int? UserId = null) : IRequest<DocumentDetailsDto?>;
/// <summary>
/// Handles queries for reading document details without loading BYTE_DATA.
/// </summary>
public class ReadDocumentDetailsQueryHandler : IRequestHandler<ReadDocumentDetailsQuery, DocumentDetailsDto?>
{
private readonly IRepository<Document> _repo;
public ReadDocumentDetailsQueryHandler(IRepository<Document> repo)
{
_repo = repo;
}
public async Task<DocumentDetailsDto?> Handle(ReadDocumentDetailsQuery query, CancellationToken cancel)
{
var docQuery = _repo.Query.AsNoTracking();
if (query.UserId is int userId)
docQuery = docQuery.Where(doc => doc.Envelope.UserId == userId);
// shortcut: manual select avoids AutoMapper ProjectTo pulling BYTE_DATA and the full navigation
// graph (Envelope → History → User etc.) which caused an extremely slow query (~87s).
// Annotations navigation is IEnumerable<T> on the entity (not ICollection<T>), so EF Core
// cannot translate it in a Select projection — we skip it entirely here; sender editor only
// needs element coordinates, not annotation payloads.
var raw = await docQuery
.Where(d => d.EnvelopeId == query.EnvelopeId)
.Select(d => new
{
d.Id,
d.EnvelopeId,
d.AddedWhen,
Elements = d.Elements == null ? null : d.Elements.Select(e => new
{
e.Id,
e.DocumentId,
e.ReceiverId,
e.ElementType,
e.X,
e.Y,
e.Width,
e.Height,
e.Page,
e.Required,
e.Tooltip,
e.ReadOnly,
e.AnnotationIndex,
e.AddedWhen,
e.ChangedWhen,
e.FullName,
e.Position,
e.Place,
}).ToList()
})
.FirstOrDefaultAsync(cancel);
if (raw is null)
return null;
return new DocumentDetailsDto
{
Id = raw.Id,
EnvelopeId = raw.EnvelopeId,
AddedWhen = raw.AddedWhen,
Elements = raw.Elements?.Select(e => new DocReceiverElementDto
{
Id = e.Id,
DocumentId = e.DocumentId,
ReceiverId = e.ReceiverId,
ElementType = e.ElementType,
X = e.X,
Y = e.Y,
Width = e.Width,
Height = e.Height,
Page = e.Page,
Required = e.Required,
Tooltip = e.Tooltip,
ReadOnly = e.ReadOnly,
AnnotationIndex = e.AnnotationIndex,
AddedWhen = e.AddedWhen,
ChangedWhen = e.ChangedWhen,
FullName = e.FullName,
Position = e.Position,
Place = e.Place,
})
};
}
}

View File

@@ -1,13 +0,0 @@
using FluentValidation;
namespace EnvelopeGenerator.Application.Documents.Queries;
public class ReadDocumentDetailsQueryValidator : AbstractValidator<ReadDocumentDetailsQuery>
{
public ReadDocumentDetailsQueryValidator()
{
RuleFor(x => x.EnvelopeId)
.GreaterThan(0)
.WithMessage("EnvelopeId is required.");
}
}

View File

@@ -4,7 +4,6 @@ using EnvelopeGenerator.Domain.Entities;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using AutoMapper; using AutoMapper;
using EnvelopeGenerator.Application.Common.Dto; using EnvelopeGenerator.Application.Common.Dto;
using DigitalData.Core.Exceptions;
namespace EnvelopeGenerator.Application.Documents.Queries; namespace EnvelopeGenerator.Application.Documents.Queries;
@@ -13,15 +12,14 @@ namespace EnvelopeGenerator.Application.Documents.Queries;
/// </summary> /// </summary>
/// <param name="Id">The unique identifier of the document. Optional.</param> /// <param name="Id">The unique identifier of the document. Optional.</param>
/// <param name="EnvelopeId">The identifier of the envelope associated with the document. Optional.</param> /// <param name="EnvelopeId">The identifier of the envelope associated with the document. Optional.</param>
/// <param name="UserId">The unique identifier of the sending user. Optional.</param> public record ReadDocumentQuery(int? Id = null, int? EnvelopeId = null) : IRequest<DocumentDto?>
public record ReadDocumentQuery(int? Id = null, int? EnvelopeId = null, int? UserId = null) : IRequest<DocumentDto>
{ {
} }
/// <summary> /// <summary>
/// Handles queries for reading <see cref="Document"/> data based on either the document ID or the envelope ID. /// Handles queries for reading <see cref="Document"/> data based on either the document ID or the envelope ID.
/// </summary> /// </summary>
public class ReadDocumentQueryHandler : IRequestHandler<ReadDocumentQuery, DocumentDto> public class ReadDocumentQueryHandler : IRequestHandler<ReadDocumentQuery, DocumentDto?>
{ {
/// <summary> /// <summary>
/// TempRepo for accessing <see cref="Document"/> entities. /// TempRepo for accessing <see cref="Document"/> entities.
@@ -52,25 +50,20 @@ public class ReadDocumentQueryHandler : IRequestHandler<ReadDocumentQuery, Docum
/// <exception cref="InvalidOperationException"> /// <exception cref="InvalidOperationException">
/// Thrown when neither <see cref="ReadDocumentQuery.Id"/> nor <see cref="ReadDocumentQuery.EnvelopeId"/> is provided. /// Thrown when neither <see cref="ReadDocumentQuery.Id"/> nor <see cref="ReadDocumentQuery.EnvelopeId"/> is provided.
/// </exception> /// </exception>
public async Task<DocumentDto> Handle(ReadDocumentQuery query, CancellationToken cancel) public async Task<DocumentDto?> Handle(ReadDocumentQuery query, CancellationToken cancel)
{ {
var docQuery = _repo.Query.Include(doc => doc.Elements).ThenInclude(e => e.Annotations).AsNoTracking();
if (query.UserId is int userId)
docQuery = docQuery.Where(doc => doc.Envelope.UserId == userId);
if (query.Id is not null) if (query.Id is not null)
{ {
var doc = await docQuery.Where(d => d.Id == query.Id).FirstOrDefaultAsync(cancel); var doc = await _repo.ReadOnly().Where(d => d.Id == query.Id).FirstOrDefaultAsync(cancel);
return _mapper.Map<DocumentDto>(doc); return _mapper.Map<DocumentDto>(doc);
} }
else if (query.EnvelopeId is not null) else if (query.EnvelopeId is not null)
{ {
var doc = await docQuery.Where(d => d.EnvelopeId == query.EnvelopeId).FirstOrDefaultAsync(cancel); var doc = await _repo.ReadOnly().Where(d => d.EnvelopeId == query.EnvelopeId).FirstOrDefaultAsync(cancel);
return _mapper.Map<DocumentDto>(doc); return _mapper.Map<DocumentDto>(doc);
} }
throw new NotFoundException(); throw new InvalidOperationException(
$"Invalid {nameof(ReadDocumentQuery)}: either {nameof(query.Id)} or {nameof(query.EnvelopeId)} must be provided.");
} }
} }

View File

@@ -1,23 +0,0 @@
using FluentValidation;
namespace EnvelopeGenerator.Application.Documents.Queries;
public class ReadDocumentQueryValidator : AbstractValidator<ReadDocumentQuery>
{
public ReadDocumentQueryValidator()
{
RuleFor(x => x)
.Must(x => (x.Id ?? 0) > 0 || (x.EnvelopeId ?? 0) > 0)
.WithMessage("Id or EnvelopeId is required.");
RuleFor(x => x.Id)
.GreaterThan(0)
.When(x => x.Id.HasValue)
.WithMessage("Id must be greater than 0.");
RuleFor(x => x.EnvelopeId)
.GreaterThan(0)
.When(x => x.EnvelopeId.HasValue)
.WithMessage("EnvelopeId must be greater than 0.");
}
}

View File

@@ -0,0 +1,41 @@
using EnvelopeGenerator.Domain;
using EnvelopeGenerator.Domain.Constants;
using MediatR;
namespace EnvelopeGenerator.Application.EmailTemplates.Commands.Reset;
/// <summary>
/// Ein Befehl zum Zurücksetzen einer E-Mail-Vorlage auf die Standardwerte.
/// Erbt von <see cref="EmailTemplateQuery"/> und ermöglicht die Angabe einer optionalen ID und eines Typs der E-Mail-Vorlage.<br/><br/>
/// Beispiele:<br/>
/// 0 - DocumentReceived: Benachrichtigung über den Empfang eines Dokuments.<br/>
/// 1 - DocumentSigned: Benachrichtigung über die Unterzeichnung eines Dokuments.<br/>
/// 2 - DocumentDeleted: Benachrichtigung über das Löschen eines Dokuments.<br/>
/// 3 - DocumentCompleted: Benachrichtigung über den Abschluss eines Dokuments.<br/>
/// 4 - DocumentAccessCodeReceived: Benachrichtigung über den Erhalt eines Zugangscodes.<br/>
/// 5 - DocumentShared: Benachrichtigung über das Teilen eines Dokuments.<br/>
/// 6 - TotpSecret: Benachrichtigung über ein TOTP-Geheimnis.<br/>
/// 7 - DocumentRejected_ADM (Für den Absender): Mail an den Absender, wenn das Dokument abgelehnt wird.<br/>
/// 8 - DocumentRejected_REC (Für den ablehnenden Empfänger): Mail an den ablehnenden Empfänger, wenn das Dokument abgelehnt wird.<br/>
/// 9 - DocumentRejected_REC_2 (Für sonstige Empfänger): Mail an andere Empfänger (Brief), wenn das Dokument abgelehnt wird.<br/>
/// </summary>
public record ResetEmailTemplateCommand : EmailTemplateQuery, IRequest
{
/// <summary>
///
/// </summary>
/// <param name="orginal"></param>
public ResetEmailTemplateCommand(EmailTemplateQuery? orginal = null) : base(orginal ?? new())
{
}
/// <summary>
///
/// </summary>
/// <param name="Id">Die optionale ID der E-Mail-Vorlage, die zurückgesetzt werden soll.</param>
/// <param name="Type">Der Typ der E-Mail-Vorlage, z. B. <see cref="EmailTemplateType"/> (optional).</param>
public ResetEmailTemplateCommand(int? Id = null, EmailTemplateType? Type = null) : base(Id, Type)
{
}
};

View File

@@ -1,38 +1,11 @@
using DigitalData.Core.Abstraction.Application.Repository; using DigitalData.Core.Abstraction.Application.Repository;
using EnvelopeGenerator.Domain.Constants;
using MediatR;
using EnvelopeGenerator.Application.Common.Dto; using EnvelopeGenerator.Application.Common.Dto;
using EnvelopeGenerator.Domain.Entities; using EnvelopeGenerator.Domain.Entities;
using MediatR;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using System.Linq;
namespace EnvelopeGenerator.Application.EmailTemplates.Commands; namespace EnvelopeGenerator.Application.EmailTemplates.Commands.Reset;
/// <summary>
/// Ein Befehl zum Zurücksetzen einer E-Mail-Vorlage auf die Standardwerte.
/// Erbt von <see cref="IEmailTemplateQuery"/> und ermöglicht die Angabe einer optionalen ID und eines Typs der E-Mail-Vorlage.<br/><br/>
/// </summary>
public record ResetEmailTemplateCommand : IEmailTemplateQuery, IRequest
{
/// <summary>
/// Die eindeutige Kennung der E-Mail-Vorlage (optional).
/// </summary>
public int? Id { get; set; }
/// <summary>
/// Der Typ der E-Mail-Vorlage, z. B. <see cref="EmailTemplateType"/> (optional). Beispiele:<br/>
/// 0 - DocumentReceived: Benachrichtigung über den Empfang eines Dokuments.<br/>
/// 1 - DocumentSigned: Benachrichtigung über die Unterzeichnung eines Dokuments.<br/>
/// 2 - DocumentDeleted: Benachrichtigung über das Löschen eines Dokuments.<br/>
/// 3 - DocumentCompleted: Benachrichtigung über den Abschluss eines Dokuments.<br/>
/// 4 - DocumentAccessCodeReceived: Benachrichtigung über den Erhalt eines Zugangscodes.<br/>
/// 5 - DocumentShared: Benachrichtigung über das Teilen eines Dokuments.<br/>
/// 6 - TotpSecret: Benachrichtigung über ein TOTP-Geheimnis.<br/>
/// 7 - DocumentRejected_ADM (für den Absender): Mail an den Absender, wenn das Dokument abgelehnt wird.<br/>
/// 8 - DocumentRejected_REC (für den ablehnenden Empfänger): Mail an den ablehnenden Empfänger, wenn das Dokument abgelehnt wird.<br/>
/// 9 - DocumentRejected_REC_2 (für sonstige Empfänger): Mail an andere Empfänger (Brief), wenn das Dokument abgelehnt wird.
/// </summary>
public EmailTemplateType? Type { get; set; }
}
/// <summary> /// <summary>
/// ///

View File

@@ -0,0 +1,29 @@
using MediatR;
using System.Text.Json.Serialization;
namespace EnvelopeGenerator.Application.EmailTemplates.Commands.Update;
/// <summary>
/// Befehl zum Aktualisieren einer E-Mail-Vorlage.
/// </summary>
/// <param name="Body">
/// (Optional)Der neue Inhalt des E-Mail-Textkörpers. Wenn null, bleibt der vorhandene Inhalt unverändert.
/// </param>
/// <param name="Subject">
/// (Optional) Der neue Betreff der E-Mail. Wenn null, bleibt der vorhandene Betreff unverändert.
/// </param>
public record UpdateEmailTemplateCommand(string? Body = null, string? Subject = null) : IRequest
{
/// <param>
/// Die Abfrage, die die E-Mail-Vorlage darstellt, die aktualisiert werden soll.
/// </param>
[JsonIgnore]
public EmailTemplateQuery? EmailTemplateQuery { get; set; }
/// <summary>
///
/// </summary>
[JsonIgnore]
public DateTime ChangedWhen { get; init; } = DateTime.Now;
}

View File

@@ -0,0 +1,72 @@
using AutoMapper;
using DigitalData.Core.Abstraction.Application.Repository;
using DigitalData.Core.Exceptions;
using EnvelopeGenerator.Domain.Entities;
using MediatR;
using Microsoft.EntityFrameworkCore;
using EnvelopeGenerator.Domain.Constants;
using EnvelopeGenerator.Application.Common.Dto;
namespace EnvelopeGenerator.Application.EmailTemplates.Commands.Update;
/// <summary>
///
/// </summary>
public class UpdateEmailTemplateCommandHandler : IRequestHandler<UpdateEmailTemplateCommand>
{
private readonly IRepository<EmailTemplate> _repository;
private readonly IMapper _mapper;
/// <summary>
///
/// </summary>
/// <param name="repository"></param>
public UpdateEmailTemplateCommandHandler(IRepository<EmailTemplate> repository, IMapper mapper)
{
_repository = repository;
_mapper = mapper;
}
/// <summary>
///
/// </summary>
/// <param name="request"></param>
/// <param name="cancel"></param>
/// <returns></returns>
/// <exception cref="InvalidOperationException"></exception>
/// <exception cref="NotFoundException"></exception>
[Obsolete("Use Read-method returning IReadQuery<TEntity> instead.")]
public async Task Handle(UpdateEmailTemplateCommand request, CancellationToken cancel)
{
EmailTemplateDto? tempDto;
if (request.EmailTemplateQuery?.Id is int id)
{
var temp = await _repository.ReadOnly().Where(t => t.Id == id).FirstOrDefaultAsync(cancel);
tempDto = _mapper.Map<EmailTemplateDto>(temp);
}
else if (request!.EmailTemplateQuery!.Type is EmailTemplateType type)
{
var temp = await _repository.ReadOnly().Where(t => t.Name == type.ToString()).FirstOrDefaultAsync(cancel);
tempDto = _mapper.Map<EmailTemplateDto>(temp);
}
else
{
throw new InvalidOperationException("Both id and type is null. Id: " + request.EmailTemplateQuery.Id +". Type: " + request.EmailTemplateQuery.Type.ToString());
}
if (tempDto == null)
{
throw new NotFoundException();
}
if (request.Body is not null)
tempDto.Body = request.Body;
if (request.Subject is not null)
tempDto.Subject = request.Subject;
await _repository.UpdateAsync(tempDto, t => t.Id == tempDto.Id, cancel);
}
}

View File

@@ -1,63 +0,0 @@
using DigitalData.Core.Abstraction.Application.Repository;
using EnvelopeGenerator.Application.Common.Commands;
using EnvelopeGenerator.Domain.Constants;
using EnvelopeGenerator.Domain.Entities;
using System.Linq.Expressions;
namespace EnvelopeGenerator.Application.EmailTemplates.Commands;
/// <summary>
///
/// </summary>
/// <param name="Body"></param>
/// <param name="Subject"></param>
public record EmailTemplateUpdateDto(string Body, string Subject);
/// <summary>
/// Befehl zum Aktualisieren einer E-Mail-Vorlage.
/// </summary>
public record UpdateEmailTemplateCommand : UpdateCommand<EmailTemplateUpdateDto, EmailTemplate>, IEmailTemplateQuery
{
/// <summary>
/// Die eindeutige Kennung der E-Mail-Vorlage (optional).
/// </summary>
public int? Id { get; set; }
/// <summary>
/// Der Typ der E-Mail-Vorlage, z. B. <see cref="EmailTemplateType"/> (optional). Beispiele:<br/>
/// 0 - DocumentReceived: Benachrichtigung über den Empfang eines Dokuments.<br/>
/// 1 - DocumentSigned: Benachrichtigung über die Unterzeichnung eines Dokuments.<br/>
/// 2 - DocumentDeleted: Benachrichtigung über das Löschen eines Dokuments.<br/>
/// 3 - DocumentCompleted: Benachrichtigung über den Abschluss eines Dokuments.<br/>
/// 4 - DocumentAccessCodeReceived: Benachrichtigung über den Erhalt eines Zugangscodes.<br/>
/// 5 - DocumentShared: Benachrichtigung über das Teilen eines Dokuments.<br/>
/// 6 - TotpSecret: Benachrichtigung über ein TOTP-Geheimnis.<br/>
/// 7 - DocumentRejected_ADM (für den Absender): Mail an den Absender, wenn das Dokument abgelehnt wird.<br/>
/// 8 - DocumentRejected_REC (für den ablehnenden Empfänger): Mail an den ablehnenden Empfänger, wenn das Dokument abgelehnt wird.<br/>
/// 9 - DocumentRejected_REC_2 (für sonstige Empfänger): Mail an andere Empfänger (Brief), wenn das Dokument abgelehnt wird.
/// </summary>
public EmailTemplateType? Type { get; set; }
/// <summary>
///
/// </summary>
/// <returns></returns>
public override Expression<Func<EmailTemplate, bool>> BuildQueryExpression()
=> Id is int id
? temp => temp.Id == id
: temp => temp!.Name == Type.ToString();
}
/// <summary>
///
/// </summary>
public class UpdateEmailTemplateCommandHandler : UpdateCommandHandler<UpdateEmailTemplateCommand, EmailTemplateUpdateDto, EmailTemplate>
{
/// <summary>
///
/// </summary>
/// <param name="repository"></param>
public UpdateEmailTemplateCommandHandler(IRepository<EmailTemplate> repository) : base(repository)
{
}
}

View File

@@ -0,0 +1,24 @@
using EnvelopeGenerator.Domain.Constants;
namespace EnvelopeGenerator.Application.EmailTemplates;
/// <summary>
/// Repräsentiert eine Abfrage für E-Mail-Vorlagen, die für Absender und Empfänger von Umschlägen verwendet werden.
/// Die Standardkultur ist "de-DE".
/// </summary>
/// <param name="Id">Die eindeutige Kennung der E-Mail-Vorlage (optional).</param>
/// <param name="Type">Der Typ der E-Mail-Vorlage, z. B. <see cref="EmailTemplateType"/> (optional). Beispiele:
/// 0 - DocumentReceived: Benachrichtigung über den Empfang eines Dokuments.
/// 1 - DocumentSigned: Benachrichtigung über die Unterzeichnung eines Dokuments.
/// 2 - DocumentDeleted: Benachrichtigung über das Löschen eines Dokuments.
/// 3 - DocumentCompleted: Benachrichtigung über den Abschluss eines Dokuments.
/// 4 - DocumentAccessCodeReceived: Benachrichtigung über den Erhalt eines Zugangscodes.
/// 5 - DocumentShared: Benachrichtigung über das Teilen eines Dokuments.
/// 6 - TotpSecret: Benachrichtigung über ein TOTP-Geheimnis.
/// 7 - DocumentRejected_ADM (Für den Absender): Mail an den Absender, wenn das Dokument abgelehnt wird.
/// 8 - DocumentRejected_REC (Für den ablehnenden Empfänger): Mail an den ablehnenden Empfänger, wenn das Dokument abgelehnt wird.
/// 9 - DocumentRejected_REC_2 (Für sonstige Empfänger): Mail an andere Empfänger (Brief), wenn das Dokument abgelehnt wird.
/// </param>
public record EmailTemplateQuery(int? Id = null, EmailTemplateType? Type = null)
{
}

View File

@@ -1,30 +0,0 @@
using EnvelopeGenerator.Domain.Constants;
namespace EnvelopeGenerator.Application.EmailTemplates;
/// <summary>
/// Stellt eine Schnittstelle für Abfragen von E-Mail-Vorlagen dar, die für Absender und Empfänger von Umschlägen verwendet werden.
/// Die Standardkultur ist "de-DE".
/// </summary>
public interface IEmailTemplateQuery
{
/// <summary>
/// Die eindeutige Kennung der E-Mail-Vorlage (optional).
/// </summary>
int? Id { get; set; }
/// <summary>
/// Der Typ der E-Mail-Vorlage, z. B. <see cref="EmailTemplateType"/> (optional). Beispiele:<br/>
/// 0 - DocumentReceived: Benachrichtigung über den Empfang eines Dokuments.<br/>
/// 1 - DocumentSigned: Benachrichtigung über die Unterzeichnung eines Dokuments.<br/>
/// 2 - DocumentDeleted: Benachrichtigung über das Löschen eines Dokuments.<br/>
/// 3 - DocumentCompleted: Benachrichtigung über den Abschluss eines Dokuments.<br/>
/// 4 - DocumentAccessCodeReceived: Benachrichtigung über den Erhalt eines Zugangscodes.<br/>
/// 5 - DocumentShared: Benachrichtigung über das Teilen eines Dokuments.<br/>
/// 6 - TotpSecret: Benachrichtigung über ein TOTP-Geheimnis.<br/>
/// 7 - DocumentRejected_ADM (für den Absender): Mail an den Absender, wenn das Dokument abgelehnt wird.<br/>
/// 8 - DocumentRejected_REC (für den ablehnenden Empfänger): Mail an den ablehnenden Empfänger, wenn das Dokument abgelehnt wird.<br/>
/// 9 - DocumentRejected_REC_2 (für sonstige Empfänger): Mail an andere Empfänger (Brief), wenn das Dokument abgelehnt wird.
/// </summary>
EmailTemplateType? Type { get; set; }
}

View File

@@ -1,8 +1,11 @@
using AutoMapper; using AutoMapper;
using EnvelopeGenerator.Application.Common.Dto; using EnvelopeGenerator.Application.EmailTemplates.Queries.Read;
using EnvelopeGenerator.Application.Common.Extensions;
using EnvelopeGenerator.Application.EmailTemplates.Commands;
using EnvelopeGenerator.Domain.Entities; using EnvelopeGenerator.Domain.Entities;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace EnvelopeGenerator.Application.EmailTemplates; namespace EnvelopeGenerator.Application.EmailTemplates;
@@ -16,9 +19,6 @@ public class MappingProfile : Profile
/// </summary> /// </summary>
public MappingProfile() public MappingProfile()
{ {
CreateMap<EmailTemplate, EmailTemplateDto>(); CreateMap<EmailTemplate, ReadEmailTemplateResponse>();
CreateMap<EmailTemplateUpdateDto, EmailTemplate>()
.MapChangedWhen();
} }
} }

View File

@@ -0,0 +1,12 @@
using MediatR;
namespace EnvelopeGenerator.Application.EmailTemplates.Queries.Read;
/// <summary>
/// Stellt eine Abfrage dar, um eine E-Mail-Vorlage zu lesen.
/// Diese Klasse erbt von <see cref="EmailTemplateQuery"/>.
/// </summary>
public record ReadEmailTemplateQuery : EmailTemplateQuery, IRequest<ReadEmailTemplateResponse?>
{
}

View File

@@ -0,0 +1,52 @@
using AutoMapper;
using EnvelopeGenerator.Application.Common.Interfaces.Repositories;
using EnvelopeGenerator.Domain.Constants;
using MediatR;
namespace EnvelopeGenerator.Application.EmailTemplates.Queries.Read;
/// <summary>
///
/// </summary>
public class ReadEmailTemplateQueryHandler : IRequestHandler<ReadEmailTemplateQuery, ReadEmailTemplateResponse?>
{
private readonly IMapper _mapper;
[Obsolete("Use Read-method returning IReadQuery<TEntity> instead.")]
private readonly IEmailTemplateRepository _repository;
/// <summary>
/// Initialisiert eine neue Instanz der <see cref="EmailTemplateController"/>-Klasse.
/// </summary>
/// <param name="mapper">
/// <param name="repository">
/// Die AutoMapper-Instanz, die zum Zuordnen von Objekten verwendet wird.
/// </param>
[Obsolete("Use Read-method returning IReadQuery<TEntity> instead.")]
public ReadEmailTemplateQueryHandler(IMapper mapper, IEmailTemplateRepository repository)
{
_mapper = mapper;
_repository = repository;
}
/// <summary>
///
/// </summary>
/// <param name="request"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
/// <exception cref="InvalidOperationException"></exception>
[Obsolete("Use IRepository")]
public async Task<ReadEmailTemplateResponse?> Handle(ReadEmailTemplateQuery request, CancellationToken cancellationToken)
{
var temp = request.Id is int id
? await _repository.ReadByIdAsync(id)
: request.Type is EmailTemplateType type
? await _repository.ReadByNameAsync(type)
: throw new InvalidOperationException("Either a valid integer ID or a valid EmailTemplateType must be provided in the request.");
var res = _mapper.Map<ReadEmailTemplateResponse>(temp);
return res;
}
}

View File

@@ -0,0 +1,37 @@
namespace EnvelopeGenerator.Application.EmailTemplates.Queries.Read;
/// <summary>
/// Stellt die Antwort für eine Abfrage von E-Mail-Vorlagen bereit.
/// </summary>
public class ReadEmailTemplateResponse
{
/// <summary>
/// Die eindeutige Kennung der E-Mail-Vorlage.
/// </summary>
public int Id { get; set; }
/// <summary>
/// Name des Typs
/// </summary>
public required string Name { get; set; }
/// <summary>
/// Das Datum und die Uhrzeit, wann die Vorlage hinzugefügt wurde.
/// </summary>
public DateTime AddedWhen { get; set; }
/// <summary>
/// Der Inhalt (Body) der E-Mail-Vorlage. Kann null sein.
/// </summary>
public string? Body { get; set; }
/// <summary>
/// Der Betreff der E-Mail-Vorlage. Kann null sein.
/// </summary>
public string? Subject { get; set; }
/// <summary>
/// Das Datum und die Uhrzeit, wann die Vorlage zuletzt geändert wurde. Kann null sein.
/// </summary>
public DateTime? ChangedWhen { get; set; }
}

View File

@@ -1,125 +0,0 @@
using AutoMapper;
using MediatR;
using EnvelopeGenerator.Application.Common.Dto;
using DigitalData.Core.Abstraction.Application.Repository;
using EnvelopeGenerator.Domain.Entities;
using Microsoft.EntityFrameworkCore;
using EnvelopeGenerator.Domain.Constants;
using DigitalData.Core.Exceptions;
using EnvelopeGenerator.Application.Common.Extensions;
namespace EnvelopeGenerator.Application.EmailTemplates.Queries;
/// <summary>
/// Stellt eine Abfrage dar, um eine E-Mail-Vorlage zu lesen.
/// Diese Klasse erbt von <see cref="IEmailTemplateQuery"/>.
/// </summary>
public record ReadEmailTemplateQuery : IEmailTemplateQuery, IRequest<IEnumerable<EmailTemplateDto>>
{
/// <summary>
/// Die eindeutige Kennung der E-Mail-Vorlage (optional).
/// </summary>
public int? Id { get; set; }
/// <summary>
/// Der Typ der E-Mail-Vorlage, z. B. <see cref="EmailTemplateType"/> (optional). Beispiele:<br/>
/// 0 - DocumentReceived: Benachrichtigung über den Empfang eines Dokuments.<br/>
/// 1 - DocumentSigned: Benachrichtigung über die Unterzeichnung eines Dokuments.<br/>
/// 2 - DocumentDeleted: Benachrichtigung über das Löschen eines Dokuments.<br/>
/// 3 - DocumentCompleted: Benachrichtigung über den Abschluss eines Dokuments.<br/>
/// 4 - DocumentAccessCodeReceived: Benachrichtigung über den Erhalt eines Zugangscodes.<br/>
/// 5 - DocumentShared: Benachrichtigung über das Teilen eines Dokuments.<br/>
/// 6 - TotpSecret: Benachrichtigung über ein TOTP-Geheimnis.<br/>
/// 7 - DocumentRejected_ADM (für den Absender): Mail an den Absender, wenn das Dokument abgelehnt wird.<br/>
/// 8 - DocumentRejected_REC (für den ablehnenden Empfänger): Mail an den ablehnenden Empfänger, wenn das Dokument abgelehnt wird.<br/>
/// 9 - DocumentRejected_REC_2 (für sonstige Empfänger): Mail an andere Empfänger (Brief), wenn das Dokument abgelehnt wird.
/// </summary>
public EmailTemplateType? Type { get; set; }
/// <summary>
///
/// </summary>
public string? LangCode { get; set; }
}
/// <summary>
///
/// </summary>
public static class ReadEmailTemplateQueryExtensions
{
/// <summary>
///
/// </summary>
/// <param name="sender"></param>
/// <param name="id"></param>
/// <param name="langCode"></param>
/// <param name="cancel"></param>
/// <returns></returns>
public static async Task<EmailTemplateDto?> ReadEmailTemplateAsync(this ISender sender, int id, string langCode, CancellationToken cancel = default)
{
var result = await sender.Send(new ReadEmailTemplateQuery { Id = id, LangCode = langCode }, cancel);
return result.FirstOrDefault();
}
/// <summary>
///
/// </summary>
/// <param name="sender"></param>
/// <param name="type"></param>
/// <param name="langCode"></param>
/// <param name="cancel"></param>
/// <returns></returns>
public static async Task<EmailTemplateDto?> ReadEmailTemplateAsync(this ISender sender, EmailTemplateType type, string langCode, CancellationToken cancel = default)
{
var result = await sender.Send(new ReadEmailTemplateQuery { Type = type, LangCode = langCode }, cancel);
return result.FirstOrDefault();
}
}
/// <summary>
///
/// </summary>
public class ReadEmailTemplateQueryHandler : IRequestHandler<ReadEmailTemplateQuery, IEnumerable<EmailTemplateDto>>
{
private readonly IMapper _mapper;
private readonly IRepository<EmailTemplate> _repo;
/// <summary>
/// Initialisiert eine neue Instanz der <see cref="EmailTemplateController"/>-Klasse.
/// </summary>
/// <param name="mapper">
/// <param name="repo">
/// Die AutoMapper-Instanz, die zum Zuordnen von Objekten verwendet wird.
/// </param>
public ReadEmailTemplateQueryHandler(IMapper mapper, IRepository<EmailTemplate> repo)
{
_mapper = mapper;
_repo = repo;
}
/// <summary>
///
/// </summary>
/// <param name="request"></param>
/// <param name="cancel"></param>
/// <returns></returns>
/// <exception cref="InvalidOperationException"></exception>
public async Task<IEnumerable<EmailTemplateDto>> Handle(ReadEmailTemplateQuery request, CancellationToken cancel)
{
var query = _repo.Query;
if (request.Id is int id)
query = query.Where(temp => temp.Id == id);
if (request.Type is EmailTemplateType type)
query = query.Where(temp => temp.Name == type.ToString());
if (request.LangCode is string langCode)
query = query.Where(temp => temp.LangCode == langCode);
var entity = await query.ToListAsync(cancel);
return _mapper.Map<IEnumerable<EmailTemplateDto>>(entity);
}
}

View File

@@ -4,7 +4,6 @@
<TargetFrameworks>net7.0;net8.0;net9.0</TargetFrameworks> <TargetFrameworks>net7.0;net8.0;net9.0</TargetFrameworks>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<LangVersion>latest</LangVersion>
<GenerateDocumentationFile>true</GenerateDocumentationFile> <GenerateDocumentationFile>true</GenerateDocumentationFile>
<DocumentationFile>bin\$(Configuration)\$(TargetFramework)\$(AssemblyName).xml</DocumentationFile> <DocumentationFile>bin\$(Configuration)\$(TargetFramework)\$(AssemblyName).xml</DocumentationFile>
</PropertyGroup> </PropertyGroup>
@@ -15,22 +14,17 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="Dapper" Version="2.1.66" /> <PackageReference Include="Dapper" Version="2.1.66" />
<PackageReference Include="DigitalData.Core.Abstraction.Application" Version="1.6.0" /> <PackageReference Include="DigitalData.Core.Abstraction.Application" Version="1.4.0" />
<PackageReference Include="DigitalData.Core.Application" Version="3.4.0" /> <PackageReference Include="DigitalData.Core.Application" Version="3.4.0" />
<PackageReference Include="DigitalData.Core.Client" Version="2.1.0" /> <PackageReference Include="DigitalData.Core.Client" Version="2.1.0" />
<PackageReference Include="DigitalData.Core.Exceptions" Version="1.1.0" /> <PackageReference Include="DigitalData.Core.Exceptions" Version="1.1.0" />
<PackageReference Include="DigitalData.EmailProfilerDispatcher" Version="3.1.1" /> <PackageReference Include="DigitalData.EmailProfilerDispatcher" Version="3.1.1" />
<PackageReference Include="FluentValidation" Version="11.11.0" /> <PackageReference Include="HtmlSanitizer" Version="8.0.865" />
<PackageReference Include="FluentValidation.DependencyInjectionExtensions" Version="11.11.0" />
<PackageReference Include="HtmlSanitizer" Version="9.0.892" />
<PackageReference Include="MediatR" Version="12.5.0" /> <PackageReference Include="MediatR" Version="12.5.0" />
<PackageReference Include="Microsoft.Identity.Client" Version="4.82.1" /> <PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="7.0.18" />
<PackageReference Include="Otp.NET" Version="1.4.0" /> <PackageReference Include="Otp.NET" Version="1.4.0" />
<PackageReference Include="QRCoder" Version="1.6.0" /> <PackageReference Include="QRCoder" Version="1.6.0" />
<PackageReference Include="QRCoder-ImageSharp" Version="0.10.0" /> <PackageReference Include="QRCoder-ImageSharp" Version="0.10.0" />
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.12" />
<PackageReference Include="System.Formats.Asn1" Version="10.0.3" />
<PackageReference Include="System.Security.AccessControl" Version="6.0.1" />
<PackageReference Include="UserManager" Version="1.1.3" /> <PackageReference Include="UserManager" Version="1.1.3" />
</ItemGroup> </ItemGroup>
@@ -39,7 +33,7 @@
</ItemGroup> </ItemGroup>
<ItemGroup Condition="'$(TargetFramework)' == 'net8.0'"> <ItemGroup Condition="'$(TargetFramework)' == 'net8.0'">
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.1" /> <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.5" />
</ItemGroup> </ItemGroup>
<ItemGroup Condition="'$(TargetFramework)' == 'net9.0'"> <ItemGroup Condition="'$(TargetFramework)' == 'net9.0'">
@@ -69,9 +63,6 @@
<LastGenOutput>Model.Designer.cs</LastGenOutput> <LastGenOutput>Model.Designer.cs</LastGenOutput>
<Generator>PublicResXFileCodeGenerator</Generator> <Generator>PublicResXFileCodeGenerator</Generator>
</EmbeddedResource> </EmbeddedResource>
<EmbeddedResource Update="Resources\Resource.fr-FR.resx">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</EmbeddedResource>
<EmbeddedResource Update="Resources\Resource.de-DE.resx"> <EmbeddedResource Update="Resources\Resource.de-DE.resx">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</EmbeddedResource> </EmbeddedResource>
@@ -82,7 +73,7 @@
<ItemGroup Condition="'$(TargetFramework)' == 'net7.0'"> <ItemGroup Condition="'$(TargetFramework)' == 'net7.0'">
<PackageReference Include="AutoMapper" Version="13.0.1" /> <PackageReference Include="AutoMapper" Version="13.0.1" />
<PackageReference Include="Microsoft.Data.SqlClient" Version="5.2.2" /> <PackageReference Include="Microsoft.Data.SqlClient" Version="5.2.3" />
<PackageReference Include="CommandDotNet"> <PackageReference Include="CommandDotNet">
<Version>7.0.5</Version> <Version>7.0.5</Version>
</PackageReference> </PackageReference>
@@ -90,6 +81,7 @@
<ItemGroup Condition="'$(TargetFramework)' == 'net8.0'"> <ItemGroup Condition="'$(TargetFramework)' == 'net8.0'">
<PackageReference Include="AutoMapper" Version="14.0.0" /> <PackageReference Include="AutoMapper" Version="14.0.0" />
<PackageReference Include="Microsoft.Data.SqlClient" Version="6.0.2" />
<PackageReference Include="CommandDotNet"> <PackageReference Include="CommandDotNet">
<Version>8.1.1</Version> <Version>8.1.1</Version>
</PackageReference> </PackageReference>
@@ -97,6 +89,7 @@
<ItemGroup Condition="'$(TargetFramework)' == 'net9.0'"> <ItemGroup Condition="'$(TargetFramework)' == 'net9.0'">
<PackageReference Include="AutoMapper" Version="14.0.0" /> <PackageReference Include="AutoMapper" Version="14.0.0" />
<PackageReference Include="Microsoft.Data.SqlClient" Version="6.0.2" />
<PackageReference Include="CommandDotNet"> <PackageReference Include="CommandDotNet">
<Version>8.1.1</Version> <Version>8.1.1</Version>
</PackageReference> </PackageReference>

View File

@@ -2,7 +2,7 @@
using MediatR; using MediatR;
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
namespace EnvelopeGenerator.Application.EnvelopeReceivers.Commands.CreateEnvelopeReceiver; namespace EnvelopeGenerator.Application.EnvelopeReceivers.Commands;
/// <summary> /// <summary>
/// Befehl zur Erstellung eines Umschlags. /// Befehl zur Erstellung eines Umschlags.
@@ -29,7 +29,7 @@ public record CreateEnvelopeReceiverCommand : CreateEnvelopeCommand, IRequest<Cr
/// <param name="X">X-Position</param> /// <param name="X">X-Position</param>
/// <param name="Y">Y-Position</param> /// <param name="Y">Y-Position</param>
/// <param name="Page">Seite, auf der sie sich befindet</param> /// <param name="Page">Seite, auf der sie sich befindet</param>
public record DocReceiverElementCreateDto([Required] double X, [Required] double Y, [Required] int Page); public record Signature([Required] double X, [Required] double Y, [Required] int Page);
/// <summary> /// <summary>
/// DTO für Empfänger, die erstellt oder abgerufen werden sollen. /// DTO für Empfänger, die erstellt oder abgerufen werden sollen.
@@ -41,7 +41,7 @@ public class ReceiverGetOrCreateCommand
/// Unterschriften auf Dokumenten. /// Unterschriften auf Dokumenten.
/// </summary> /// </summary>
[Required] [Required]
public List<DocReceiverElementCreateDto> DocReceiverElements { get; init; } = new(); public List<Signature> Signatures { get; init; } = new();
/// <summary> /// <summary>
/// Der Name, mit dem der Empfänger angesprochen werden soll. /// Der Name, mit dem der Empfänger angesprochen werden soll.

View File

@@ -4,7 +4,7 @@ using MediatR;
using EnvelopeGenerator.Application.Common.Dto.Receiver; using EnvelopeGenerator.Application.Common.Dto.Receiver;
using EnvelopeGenerator.Application.Common.Interfaces.SQLExecutor; using EnvelopeGenerator.Application.Common.Interfaces.SQLExecutor;
namespace EnvelopeGenerator.Application.EnvelopeReceivers.Commands.CreateEnvelopeReceiver; namespace EnvelopeGenerator.Application.EnvelopeReceivers.Commands;
/// <summary> /// <summary>
/// Handles the creation of an envelope along with its associated document and recipients. /// Handles the creation of an envelope along with its associated document and recipients.

View File

@@ -1,7 +1,7 @@
using EnvelopeGenerator.Application.Common.Dto; using EnvelopeGenerator.Application.Common.Dto;
using EnvelopeGenerator.Application.Common.Dto.Receiver; using EnvelopeGenerator.Application.Common.Dto.Receiver;
namespace EnvelopeGenerator.Application.EnvelopeReceivers.Commands.CreateEnvelopeReceiver; namespace EnvelopeGenerator.Application.EnvelopeReceivers.Commands;
/// <summary> /// <summary>
/// ///

View File

@@ -1,84 +0,0 @@
using DigitalData.Core.Abstraction.Application.Repository;
using DigitalData.Core.Exceptions;
using EnvelopeGenerator.Domain.Constants;
using EnvelopeGenerator.Domain.Entities;
using EnvelopeGenerator.Domain.Exceptions;
using MediatR;
using Microsoft.EntityFrameworkCore;
namespace EnvelopeGenerator.Application.EnvelopeReceivers.Commands;
/// <summary>
///
/// </summary>
public record RemoveEnvelopeReceiverCommand : IRequest
{
/// <summary>
///
/// </summary>
public required int EnvelopeId { get; init; }
/// <summary>
///
/// </summary>
public required int ReceiverId { get; init; }
internal int? UserId { get; private set; }
/// <summary>
///
/// </summary>
/// <param name="userId"></param>
/// <returns></returns>
public RemoveEnvelopeReceiverCommand Authenticated(int userId)
{
UserId = userId;
return this;
}
}
/// <summary>
///
/// </summary>
/// <param name="Repo"></param>
public class RemoveEnvelopeReceiverCommandHandler(
IRepository<EnvelopeReceiver> Repo,
IRepository<DocReceiverElement> ElementRepo,
IRepository<ElementAnnotation> AnnotRepo) : IRequestHandler<RemoveEnvelopeReceiverCommand>
{
/// <summary>
///
/// </summary>
/// <param name="request"></param>
/// <param name="cancel"></param>
/// <returns></returns>
/// <exception cref="NotImplementedException"></exception>
public async Task Handle(RemoveEnvelopeReceiverCommand request, CancellationToken cancel)
{
var er = await Repo.Query
.Where(er => er.EnvelopeId == request.EnvelopeId && er.ReceiverId == request.ReceiverId)
.Include(er => er.Envelope)
.SingleOrDefaultAsync(cancel)
?? throw new NotFoundException();
if (er.Envelope is null)
throw new InvalidOperationException($"In {nameof(RemoveEnvelopeReceiverCommandHandler)} EnvelopeReceiver.Envelope is null." +
$"This should not happen." +
$"EnvelopeId: {request.EnvelopeId}, ReceiverId: {request.ReceiverId}");
if (request.UserId is int userId && er.Envelope.UserId != userId)
throw new AuthorizationException("Cannot remove receiver from envelope that you do not own.");
if (er.Envelope.Status > EnvelopeStatus.EnvelopeSaved)
throw new ForbiddenException("Cannot remove receiver from envelope that is already processed.");
// 1. Delete all ElementAnnotations for this EnvelopeReceiver
await AnnotRepo.DeleteAsync(ann => ann.Element.Document.EnvelopeId == request.EnvelopeId && ann.Element.ReceiverId == request.ReceiverId, cancel);
// 2. Delete all DocReceiverElements for this EnvelopeReceiver
await ElementRepo.DeleteAsync(el => el.Document.EnvelopeId == request.EnvelopeId && el.ReceiverId == request.ReceiverId, cancel);
// 3. Delete the EnvelopeReceiver
await Repo.DeleteAsync(er => er.EnvelopeId == request.EnvelopeId && er.ReceiverId == request.ReceiverId, cancel);
}
}

View File

@@ -16,9 +16,5 @@ public class MappingProfile : Profile
public MappingProfile() public MappingProfile()
{ {
CreateMap<Receiver, ReceiverDto>(); CreateMap<Receiver, ReceiverDto>();
CreateMap<EnvelopeReceiver, RecentReceiverSuggestionDto>()
.ForMember(dest => dest.FullName, opt => opt.MapFrom(src => src.Name))
.ForMember(dest => dest.EmailAddress, opt => opt.MapFrom(src => src.Receiver.EmailAddress));
} }
} }

View File

@@ -1,4 +1,4 @@
using AutoMapper; using AutoMapper;
using DigitalData.Core.Abstraction.Application.Repository; using DigitalData.Core.Abstraction.Application.Repository;
using DigitalData.Core.Exceptions; using DigitalData.Core.Exceptions;
using EnvelopeGenerator.Application.Envelopes.Queries; using EnvelopeGenerator.Application.Envelopes.Queries;
@@ -47,13 +47,7 @@ namespace EnvelopeGenerator.Application.EnvelopeReceivers.Queries;
/// Die Antwort enthält Details wie den Include, die Zuordnung zwischen Umschlag und Empfänger /// Die Antwort enthält Details wie den Include, die Zuordnung zwischen Umschlag und Empfänger
/// sowie zusätzliche Metadaten. /// sowie zusätzliche Metadaten.
/// </remarks> /// </remarks>
public record ReadEnvelopeReceiverQuery : EnvelopeReceiverQueryBase<ReadEnvelopeQuery, ReadReceiverQuery>, IRequest<IEnumerable<EnvelopeReceiverDto>> public record ReadEnvelopeReceiverQuery : EnvelopeReceiverQueryBase<ReadEnvelopeQuery, ReadReceiverQuery>, IRequest<IEnumerable<EnvelopeReceiverDto>>;
{
/// <summary>
/// Optionaler Benutzernamefilter, um Ergebnisse auf Umschläge eines bestimmten Besitzers einzuschränken.
/// </summary>
public string? Username { get; init; }
}
/// <summary> /// <summary>
/// ///
@@ -67,11 +61,10 @@ public static class Extensions
/// <param name="key"></param> /// <param name="key"></param>
/// <param name="cancel"></param> /// <param name="cancel"></param>
/// <returns></returns> /// <returns></returns>
public static async Task<EnvelopeReceiverDto?> ReadEnvelopeReceiverAsync(this IMediator mediator, string key, CancellationToken cancel = default) public static Task<EnvelopeReceiverDto?> ReadEnvelopeReceiverAsync(this IMediator mediator, string key, CancellationToken cancel = default)
{ {
var q = new ReadEnvelopeReceiverQuery() { Key = key }; var q = new ReadEnvelopeReceiverQuery() { Key = key };
var envRcvs = await mediator.Send(q, cancel); return mediator.Send(q, cancel).Then(envRcvs => envRcvs.FirstOrDefault());
return envRcvs.FirstOrDefault();
} }
/// <summary> /// <summary>
@@ -82,28 +75,30 @@ public static class Extensions
/// <param name="signature"></param> /// <param name="signature"></param>
/// <param name="cancel"></param> /// <param name="cancel"></param>
/// <returns></returns> /// <returns></returns>
public static async Task<EnvelopeReceiverDto?> ReadEnvelopeReceiverAsync(this IMediator mediator, string uuid, string signature, CancellationToken cancel = default) public static Task<EnvelopeReceiverDto?> ReadEnvelopeReceiverAsync(this IMediator mediator, string uuid, string signature, CancellationToken cancel = default)
{ {
var q = new ReadEnvelopeReceiverQuery(); var q = new ReadEnvelopeReceiverQuery();
q.Envelope.Uuid = uuid; q.Envelope.Uuid = uuid;
q.Receiver.Signature = signature; q.Receiver.Signature = signature;
var envRcvs = await mediator.Send(q, cancel); return mediator.Send(q, cancel).Then(envRcvs => envRcvs.FirstOrDefault());
return envRcvs.FirstOrDefault();
} }
}
/// <summary> /// <summary>
/// Verarbeitet <see cref="ReadEnvelopeReceiverQuery"/> und liefert passende <see cref="EnvelopeReceiverDto"/>-Ergebnisse. ///
/// </summary> /// </summary>
public class ReadEnvelopeReceiverQueryHandler : IRequestHandler<ReadEnvelopeReceiverQuery, IEnumerable<EnvelopeReceiverDto>> public class ReadEnvelopeReceiverQueryHandler : IRequestHandler<ReadEnvelopeReceiverQuery, IEnumerable<EnvelopeReceiverDto>>
{ {
private readonly IRepository<EnvelopeReceiver> _repo; private readonly IRepository<EnvelopeReceiver> _repo;
private readonly IRepository<Receiver> _rcvRepo; private readonly IRepository<Receiver> _rcvRepo;
private readonly IMapper _mapper; private readonly IMapper _mapper;
/// <summary> /// <summary>
/// ///
/// </summary> /// </summary>
/// <param name="envelopeReceiver"></param> /// <param name="envelopeReceiver"></param>
/// <param name="rcvRepo"></param>
/// <param name="mapper"></param> /// <param name="mapper"></param>
public ReadEnvelopeReceiverQueryHandler(IRepository<EnvelopeReceiver> envelopeReceiver, IRepository<Receiver> rcvRepo, IMapper mapper) public ReadEnvelopeReceiverQueryHandler(IRepository<EnvelopeReceiver> envelopeReceiver, IRepository<Receiver> rcvRepo, IMapper mapper)
{ {
@@ -121,10 +116,7 @@ public static class Extensions
/// <exception cref="BadRequestException"></exception> /// <exception cref="BadRequestException"></exception>
public async Task<IEnumerable<EnvelopeReceiverDto>> Handle(ReadEnvelopeReceiverQuery request, CancellationToken cancel) public async Task<IEnumerable<EnvelopeReceiverDto>> Handle(ReadEnvelopeReceiverQuery request, CancellationToken cancel)
{ {
var q = _repo.Query.Where(request, notnull: false); var q = _repo.ReadOnly().Where(request, notnull: false);
if (request.Username is string username)
q = q.Where(er => er.Envelope!.User.Username == username);
if (request.Envelope.Status is not null) if (request.Envelope.Status is not null)
{ {
@@ -149,15 +141,14 @@ public static class Extensions
.Include(er => er.Receiver) .Include(er => er.Receiver)
.ToListAsync(cancel); .ToListAsync(cancel);
if (request.Receiver.HasAnyCriteria && envRcvs.Count != 0) if (request.Receiver.HasAnyCriteria && envRcvs.Any())
{ {
var receiver = await _rcvRepo.Query.Where(request.Receiver).FirstAsync(cancel); var receiver = await _rcvRepo.ReadOnly().Where(request.Receiver).FirstAsync(cancel);
foreach (var envRcv in envRcvs) foreach (var envRcv in envRcvs)
envRcv.Envelope?.Documents?.FirstOrDefault()?.Elements?.RemoveAll(s => s.ReceiverId != receiver.Id); envRcv.Envelope?.Documents?.First().Elements.RemoveAll(s => s.ReceiverId != receiver.Id);
} }
return _mapper.Map<List<EnvelopeReceiverDto>>(envRcvs); return _mapper.Map<List<EnvelopeReceiverDto>>(envRcvs);
} }
} }
}

View File

@@ -1,144 +0,0 @@
using AutoMapper;
using DigitalData.Core.Abstraction.Application.Repository;
using EnvelopeGenerator.Application.Envelopes.Queries;
using EnvelopeGenerator.Application.Receivers.Queries;
using MediatR;
using EnvelopeGenerator.Domain.Entities;
using Microsoft.EntityFrameworkCore;
using EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiver;
using EnvelopeGenerator.Application.Common.Query;
using EnvelopeGenerator.Application.Common.Extensions;
namespace EnvelopeGenerator.Application.EnvelopeReceivers.Queries;
/// <summary>
/// Represents a query for reading an envelope receiver including sensitive fields
/// (access code, phone number) that are excluded from the standard <see cref="ReadEnvelopeReceiverQuery"/>.
/// </summary>
/// <remarks>
/// Returns a single <see cref="EnvelopeReceiverSecretDto"/> matched by UUID and receiver signature.
/// Equivalent to the legacy <c>ReadWithSecretByUuidSignatureAsync</c> service method.
/// </remarks>
public record ReadEnvelopeReceiverSecretQuery
: EnvelopeReceiverQueryBase<ReadEnvelopeQuery, ReadReceiverQuery>,
IRequest<EnvelopeReceiverSecretDto?>
{
/// <summary>
///
/// </summary>
public ReadEnvelopeReceiverSecretQuery() { }
/// <summary>
///
/// </summary>
/// <param name="envelopeId"></param>
/// <param name="receiverId"></param>
public ReadEnvelopeReceiverSecretQuery(int envelopeId, int receiverId)
{
Envelope.Id = envelopeId;
Receiver.Id = receiverId;
}
}
/// <summary>
/// Extension methods for dispatching <see cref="ReadEnvelopeReceiverSecretQuery"/> via <see cref="IMediator"/>.
/// </summary>
public static class ReadEnvelopeReceiverSecretQueryExtensions
{
/// <summary>
/// Sends a <see cref="ReadEnvelopeReceiverSecretQuery"/> using the composite key (uuid::signature).
/// </summary>
/// <param name="mediator">The mediator instance.</param>
/// <param name="key">Composite key in the format <c>uuid::signature</c>.</param>
/// <param name="cancel">Cancellation token.</param>
/// <returns>The matching <see cref="EnvelopeReceiverSecretDto"/>, or <c>null</c> if not found.</returns>
public static Task<EnvelopeReceiverSecretDto?> ReadEnvelopeReceiverSecretAsync(
this IMediator mediator,
string key,
CancellationToken cancel = default)
=> mediator.Send(new ReadEnvelopeReceiverSecretQuery { Key = key }, cancel);
/// <summary>
/// Sends a <see cref="ReadEnvelopeReceiverSecretQuery"/> using UUID and receiver signature.
/// </summary>
/// <param name="mediator">The mediator instance.</param>
/// <param name="uuid">Envelope UUID.</param>
/// <param name="signature">Receiver signature.</param>
/// <param name="cancel">Cancellation token.</param>
/// <returns>The matching <see cref="EnvelopeReceiverSecretDto"/>, or <c>null</c> if not found.</returns>
public static Task<EnvelopeReceiverSecretDto?> ReadEnvelopeReceiverSecretAsync(
this IMediator mediator,
string uuid,
string signature,
CancellationToken cancel = default)
{
var q = new ReadEnvelopeReceiverSecretQuery();
q.Envelope.Uuid = uuid;
q.Receiver.Signature = signature;
return mediator.Send(q, cancel);
}
/// <summary>
/// Handles <see cref="ReadEnvelopeReceiverSecretQuery"/> and returns a
/// <see cref="EnvelopeReceiverSecretDto"/> containing sensitive fields.
/// </summary>
public class ReadEnvelopeReceiverSecretQueryHandler
: IRequestHandler<ReadEnvelopeReceiverSecretQuery, EnvelopeReceiverSecretDto?>
{
private readonly IRepository<EnvelopeReceiver> _repo;
private readonly IRepository<Receiver> _rcvRepo;
private readonly IMapper _mapper;
/// <summary>
/// Initializes a new instance of <see cref="ReadEnvelopeReceiverSecretQueryHandler"/>.
/// </summary>
/// <param name="envelopeReceiver">Repository for <see cref="EnvelopeReceiver"/>.</param>
/// <param name="rcvRepo">Repository for <see cref="Receiver"/>.</param>
/// <param name="mapper">AutoMapper instance.</param>
public ReadEnvelopeReceiverSecretQueryHandler(
IRepository<EnvelopeReceiver> envelopeReceiver,
IRepository<Receiver> rcvRepo,
IMapper mapper)
{
_repo = envelopeReceiver;
_rcvRepo = rcvRepo;
_mapper = mapper;
}
/// <summary>
/// Handles the query and returns the matching <see cref="EnvelopeReceiverSecretDto"/>.
/// </summary>
/// <param name="request">The query containing filter criteria.</param>
/// <param name="cancel">Cancellation token.</param>
/// <returns>
/// The matched <see cref="EnvelopeReceiverSecretDto"/>, or <c>null</c> if no record is found.
/// </returns>
public async Task<EnvelopeReceiverSecretDto?> Handle(
ReadEnvelopeReceiverSecretQuery request,
CancellationToken cancel)
{
var q = _repo.Query.Where(request, notnull: false);
var envRcvs = await q
.Include(er => er.Envelope).ThenInclude(e => e!.Documents!).ThenInclude(d => d.Elements)
.Include(er => er.Envelope).ThenInclude(e => e!.Histories)
.Include(er => er.Envelope).ThenInclude(e => e!.User)
.Include(er => er.Receiver)
.ToListAsync(cancel);
if (request.Receiver.HasAnyCriteria && envRcvs.Count != 0)
{
var receiver = await _rcvRepo.Query.Where(request.Receiver).FirstAsync(cancel);
foreach (var item in envRcvs)
item.Envelope?.Documents?.FirstOrDefault()?.Elements?.RemoveAll(s => s.ReceiverId != receiver.Id);
}
var envRcv = envRcvs.FirstOrDefault();
if (envRcv is null)
return null;
return _mapper.Map<EnvelopeReceiverSecretDto>(envRcv);
}
}
}

Some files were not shown because too many files have changed in this diff Show More