[CLAUDE] PurchaseEvaluation: S22+4 Chunk A — BE attachment view endpoint + AdjustBudget command

Feature 1 (attachment preview):
- NEW `GET /api/purchase-evaluations/{id}/attachments/{attId}/view`
- Cùng handler download, override `Content-Disposition: inline` để FE nhúng iframe
- Permission: same scope GET phiếu (Plan E V2 strict scope)

Feature 2 (điều chỉnh ngân sách):
- NEW `AdjustPurchaseEvaluationBudgetCommand` + Handler + Validator
- NEW `PATCH /api/purchase-evaluations/{id}/budget-adjust` body
  `{budgetId, budgetManualName, budgetManualAmount}`
- Phase + actor scope guard:
  * DangSoanThao/TraLai → chỉ Drafter của phiếu
  * ChoDuyet → Approver currentLevel (match ApproverUserId) — V2 only
  * Admin → bypass tất cả
- Audit changelog với diff narrative: "Điều chỉnh ngân sách: link X→Y, số tiền A→Bđ [Drafter/Approver Bước/Cấp/Admin]"
- Tách riêng KHÔNG dùng UpdatePeDraft vì Approver scope KHÔNG nên được edit
  Section 1 fields (TenGoiThau/DiaDiem/MoTa/PaymentTerms)

Verify:
- dotnet build SolutionErp.slnx — 0 err, 2 warn DocxRenderer pre-existing
- Test defer carry Plan C (UAT mode §7) — guard logic critical, ưu tiên cho S23+

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
pqhuy1987
2026-05-13 22:25:49 +07:00
parent 0e707891ff
commit 37b51d7f07
2 changed files with 163 additions and 0 deletions

View File

@ -52,6 +52,18 @@ public class PurchaseEvaluationsController(IMediator mediator) : ControllerBase
return NoContent();
}
// S22+4 — Feature 2: Section "Điều chỉnh ngân sách" tách endpoint riêng.
// Cho phép Drafter (DangSoanThao/TraLai) OR Approver currentLevel (ChoDuyet)
// OR Admin adjust Budget* fields. Handler kiểm phase + actor scope.
[HttpPatch("{id:guid}/budget-adjust")]
public async Task<IActionResult> AdjustBudget(Guid id, [FromBody] AdjustBudgetBody body, CancellationToken ct)
{
await mediator.Send(new AdjustPurchaseEvaluationBudgetCommand(
id, body.BudgetId, body.BudgetManualName, body.BudgetManualAmount), ct);
return NoContent();
}
public record AdjustBudgetBody(Guid? BudgetId, string? BudgetManualName, decimal? BudgetManualAmount);
[HttpPost("{id:guid}/transitions")]
public async Task<IActionResult> Transition(Guid id, [FromBody] TransitionPeBody body, CancellationToken ct)
{
@ -182,6 +194,17 @@ public class PurchaseEvaluationsController(IMediator mediator) : ControllerBase
return File(f.Content, f.ContentType, f.FileName);
}
// S22+4 — Inline view endpoint cho FE preview modal (PDF iframe, image <img>).
// Cùng handler download, khác Content-Disposition (inline vs attachment).
// Permission: same actor scope như GET phiếu (Plan E V2 strict).
[HttpGet("{id:guid}/attachments/{attId:guid}/view")]
public async Task<IActionResult> ViewAttachment(Guid id, Guid attId, CancellationToken ct)
{
var f = await mediator.Send(new DownloadPurchaseEvaluationAttachmentQuery(id, attId), ct);
Response.Headers["Content-Disposition"] = $"inline; filename=\"{f.FileName}\"";
return File(f.Content, f.ContentType);
}
[HttpDelete("{id:guid}/attachments/{attId:guid}")]
public async Task<IActionResult> DeleteAttachment(Guid id, Guid attId, CancellationToken ct)
{