diff --git a/EnvelopeGenerator.Application/Common/Interfaces/IDocReceiverElementWriter.cs b/EnvelopeGenerator.Application/Common/Interfaces/IDocReceiverElementWriter.cs deleted file mode 100644 index 50941dee..00000000 --- a/EnvelopeGenerator.Application/Common/Interfaces/IDocReceiverElementWriter.cs +++ /dev/null @@ -1,10 +0,0 @@ -using EnvelopeGenerator.Domain.Entities; - -namespace EnvelopeGenerator.Application.Common.Interfaces; - -public interface IDocReceiverElementWriter -{ - Task DeleteByDocumentAndReceiverAsync(int documentId, int receiverId, CancellationToken cancellationToken = default); - - Task InsertAsync(DocReceiverElement element, CancellationToken cancellationToken = default); -} diff --git a/EnvelopeGenerator.Application/Envelopes/Commands/CreateEnvelopeCommandHandler.cs b/EnvelopeGenerator.Application/Envelopes/Commands/CreateEnvelopeCommandHandler.cs index f8d9a4db..6229e4f7 100644 --- a/EnvelopeGenerator.Application/Envelopes/Commands/CreateEnvelopeCommandHandler.cs +++ b/EnvelopeGenerator.Application/Envelopes/Commands/CreateEnvelopeCommandHandler.cs @@ -5,7 +5,6 @@ using DigitalData.Core.Abstraction.Application.Repository; using DigitalData.Core.Exceptions; using EnvelopeGenerator.Domain.Entities; using EnvelopeGenerator.Application.Common.Dto; -using EnvelopeGenerator.Application.Common.Interfaces; using EnvelopeGenerator.Application.Common.Interfaces.SQLExecutor; using EnvelopeGenerator.Application.Histories.Commands; using EnvelopeGenerator.Domain.Constants; @@ -33,8 +32,6 @@ public class CreateEnvelopeCommandHandler : IRequestHandler DocReceiverElementRepository => _provider.GetRequiredService>(); - private IDocReceiverElementWriter DocReceiverElementWriter => _provider.GetRequiredService(); - private IMediator Mediator => _provider.GetRequiredService(); private readonly IMapper _mapper; @@ -207,11 +204,13 @@ public class CreateEnvelopeCommandHandler : IRequestHandler query + .Where(e => e.DocumentId == document.Id) + .Where(e => e.ReceiverId == link.ReceiverId), cancel); foreach (var field in receiverDraft.Fields.Where(f => f.Page > 0)) { - await DocReceiverElementWriter.InsertAsync(new DocReceiverElement + await DocReceiverElementRepository.CreateAsync(new DocReceiverElement { DocumentId = document.Id, ReceiverId = link.ReceiverId, diff --git a/EnvelopeGenerator.Infrastructure/DependencyInjection.cs b/EnvelopeGenerator.Infrastructure/DependencyInjection.cs index 97129709..8f2c3439 100644 --- a/EnvelopeGenerator.Infrastructure/DependencyInjection.cs +++ b/EnvelopeGenerator.Infrastructure/DependencyInjection.cs @@ -12,11 +12,9 @@ using DigitalData.UserManager.Domain.Entities; using CommandDotNet.Execution; using EnvelopeGenerator.Infrastructure.Repositories; using EnvelopeGenerator.Infrastructure.Executor; -using EnvelopeGenerator.Infrastructure.Writers; using EnvelopeGenerator.Application.Common.Interfaces.Repositories; using EnvelopeGenerator.Application.Common.Interfaces.SQLExecutor; using EnvelopeGenerator.Application.Common.Interfaces.Services; -using EnvelopeGenerator.Application.Common.Interfaces; using DigitalData.EmailProfilerDispatcher.Abstraction.Entities; #elif NETFRAMEWORK using System; @@ -58,7 +56,6 @@ namespace EnvelopeGenerator.Infrastructure services.TryAddScoped(); services.TryAddScoped(); services.TryAddScoped(); - services.TryAddScoped(); // User Preferences — distributed cache (dbo.TBDD_CACHE) services.TryAddScoped(); diff --git a/EnvelopeGenerator.Infrastructure/Writers/DocReceiverElementWriter.cs b/EnvelopeGenerator.Infrastructure/Writers/DocReceiverElementWriter.cs deleted file mode 100644 index c06ecf64..00000000 --- a/EnvelopeGenerator.Infrastructure/Writers/DocReceiverElementWriter.cs +++ /dev/null @@ -1,56 +0,0 @@ -#if NET -using EnvelopeGenerator.Application.Common.Interfaces; -using EnvelopeGenerator.Domain.Entities; -using Microsoft.EntityFrameworkCore; - -namespace EnvelopeGenerator.Infrastructure.Writers; - -public class DocReceiverElementWriter(EGDbContext dbContext) : IDocReceiverElementWriter -{ - public Task DeleteByDocumentAndReceiverAsync(int documentId, int receiverId, CancellationToken cancellationToken = default) - { - return dbContext.Database.ExecuteSqlInterpolatedAsync( - $"DELETE FROM [dbo].[TBSIG_DOCUMENT_RECEIVER_ELEMENT] WHERE [DOCUMENT_ID] = {documentId} AND [RECEIVER_ID] = {receiverId}", - cancellationToken); - } - - public Task InsertAsync(DocReceiverElement element, CancellationToken cancellationToken = default) - { - return dbContext.Database.ExecuteSqlInterpolatedAsync($@" -INSERT INTO [dbo].[TBSIG_DOCUMENT_RECEIVER_ELEMENT] - ([DOCUMENT_ID] - ,[RECEIVER_ID] - ,[ELEMENT_TYPE] - ,[POSITION_X] - ,[POSITION_Y] - ,[WIDTH] - ,[HEIGHT] - ,[REQUIRED] - ,[READ_ONLY] - ,[PAGE] - ,[ADDED_WHEN] - ,[FULL_NAME] - ,[POSITION] - ,[PLACE] - ,[TOOLTIP] - ,[ANNOTATION_INDEX]) - VALUES ({element.DocumentId} - ,{element.ReceiverId} - ,{element.ElementType} - ,{element.X} - ,{element.Y} - ,{element.Width} - ,{element.Height} - ,{element.Required} - ,{element.ReadOnly} - ,{element.Page} - ,{element.AddedWhen} - ,{element.FullName} - ,{element.Position} - ,{element.Place} - ,{element.Tooltip} - ,{element.AnnotationIndex})", - cancellationToken); - } -} -#endif diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server/appsettings.json b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/appsettings.json index 6c2c3269..8e1098f6 100644 --- a/EnvelopeGenerator.Server/EnvelopeGenerator.Server/appsettings.json +++ b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/appsettings.json @@ -187,6 +187,7 @@ "DbTriggerParams": { "Envelope": [ "TBSIG_ENVELOPE_HISTORY_AFT_INS" ], "History": [ "TBSIG_ENVELOPE_HISTORY_AFT_INS" ], + "DocReceiverElement": [ "TBSIG_DOCUMENT_RECEIVER_ELEMENT_AFT_INS", "TBSIG_DOCUMENT_RECEIVER_ELEMENT_AFT_UPD" ], "EmailOut": [ "TBEMLP_EMAIL_OUT_AFT_INS", "TBEMLP_EMAIL_OUT_AFT_UPD" ], "EnvelopeReceiverReadOnly": [ "TBSIG_ENVELOPE_RECEIVER_READ_ONLY_UPD" ], "Receiver": [], diff --git a/EnvelopeGenerator.Web/appsettings.Database.json b/EnvelopeGenerator.Web/appsettings.Database.json index fd5f777a..8c2c2ccb 100644 --- a/EnvelopeGenerator.Web/appsettings.Database.json +++ b/EnvelopeGenerator.Web/appsettings.Database.json @@ -7,9 +7,10 @@ "DbTriggerParams": { "Envelope": [ "TBSIG_ENVELOPE_AFT_INS" ], "History": [ "TBSIG_ENVELOPE_HISTORY_AFT_INS" ], + "DocReceiverElement": [ "TBSIG_DOCUMENT_RECEIVER_ELEMENT_AFT_INS", "TBSIG_DOCUMENT_RECEIVER_ELEMENT_AFT_UPD" ], "EmailOut": [ "TBEMLP_EMAIL_OUT_AFT_INS", "TBEMLP_EMAIL_OUT_AFT_UPD" ], "EnvelopeReceiverReadOnly": [ "TBSIG_ENVELOPE_RECEIVER_READ_ONLY_UPD" ], "Receiver": [], "EmailTemplate": [ "TBSIG_EMAIL_TEMPLATE_AFT_UPD" ] } -} \ No newline at end of file +} diff --git a/SENDER_SAVE_EDIT_MIGRATION_PLAN.md b/SENDER_SAVE_EDIT_MIGRATION_PLAN.md new file mode 100644 index 00000000..0832d666 --- /dev/null +++ b/SENDER_SAVE_EDIT_MIGRATION_PLAN.md @@ -0,0 +1,281 @@ +# Sender Save/Edit Migration Plan (Legacy Form -> Blazor Server) + +## Objective +Migrate the legacy `frmEnvelopeEditor` save/edit workflow into current architecture so that: + +1. Sender can **save incomplete envelope as draft** (`EnvelopeSaved = 1002`) from `EnvelopeSenderEditorPage.razor`. +2. Sender can **resume editing** saved/created envelopes from `EnvelopeSenderPage.razor` via **Bearbeiten** and continue where they left off. + +This plan introduces a distinct draft flow and a same-ID send-from-draft flow, aligned with legacy Form app semantics. + +--- + +## Scope + +### In scope +- Add separate **Save** and **Send** actions in sender editor. +- Persist draft data server-side with partial/incomplete payload support. +- Re-open existing draft from dashboard and restore editor state. +- Preserve existing delete/withdraw logic and status model. +- Add endpoints and application commands required for progressive sender workflow. + +### Out of scope (this ticket) +- Full redesign of editor UX. +- Multi-document support parity with `frmOrderFiles`. +- New DB schema changes (unless separately approved). + +--- + +## Current Gap Analysis + +## Current behavior (problem) +- `EnvelopeSenderEditorPage.razor` has only one save path (`SaveAsync`) that behaves like "send": + - requires title + PDF + receivers + fields. + - calls `EnvelopeReceiverService.CreateAsync(...)` -> `/api/EnvelopeReceiver` monolithic create/send pipeline. +- `EnvelopeSenderPage.razor` `EditEnvelope()` is TODO and does not navigate to editable persisted draft data. + +## Legacy behavior (target) +- `frmEnvelopeEditor` has both: + - **Save**: persists envelope without full validation/sending. + - **Send**: validates required business data then sends invitations. +- Dashboard "Bearbeiten" opens selected unsent envelope and restores work state. + +--- + +## Architectural Decisions + +1. **Split command model by intent** + - Draft persistence must not reuse monolithic `/api/EnvelopeReceiver` create+send endpoint. + - Introduce dedicated sender workflow commands/endpoints for draft lifecycle. + +2. **Progressive persistence** + - Persist envelope metadata, document, receivers, and fields incrementally. + - Draft save accepts partial data and updates only provided segments. + +3. **Status ownership** + - Draft operations set status to `EnvelopeSaved` (1002). + - Send operation transitions to queued/sent path (existing 1003+ behavior). + +4. **Editor route strategy** + - Keep `/sender/editor` for new envelope. + - Support edit mode via query: `/sender/editor?envelopeId={id}` (or route variant in follow-up if preferred). + +5. **No DB schema migration by default** + - Use existing tables (`TBSIG_ENVELOPE`, `TBSIG_DOCUMENT`, `TBSIG_ENVELOPE_RECEIVER`, `TBSIG_DOC_RECEIVER_ELEMENT`). + - If a field is missing for a requirement, handle in application logic first and escalate only if unavoidable. + +--- + +## Target Workflow + +## A) New envelope (draft first) +1. User opens `/sender/editor`. +2. User enters any subset of data. +3. User clicks **Save**. +4. System creates or updates draft envelope and sets status `EnvelopeSaved`. +5. User gets confirmation and remains in editor (or optional return to dashboard). + +## B) New envelope (direct send) +1. User fills required data. +2. User clicks **Send**. +3. System validates complete data and sends invitations. +4. Status transitions to queued/sent flow. + +## C) Edit existing draft/created +1. User selects row in `/sender` with `EnvelopeCreated` or `EnvelopeSaved`. +2. User clicks **Bearbeiten**. +3. App navigates to `/sender/editor?envelopeId={id}`. +4. Editor loads persisted data (metadata, PDF, receivers, fields). +5. User can save again (draft) or send. + +--- + +## Detailed Implementation Plan + +## Phase 1 - Contracts and Application Use Cases + +### 1.1 Add sender draft commands (Application) +- `CreateEnvelopeCommand` (create/update draft with partial data) +- `ReadEnvelopeDraftQuery` (load full editable projection by envelope id) +- `CreateEnvelopeCommand` (same request, `Send=true` for final send) + +Each command should enforce sender ownership (`UserId`) and return structured result DTOs. + +### 1.2 Validation policy split +- **Draft Save validation (minimal):** + - title optional at first save? choose one policy and keep consistent. + - no requirement for receivers/fields/document. +- **Send validation (strict):** + - title required + - document required + - at least one receiver + - required signature field mapping per receiver (business rule) + +### 1.3 History policy +- On draft save/update: optional lightweight history entry (`EnvelopeSaved`) if legacy parity requires it. +- On send: keep current history/send pipeline behavior. + +--- + +## Phase 2 - Server Endpoints + +Implement in `EnvelopeGenerator.Server` only. + +### 2.1 Draft endpoints +- `POST /api/Envelope` + - Create/update draft envelope (partial payload allowed), and send when `Send=true`. +- `GET /api/Envelope/{id}/draft` + - Read full editable aggregate for editor hydration. + +### 2.2 Authorization + errors +- 400 validation issues +- 403 sender mismatch +- 404 envelope not found +- 409 invalid state transition (optional, if needed) + +--- + +## Phase 3 - Sender Editor UI (`EnvelopeSenderEditorPage.razor`) + +### 3.1 Action buttons +- Keep current action as **Send** (rename from "Speichern" to "Senden"). +- Add separate **Save** button next to Send. + +### 3.2 Editor mode state +- Add `_editingEnvelopeId` from query param `envelopeId`. +- `null` => create mode, value => edit mode. + +### 3.3 Load existing draft +- On parameter set / first render in edit mode: + - call draft read endpoint. + - hydrate title, message, PDF bytes, receiver list, field list. + - map DB inches <-> PDF points correctly: + - DB inches -> UI points: `pt = inches * 72` + - UI points -> DB inches: `inches = pt / 72` + +### 3.4 Save behavior (new) +- Save button calls `SaveDraftAsync()`: + - no strict send validations. + - create draft if no id yet, otherwise update. + - force status `EnvelopeSaved`. + - keep user in editor and show non-blocking success feedback. + +### 3.5 Send behavior (existing path replacement) +- Send button calls `SendAsync()`: + - strict validation. + - if no draft id: create draft first then send, or direct send command. + - if edit mode: send current draft id. + +### 3.6 Session cache coexistence +- Keep `esid` memory cache for transient UX resilience. +- In edit mode, server data is source of truth; cache is fallback only. + +--- + +## Phase 4 - Dashboard Edit Resume (`EnvelopeSenderPage.razor`) + +### 4.1 Edit button enablement +- Enable **Bearbeiten** for statuses: + - `EnvelopeCreated` + - `EnvelopeSaved` +- Keep disabled for sent/queued/completed/deleted/withdrawn/rejected. + +### 4.2 Navigation +- Implement `EditEnvelope()`: + - `Navigation.NavigateTo($"/sender/editor?envelopeId={_selectedEnvelope.Id}")`. + +### 4.3 Optional UX +- Double-click on editable rows can open editor instead of preview (or keep current preview behavior per product decision). + +--- + +## Phase 5 - Data Mapping Rules + +1. **Document** + - Persist original PDF bytes (no visual placeholder burn-in). +2. **Receivers** + - Draft can contain receivers with/without fields. +3. **Fields** + - Store in DB inches with fixed signature size semantics. +4. **Ownership** + - Sender can read/update/send only own envelopes. + +--- + +## Suggested DTO Shape (Draft) + +Use one aggregate DTO for editor hydration/save/update: + +- `EnvelopeDraftDto` + - `EnvelopeId` + - `Status` + - `Title` + - `Message` + - `DocumentBase64` (or document id + fetch endpoint) + - `Receivers[]` + - `ReceiverId?` + - `Name` + - `Email` + - `Phone` + - `Fields[] { XInches, YInches, Page }` + +--- + +## Migration Strategy + +## Step A (safe vertical slice) +- Add read/edit route query handling and dashboard navigation first. +- Hydrate editor from existing envelope data where possible. + +## Step B (draft save) +- Implement draft create/update endpoints + app commands. +- Wire Save button. + +## Step C (send from draft) +- Move Send to explicit command/endpoint. +- Ensure existing email/history behavior remains intact. + +## Step D (hardening) +- Add tests + manual QA checklist. + +--- + +## Testing Plan + +## Automated (Application) +- Save draft with partial data -> status `EnvelopeSaved`. +- Edit draft updates existing records, no duplicate orphan data. +- Send draft with missing required data -> validation error. +- Send draft complete -> queued/sent flow success. +- Ownership checks -> 403 behavior. + +## Manual (UI) +1. New editor -> save with only title. +2. Reopen from dashboard -> state restored. +3. Add PDF only -> save -> reopen -> PDF still present. +4. Add receivers/fields incrementally -> save repeatedly. +5. Final send -> status transitions and receiver notifications. + +--- + +## Risks and Mitigations + +1. **Monolithic endpoint coupling risk** + - Mitigation: draft/send dedicated endpoints. +2. **Data duplication on repeated save** + - Mitigation: idempotent upsert logic for receivers/fields. +3. **Coordinate regression** + - Mitigation: centralize inches/points conversion helpers. +4. **Trigger side effects/history duplication** + - Mitigation: verify DB-trigger interactions per status events. + +--- + +## Definition of Done + +- Editor has two distinct actions: **Save** and **Send**. +- Save allows incomplete payload and persists with `EnvelopeSaved` status. +- Dashboard **Bearbeiten** opens editor for `EnvelopeCreated`/`EnvelopeSaved` and restores state. +- Sender can continue from last saved point and eventually send. +- Server endpoints support save/edit/send draft workflow with same envelope id. +- No DB schema changes introduced without explicit approval.