1079 lines
26 KiB
Markdown
1079 lines
26 KiB
Markdown
# Purchase Requisition Approval & Immutability - System Administrator Guide
|
|
|
|
## Document Information
|
|
- **Document Type**: Technical Implementation Guide
|
|
- **Audience**: System Administrators, DevOps, Technical Support
|
|
- **Last Updated**: 2025-10-30
|
|
- **Version**: 1.0
|
|
|
|
---
|
|
|
|
## Table of Contents
|
|
|
|
1. [Architecture Overview](#architecture-overview)
|
|
2. [Implementation Details](#implementation-details)
|
|
3. [Database Schema](#database-schema)
|
|
4. [API Endpoints](#api-endpoints)
|
|
5. [Configuration](#configuration)
|
|
6. [Security Considerations](#security-considerations)
|
|
7. [Monitoring and Logging](#monitoring-and-logging)
|
|
8. [Troubleshooting](#troubleshooting)
|
|
9. [Maintenance Procedures](#maintenance-procedures)
|
|
10. [Testing and Validation](#testing-and-validation)
|
|
|
|
---
|
|
|
|
## Architecture Overview
|
|
|
|
### System Components
|
|
|
|
```
|
|
┌─────────────┐ ┌─────────────┐ ┌──────────────┐
|
|
│ Angular │──────│ ASP.NET │──────│ PostgreSQL │
|
|
│ Frontend │ HTTP │ Core API │ EF │ Database │
|
|
└─────────────┘ └─────────────┘ └──────────────┘
|
|
(UI) (Business Logic) (Data Store)
|
|
```
|
|
|
|
### Immutability Implementation Layers
|
|
|
|
1. **Frontend Layer** (`purchase-requisition-form.component.ts`)
|
|
- UI/UX protection
|
|
- Visual indicators
|
|
- Client-side validation
|
|
- User experience
|
|
|
|
2. **API Layer** (`PurchaseRequisitionsController.cs`)
|
|
- HTTP endpoint protection
|
|
- Request validation
|
|
- Error handling
|
|
- Audit logging trigger
|
|
|
|
3. **Service Layer** (`PurchaseRequisitionService.cs`)
|
|
- Business logic
|
|
- Exception propagation
|
|
|
|
4. **Repository Layer** (`PurchaseRequisitionRepository.cs`)
|
|
- Data access
|
|
- Status validation
|
|
- Exception throwing
|
|
|
|
5. **Database Layer** (PostgreSQL)
|
|
- Data persistence
|
|
- Audit log storage
|
|
- Transaction management
|
|
|
|
### Status Flow
|
|
|
|
```
|
|
Draft (Editable)
|
|
↓ Submit
|
|
PendingApproval (Locked)
|
|
↓ Approve ↓ Reject
|
|
Approved (Locked) → Back to Draft (Editable)
|
|
↓ Convert
|
|
ConvertedToRfq/PO (Locked)
|
|
↓ Complete
|
|
Closed (Locked)
|
|
```
|
|
|
|
---
|
|
|
|
## Implementation Details
|
|
|
|
### Backend Implementation
|
|
|
|
#### Custom Exception Class
|
|
|
|
**File**: `/piam-api/src/PiamMasterData.Domain/Exceptions/ImmutableDocumentException.cs`
|
|
|
|
```csharp
|
|
public class ImmutableDocumentException : Exception
|
|
{
|
|
public string DocumentType { get; }
|
|
public string DocumentId { get; }
|
|
public string? CurrentStatus { get; }
|
|
public string? AllowedStatus { get; }
|
|
|
|
public ImmutableDocumentException(
|
|
string documentType,
|
|
string documentId,
|
|
string? currentStatus = null,
|
|
string? allowedStatus = null)
|
|
: base($"Cannot modify {documentType} in '{currentStatus}' status. " +
|
|
$"Only '{allowedStatus}' status allows modifications.")
|
|
{
|
|
DocumentType = documentType;
|
|
DocumentId = documentId;
|
|
CurrentStatus = currentStatus;
|
|
AllowedStatus = allowedStatus;
|
|
}
|
|
}
|
|
```
|
|
|
|
#### Repository Validation
|
|
|
|
**File**: `/piam-api/src/PiamMasterData.Infrastructure/Repositories/PurchaseRequisitionRepository.cs`
|
|
|
|
**Lines**: 304-311
|
|
|
|
```csharp
|
|
if (requisition.Status != PurchaseRequisitionStatus.Draft)
|
|
{
|
|
throw new ImmutableDocumentException(
|
|
documentType: "Purchase Requisition",
|
|
documentId: id.ToString(),
|
|
currentStatus: requisition.Status.ToString(),
|
|
allowedStatus: nameof(PurchaseRequisitionStatus.Draft)
|
|
);
|
|
}
|
|
```
|
|
|
|
#### Controller Error Handling
|
|
|
|
**File**: `/piam-api/src/PiamMasterData.Api/Controllers/PurchaseRequisitionsController.cs`
|
|
|
|
**Lines**: 120-143
|
|
|
|
```csharp
|
|
catch (ImmutableDocumentException ex)
|
|
{
|
|
// Log the modification attempt for audit purposes
|
|
await LogModificationAttemptAsync(
|
|
id,
|
|
"UPDATE_ATTEMPT",
|
|
ex.CurrentStatus ?? "UNKNOWN",
|
|
false,
|
|
dto,
|
|
ex.Message,
|
|
cancellationToken);
|
|
|
|
return StatusCode(403, new
|
|
{
|
|
error = ex.Message,
|
|
errorCode = "PR_IMMUTABLE_STATUS",
|
|
details = new
|
|
{
|
|
documentType = ex.DocumentType,
|
|
documentId = ex.DocumentId,
|
|
currentStatus = ex.CurrentStatus,
|
|
allowedStatus = ex.AllowedStatus
|
|
}
|
|
});
|
|
}
|
|
```
|
|
|
|
### Frontend Implementation
|
|
|
|
#### Component Logic
|
|
|
|
**File**: `/piam-web/src/app/features/procurement/components/purchase-requisition-form/purchase-requisition-form.component.ts`
|
|
|
|
**Lines**: 186-203
|
|
|
|
```typescript
|
|
// Check if document is immutable (cannot be edited due to status)
|
|
get isImmutable(): boolean {
|
|
const immutableStatuses: PurchaseRequisitionStatus[] = [
|
|
'Approved',
|
|
'PendingApproval',
|
|
'ConvertedToRfq',
|
|
'ConvertedToPurchaseOrder',
|
|
'Closed',
|
|
];
|
|
return this.detail ? immutableStatuses.includes(this.detail.status) : false;
|
|
}
|
|
|
|
// Determine if user can edit (save/submit buttons should show)
|
|
get canEdit(): boolean {
|
|
if (!this.detail) return false;
|
|
return this.detail.status === 'Draft' && !this.isReadOnly;
|
|
}
|
|
```
|
|
|
|
#### Template Visual Indicators
|
|
|
|
**File**: `/piam-web/src/app/features/procurement/components/purchase-requisition-form/purchase-requisition-form.component.html`
|
|
|
|
**Key Elements**:
|
|
1. Lock icon in header (lines 12-19)
|
|
2. Enhanced status badge (lines 22-31)
|
|
3. Informational banner (lines 62-75)
|
|
4. Conditional button visibility (lines 40-55)
|
|
5. View Only badge (lines 33-36)
|
|
|
|
---
|
|
|
|
## Database Schema
|
|
|
|
### Purchase Requisition Table
|
|
|
|
**Table**: `purchase_requisitions`
|
|
|
|
**Key Columns**:
|
|
```sql
|
|
id UUID PRIMARY KEY
|
|
requisition_number VARCHAR(50) UNIQUE NOT NULL
|
|
status VARCHAR(50) NOT NULL
|
|
approved_by VARCHAR(255)
|
|
approved_at_utc TIMESTAMP
|
|
created_at_utc TIMESTAMP NOT NULL
|
|
updated_at_utc TIMESTAMP NOT NULL
|
|
```
|
|
|
|
**Status Values**:
|
|
- `Draft` - Editable
|
|
- `PendingApproval` - Locked
|
|
- `Approved` - Locked
|
|
- `Rejected` - Editable
|
|
- `ConvertedToRfq` - Locked
|
|
- `ConvertedToPurchaseOrder` - Locked
|
|
- `Closed` - Locked
|
|
|
|
### Audit Log Table
|
|
|
|
**Table**: `purchase_requisition_audit_logs`
|
|
|
|
**Schema**:
|
|
```sql
|
|
CREATE TABLE purchase_requisition_audit_logs (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
purchase_requisition_id UUID NOT NULL,
|
|
action VARCHAR(50) NOT NULL,
|
|
user_id VARCHAR(255),
|
|
user_name VARCHAR(255) NOT NULL,
|
|
attempted_changes TEXT,
|
|
status_at_attempt VARCHAR(50) NOT NULL,
|
|
was_successful BOOLEAN NOT NULL,
|
|
failure_reason TEXT,
|
|
ip_address VARCHAR(45),
|
|
user_agent TEXT,
|
|
created_at_utc TIMESTAMP NOT NULL DEFAULT now(),
|
|
|
|
FOREIGN KEY (purchase_requisition_id)
|
|
REFERENCES purchase_requisitions(id) ON DELETE CASCADE
|
|
);
|
|
|
|
CREATE INDEX ix_pr_audit_logs_pr_id
|
|
ON purchase_requisition_audit_logs(purchase_requisition_id);
|
|
|
|
CREATE INDEX ix_pr_audit_logs_created_at
|
|
ON purchase_requisition_audit_logs(created_at_utc DESC);
|
|
|
|
CREATE INDEX ix_pr_audit_logs_pr_id_created_at
|
|
ON purchase_requisition_audit_logs(purchase_requisition_id, created_at_utc DESC);
|
|
```
|
|
|
|
**Action Types**:
|
|
- `UPDATE_ATTEMPT` - Attempted to modify locked PR
|
|
- `STATUS_CHANGE` - Status transition
|
|
- `APPROVAL` - PR approved
|
|
- `REJECTION` - PR rejected
|
|
- `CONVERSION` - Converted to RFQ/PO
|
|
|
|
### Migration
|
|
|
|
**File**: `/piam-api/src/PiamMasterData.Infrastructure/Migrations/20251029204955_AddPurchaseRequisitionAuditLog.cs`
|
|
|
|
**Commands**:
|
|
```bash
|
|
# Create migration
|
|
cd /piam-api/src/PiamMasterData.Infrastructure
|
|
dotnet ef migrations add AddPurchaseRequisitionAuditLog
|
|
|
|
# Apply migration
|
|
dotnet ef database update
|
|
|
|
# Verify
|
|
psql -h localhost -U [user] -d [database] -c "\d purchase_requisition_audit_logs"
|
|
```
|
|
|
|
---
|
|
|
|
## API Endpoints
|
|
|
|
### Update Purchase Requisition
|
|
|
|
**Endpoint**: `PUT /api/purchase-requisitions/{id}`
|
|
|
|
**Request**:
|
|
```http
|
|
PUT /api/purchase-requisitions/a1b2c3d4-5678-90ab-cdef-123456789012
|
|
Content-Type: application/json
|
|
|
|
{
|
|
"departmentCode": "IT",
|
|
"budgetCode": "OPEX-2025-IT-001",
|
|
"purpose": "Updated purpose",
|
|
"lines": [...]
|
|
}
|
|
```
|
|
|
|
**Success Response** (200 OK):
|
|
```json
|
|
{
|
|
"id": "a1b2c3d4-5678-90ab-cdef-123456789012",
|
|
"requisitionNumber": "PR-2025-001234",
|
|
"status": "Draft",
|
|
...
|
|
}
|
|
```
|
|
|
|
**Error Response** (403 Forbidden):
|
|
```json
|
|
{
|
|
"error": "Cannot modify Purchase Requisition in 'Approved' status. Only 'Draft' status allows modifications.",
|
|
"errorCode": "PR_IMMUTABLE_STATUS",
|
|
"details": {
|
|
"documentType": "Purchase Requisition",
|
|
"documentId": "a1b2c3d4-5678-90ab-cdef-123456789012",
|
|
"currentStatus": "Approved",
|
|
"allowedStatus": "Draft"
|
|
}
|
|
}
|
|
```
|
|
|
|
### Get Audit Logs
|
|
|
|
**Endpoint**: `GET /api/purchase-requisitions/{id}/audit-logs`
|
|
|
|
**Request**:
|
|
```http
|
|
GET /api/purchase-requisitions/a1b2c3d4-5678-90ab-cdef-123456789012/audit-logs?pageNumber=1&pageSize=25
|
|
```
|
|
|
|
**Response** (200 OK):
|
|
```json
|
|
{
|
|
"items": [
|
|
{
|
|
"id": "log-uuid-1",
|
|
"purchaseRequisitionId": "a1b2c3d4-5678-90ab-cdef-123456789012",
|
|
"action": "UPDATE_ATTEMPT",
|
|
"userId": "user-123",
|
|
"userName": "john.doe",
|
|
"attemptedChanges": "{\"purpose\":\"New purpose\"}",
|
|
"statusAtAttempt": "Approved",
|
|
"wasSuccessful": false,
|
|
"failureReason": "Cannot modify PR in Approved status",
|
|
"ipAddress": "192.168.1.100",
|
|
"userAgent": "Mozilla/5.0...",
|
|
"createdAtUtc": "2025-10-30T14:23:45Z"
|
|
}
|
|
],
|
|
"totalCount": 1,
|
|
"pageNumber": 1,
|
|
"pageSize": 25
|
|
}
|
|
```
|
|
|
|
### Approve Purchase Requisition
|
|
|
|
**Endpoint**: `POST /api/purchase-requisitions/{id}/approve`
|
|
|
|
**Important**: ⚠️ This endpoint currently has NO authorization checks
|
|
|
|
**Request**:
|
|
```http
|
|
POST /api/purchase-requisitions/a1b2c3d4-5678-90ab-cdef-123456789012/approve
|
|
Content-Type: application/json
|
|
|
|
{
|
|
"remarks": "Approved for procurement"
|
|
}
|
|
```
|
|
|
|
**Response** (200 OK):
|
|
```json
|
|
{
|
|
"id": "a1b2c3d4-5678-90ab-cdef-123456789012",
|
|
"status": "Approved",
|
|
"approvedBy": "approver.name",
|
|
"approvedAtUtc": "2025-10-30T15:00:00Z",
|
|
...
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Configuration
|
|
|
|
### Dependency Injection
|
|
|
|
**File**: `/piam-api/src/PiamMasterData.Infrastructure/DependencyInjection.cs`
|
|
|
|
**Line**: 90
|
|
|
|
```csharp
|
|
services.AddScoped<IPurchaseRequisitionAuditService, PurchaseRequisitionAuditService>();
|
|
```
|
|
|
|
### Service Registration
|
|
|
|
All required services are registered in `DependencyInjection.cs`:
|
|
|
|
```csharp
|
|
services.AddHttpContextAccessor(); // Line 21 - Required for audit logging
|
|
services.AddScoped<IPurchaseRequisitionRepository, PurchaseRequisitionRepository>();
|
|
services.AddScoped<IPurchaseRequisitionAuditService, PurchaseRequisitionAuditService>();
|
|
```
|
|
|
|
### Connection String
|
|
|
|
**File**: `appsettings.json`
|
|
|
|
```json
|
|
{
|
|
"ConnectionStrings": {
|
|
"DefaultConnection": "Host=localhost;Database=PiamMasterData;Username=user;Password=pass"
|
|
}
|
|
}
|
|
```
|
|
|
|
### Frontend Environment
|
|
|
|
**File**: `/piam-web/src/environments/environment.ts`
|
|
|
|
```typescript
|
|
export const environment = {
|
|
production: false,
|
|
apiUrl: 'http://localhost:5200/api'
|
|
};
|
|
```
|
|
|
|
---
|
|
|
|
## Security Considerations
|
|
|
|
### Current Implementation
|
|
|
|
#### ✅ Implemented Security Features
|
|
|
|
1. **Status-Based Access Control**
|
|
- Repository enforces status checks
|
|
- Cannot bypass via API calls
|
|
- Consistent across all endpoints
|
|
|
|
2. **Audit Logging**
|
|
- All modification attempts logged
|
|
- User context captured
|
|
- IP address and user agent recorded
|
|
|
|
3. **Exception Handling**
|
|
- Proper HTTP status codes
|
|
- Structured error responses
|
|
- No sensitive data leakage
|
|
|
|
4. **Frontend Protection**
|
|
- UI prevents unauthorized actions
|
|
- Visual indicators for locked documents
|
|
- Form validation
|
|
|
|
#### ❌ Missing Security Features
|
|
|
|
1. **NO Authorization Attributes**
|
|
- No `[Authorize]` on controller actions
|
|
- Anyone with API access can approve
|
|
- No role-based access control
|
|
|
|
**Recommended Fix**:
|
|
```csharp
|
|
/// <summary>
|
|
/// Approves a purchase requisition
|
|
/// </summary>
|
|
[Authorize(Policy = "Procurement.PurchaseRequisitions.Approve")]
|
|
[HttpPost("{id:guid}/approve")]
|
|
public async Task<ActionResult<PurchaseRequisitionDetailDto>> ApproveRequisition(...)
|
|
```
|
|
|
|
2. **No Permission Checks**
|
|
- Backend doesn't verify user permissions
|
|
- No distinction between creator/approver
|
|
|
|
**Recommended Implementation**:
|
|
```csharp
|
|
// Check if user has permission to approve
|
|
if (!User.HasPermission("Procurement.PurchaseRequisitions.Approve"))
|
|
{
|
|
return Forbidden();
|
|
}
|
|
```
|
|
|
|
3. **No CSRF Protection**
|
|
- Consider adding anti-forgery tokens
|
|
- Especially for state-changing operations
|
|
|
|
### Threat Model
|
|
|
|
| Threat | Risk Level | Mitigation |
|
|
|--------|------------|------------|
|
|
| Direct API bypass | High | ✅ Enforced at repository level |
|
|
| Unauthorized approval | High | ❌ Add authorization attributes |
|
|
| Audit log tampering | Medium | ✅ Database permissions, indexes |
|
|
| Session hijacking | Medium | ⏳ Implement proper authentication |
|
|
| CSRF attacks | Medium | ⏳ Add anti-forgery tokens |
|
|
| SQL injection | Low | ✅ Using EF Core with parameters |
|
|
|
|
### Recommended Security Enhancements
|
|
|
|
1. **Implement Authorization**
|
|
```bash
|
|
# Add authorization attributes to all endpoints
|
|
# Define policies in Startup.cs
|
|
# Implement permission checks
|
|
```
|
|
|
|
2. **Add API Rate Limiting**
|
|
```csharp
|
|
services.AddRateLimiting(options => { ... });
|
|
```
|
|
|
|
3. **Enable CORS Properly**
|
|
```csharp
|
|
services.AddCors(options =>
|
|
{
|
|
options.AddPolicy("PiamPolicy", builder =>
|
|
{
|
|
builder.WithOrigins("https://yourdomain.com")
|
|
.AllowAnyMethod()
|
|
.AllowAnyHeader();
|
|
});
|
|
});
|
|
```
|
|
|
|
4. **Add Request Validation**
|
|
```csharp
|
|
[ValidateAntiForgeryToken]
|
|
[ValidateModel]
|
|
```
|
|
|
|
---
|
|
|
|
## Monitoring and Logging
|
|
|
|
### Application Logs
|
|
|
|
**Configuration**: `appsettings.json`
|
|
|
|
```json
|
|
{
|
|
"Logging": {
|
|
"LogLevel": {
|
|
"Default": "Information",
|
|
"Microsoft.EntityFrameworkCore": "Warning",
|
|
"PiamMasterData.Infrastructure.Services": "Information"
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
### Audit Log Service
|
|
|
|
**File**: `/piam-api/src/PiamMasterData.Infrastructure/Services/PurchaseRequisitionAuditService.cs`
|
|
|
|
**Key Method**: `LogModificationAttemptAsync`
|
|
|
|
**Logs**:
|
|
- User name and ID
|
|
- Action attempted
|
|
- Status at time of attempt
|
|
- IP address and user agent
|
|
- Attempted changes (JSON)
|
|
- Success/failure status
|
|
- Failure reason
|
|
|
|
### Monitoring Queries
|
|
|
|
#### Recent Modification Attempts
|
|
```sql
|
|
SELECT
|
|
pr.requisition_number,
|
|
pal.action,
|
|
pal.user_name,
|
|
pal.status_at_attempt,
|
|
pal.was_successful,
|
|
pal.failure_reason,
|
|
pal.created_at_utc
|
|
FROM purchase_requisition_audit_logs pal
|
|
JOIN purchase_requisitions pr ON pal.purchase_requisition_id = pr.id
|
|
WHERE pal.was_successful = false
|
|
AND pal.created_at_utc > NOW() - INTERVAL '7 days'
|
|
ORDER BY pal.created_at_utc DESC;
|
|
```
|
|
|
|
#### Failed Modification Attempts by User
|
|
```sql
|
|
SELECT
|
|
user_name,
|
|
COUNT(*) as attempt_count,
|
|
MAX(created_at_utc) as last_attempt
|
|
FROM purchase_requisition_audit_logs
|
|
WHERE was_successful = false
|
|
AND action = 'UPDATE_ATTEMPT'
|
|
AND created_at_utc > NOW() - INTERVAL '30 days'
|
|
GROUP BY user_name
|
|
ORDER BY attempt_count DESC;
|
|
```
|
|
|
|
#### Approval Activity
|
|
```sql
|
|
SELECT
|
|
pr.requisition_number,
|
|
pr.status,
|
|
pr.approved_by,
|
|
pr.approved_at_utc,
|
|
pr.created_at_utc,
|
|
EXTRACT(EPOCH FROM (pr.approved_at_utc - pr.created_at_utc))/3600 as hours_to_approval
|
|
FROM purchase_requisitions pr
|
|
WHERE pr.approved_at_utc IS NOT NULL
|
|
AND pr.approved_at_utc > NOW() - INTERVAL '30 days'
|
|
ORDER BY pr.approved_at_utc DESC;
|
|
```
|
|
|
|
### Performance Monitoring
|
|
|
|
**Key Metrics**:
|
|
1. Average time to approval
|
|
2. Number of modification attempts
|
|
3. Failed approval attempts
|
|
4. API response times
|
|
5. Database query performance
|
|
|
|
**Tools**:
|
|
- Application Insights (if using Azure)
|
|
- ELK Stack (Elasticsearch, Logstash, Kibana)
|
|
- Prometheus + Grafana
|
|
- PostgreSQL pg_stat_statements
|
|
|
|
---
|
|
|
|
## Troubleshooting
|
|
|
|
### Common Issues
|
|
|
|
#### Issue: Users Report "Cannot Edit" But PR is in Draft
|
|
|
|
**Diagnosis**:
|
|
```sql
|
|
-- Check PR status
|
|
SELECT id, requisition_number, status, updated_at_utc
|
|
FROM purchase_requisitions
|
|
WHERE requisition_number = 'PR-2025-001234';
|
|
|
|
-- Check for status anomalies
|
|
SELECT status, COUNT(*)
|
|
FROM purchase_requisitions
|
|
GROUP BY status;
|
|
```
|
|
|
|
**Possible Causes**:
|
|
1. Browser cache showing old status
|
|
2. Frontend/backend version mismatch
|
|
3. Database update didn't complete
|
|
|
|
**Solutions**:
|
|
1. Clear browser cache (Ctrl+Shift+Delete)
|
|
2. Verify backend and frontend versions
|
|
3. Check database directly
|
|
4. Review application logs
|
|
|
|
#### Issue: Audit Logs Not Being Created
|
|
|
|
**Diagnosis**:
|
|
```bash
|
|
# Check if table exists
|
|
psql -h localhost -U user -d database -c "\d purchase_requisition_audit_logs"
|
|
|
|
# Check for recent logs
|
|
psql -h localhost -U user -d database -c "SELECT COUNT(*) FROM purchase_requisition_audit_logs;"
|
|
|
|
# Check service registration
|
|
grep -r "PurchaseRequisitionAuditService" src/
|
|
```
|
|
|
|
**Possible Causes**:
|
|
1. Migration not applied
|
|
2. Service not registered in DI
|
|
3. Exception in audit logging (silently caught)
|
|
4. Database permissions issue
|
|
|
|
**Solutions**:
|
|
1. Run migrations: `dotnet ef database update`
|
|
2. Verify DI registration in `DependencyInjection.cs:90`
|
|
3. Check application logs for exceptions
|
|
4. Verify database user has INSERT permissions
|
|
|
|
#### Issue: 403 Errors on Draft PRs
|
|
|
|
**Diagnosis**:
|
|
```sql
|
|
-- Verify PR status
|
|
SELECT id, requisition_number, status
|
|
FROM purchase_requisitions
|
|
WHERE id = 'a1b2c3d4-5678-90ab-cdef-123456789012';
|
|
```
|
|
|
|
**Possible Causes**:
|
|
1. Status field corrupted
|
|
2. Status enum mismatch
|
|
3. Code deployment issue
|
|
|
|
**Solutions**:
|
|
1. Verify database value matches enum
|
|
2. Check for recent code deployments
|
|
3. Review repository code
|
|
4. Check for custom status values
|
|
|
|
#### Issue: Approved PRs Being Modified
|
|
|
|
**Critical Security Issue**
|
|
|
|
**Immediate Actions**:
|
|
1. Review audit logs immediately
|
|
2. Identify affected PRs
|
|
3. Notify security team
|
|
4. Check for unauthorized access
|
|
|
|
**Investigation**:
|
|
```sql
|
|
-- Find modified approved PRs
|
|
SELECT
|
|
pr.requisition_number,
|
|
pr.status,
|
|
pr.updated_at_utc,
|
|
pr.approved_at_utc
|
|
FROM purchase_requisitions pr
|
|
WHERE pr.status != 'Draft'
|
|
AND pr.updated_at_utc > pr.approved_at_utc;
|
|
|
|
-- Check audit logs
|
|
SELECT *
|
|
FROM purchase_requisition_audit_logs
|
|
WHERE action = 'UPDATE_ATTEMPT'
|
|
AND was_successful = true;
|
|
```
|
|
|
|
**Root Cause Analysis**:
|
|
- Repository validation bypass?
|
|
- Direct database modification?
|
|
- Code deployment introduced bug?
|
|
- Migration script error?
|
|
|
|
---
|
|
|
|
## Maintenance Procedures
|
|
|
|
### Database Maintenance
|
|
|
|
#### Audit Log Retention
|
|
|
|
**Policy**: Retain 7 years per audit requirements
|
|
|
|
**Archive Old Logs**:
|
|
```sql
|
|
-- Create archive table (one-time)
|
|
CREATE TABLE purchase_requisition_audit_logs_archive (
|
|
LIKE purchase_requisition_audit_logs INCLUDING ALL
|
|
);
|
|
|
|
-- Archive logs older than 5 years
|
|
INSERT INTO purchase_requisition_audit_logs_archive
|
|
SELECT * FROM purchase_requisition_audit_logs
|
|
WHERE created_at_utc < NOW() - INTERVAL '5 years';
|
|
|
|
-- Optional: Delete archived records from main table
|
|
-- DELETE FROM purchase_requisition_audit_logs
|
|
-- WHERE created_at_utc < NOW() - INTERVAL '5 years';
|
|
```
|
|
|
|
#### Index Maintenance
|
|
|
|
```sql
|
|
-- Reindex for performance
|
|
REINDEX TABLE purchase_requisition_audit_logs;
|
|
REINDEX TABLE purchase_requisitions;
|
|
|
|
-- Vacuum to reclaim space
|
|
VACUUM ANALYZE purchase_requisition_audit_logs;
|
|
VACUUM ANALYZE purchase_requisitions;
|
|
|
|
-- Check index usage
|
|
SELECT
|
|
schemaname,
|
|
tablename,
|
|
indexname,
|
|
idx_scan,
|
|
idx_tup_read,
|
|
idx_tup_fetch
|
|
FROM pg_stat_user_indexes
|
|
WHERE tablename IN ('purchase_requisition_audit_logs', 'purchase_requisitions')
|
|
ORDER BY idx_scan;
|
|
```
|
|
|
|
### Backup Procedures
|
|
|
|
#### Database Backup
|
|
|
|
```bash
|
|
# Full backup
|
|
pg_dump -h localhost -U user -d database -F c -f piam_backup_$(date +%Y%m%d).backup
|
|
|
|
# Backup specific tables
|
|
pg_dump -h localhost -U user -d database -t purchase_requisitions -t purchase_requisition_audit_logs -F c -f pr_tables_$(date +%Y%m%d).backup
|
|
|
|
# Restore
|
|
pg_restore -h localhost -U user -d database -c piam_backup_20251030.backup
|
|
```
|
|
|
|
#### Schedule Backups
|
|
|
|
```bash
|
|
# Add to crontab
|
|
# Daily backup at 2 AM
|
|
0 2 * * * /usr/local/bin/backup-piam-db.sh
|
|
|
|
# Weekly full backup on Sunday at 1 AM
|
|
0 1 * * 0 /usr/local/bin/backup-piam-db-full.sh
|
|
```
|
|
|
|
### Application Updates
|
|
|
|
#### Deployment Checklist
|
|
|
|
1. ✅ **Before Deployment**
|
|
- [ ] Review all code changes
|
|
- [ ] Run unit tests
|
|
- [ ] Run integration tests
|
|
- [ ] Backup database
|
|
- [ ] Notify users of maintenance window
|
|
|
|
2. ✅ **During Deployment**
|
|
- [ ] Stop application
|
|
- [ ] Apply database migrations
|
|
- [ ] Deploy new code
|
|
- [ ] Update configuration if needed
|
|
- [ ] Start application
|
|
|
|
3. ✅ **After Deployment**
|
|
- [ ] Verify application starts
|
|
- [ ] Test critical paths
|
|
- [ ] Check logs for errors
|
|
- [ ] Verify database connections
|
|
- [ ] Monitor for issues
|
|
|
|
#### Migration Commands
|
|
|
|
```bash
|
|
# List migrations
|
|
dotnet ef migrations list
|
|
|
|
# Add migration
|
|
dotnet ef migrations add MigrationName
|
|
|
|
# Apply migrations
|
|
dotnet ef database update
|
|
|
|
# Rollback to specific migration
|
|
dotnet ef database update PreviousMigrationName
|
|
|
|
# Generate SQL script (for review)
|
|
dotnet ef migrations script > migration.sql
|
|
```
|
|
|
|
---
|
|
|
|
## Testing and Validation
|
|
|
|
### Manual Testing
|
|
|
|
#### Test Case 1: Update Draft PR
|
|
|
|
```bash
|
|
# Should succeed
|
|
curl -X PUT "http://localhost:5200/api/purchase-requisitions/{draft-id}" \
|
|
-H "Content-Type: application/json" \
|
|
-d '{
|
|
"departmentCode": "IT",
|
|
"purpose": "Updated purpose",
|
|
"lines": [...]
|
|
}'
|
|
|
|
# Expected: 200 OK
|
|
```
|
|
|
|
#### Test Case 2: Update Approved PR
|
|
|
|
```bash
|
|
# Should fail
|
|
curl -X PUT "http://localhost:5200/api/purchase-requisitions/{approved-id}" \
|
|
-H "Content-Type: application/json" \
|
|
-d '{
|
|
"purpose": "Trying to change approved PR"
|
|
}'
|
|
|
|
# Expected: 403 Forbidden with PR_IMMUTABLE_STATUS error code
|
|
```
|
|
|
|
#### Test Case 3: Verify Audit Logging
|
|
|
|
```bash
|
|
# Attempt to modify approved PR (should fail)
|
|
curl -X PUT "http://localhost:5200/api/purchase-requisitions/{approved-id}" \
|
|
-H "Content-Type: application/json" \
|
|
-d '{"purpose": "Test"}'
|
|
|
|
# Check audit logs
|
|
curl "http://localhost:5200/api/purchase-requisitions/{approved-id}/audit-logs"
|
|
|
|
# Verify log entry exists with:
|
|
# - action: "UPDATE_ATTEMPT"
|
|
# - wasSuccessful: false
|
|
# - statusAtAttempt: "Approved"
|
|
```
|
|
|
|
### Automated Testing
|
|
|
|
#### Unit Test Example
|
|
|
|
```csharp
|
|
[Fact]
|
|
public async Task UpdateRequisitionAsync_WhenStatusIsApproved_ThrowsImmutableDocumentException()
|
|
{
|
|
// Arrange
|
|
var approvedPR = new PurchaseRequisition
|
|
{
|
|
Id = Guid.NewGuid(),
|
|
Status = PurchaseRequisitionStatus.Approved
|
|
};
|
|
_mockRepository.Setup(r => r.GetByIdAsync(It.IsAny<Guid>()))
|
|
.ReturnsAsync(approvedPR);
|
|
|
|
// Act & Assert
|
|
await Assert.ThrowsAsync<ImmutableDocumentException>(
|
|
() => _service.UpdateRequisitionAsync(approvedPR.Id, new UpdatePurchaseRequisitionDto())
|
|
);
|
|
}
|
|
```
|
|
|
|
#### Integration Test Example
|
|
|
|
```csharp
|
|
[Fact]
|
|
public async Task PUT_ApprovedPR_Returns403Forbidden()
|
|
{
|
|
// Arrange
|
|
var approvedPR = await CreateApprovedPRAsync();
|
|
var updateDto = new UpdatePurchaseRequisitionDto
|
|
{
|
|
Purpose = "Trying to update"
|
|
};
|
|
|
|
// Act
|
|
var response = await _client.PutAsJsonAsync(
|
|
$"/api/purchase-requisitions/{approvedPR.Id}",
|
|
updateDto
|
|
);
|
|
|
|
// Assert
|
|
Assert.Equal(HttpStatusCode.Forbidden, response.StatusCode);
|
|
|
|
var error = await response.Content.ReadFromJsonAsync<ErrorResponse>();
|
|
Assert.Equal("PR_IMMUTABLE_STATUS", error.ErrorCode);
|
|
}
|
|
```
|
|
|
|
### Performance Testing
|
|
|
|
```bash
|
|
# Load test with Apache Bench
|
|
ab -n 1000 -c 10 -p pr-update.json -T application/json \
|
|
http://localhost:5200/api/purchase-requisitions/{id}
|
|
|
|
# Monitor response times
|
|
# Expected: < 100ms for status validation
|
|
# Expected: < 200ms for full update operation
|
|
```
|
|
|
|
---
|
|
|
|
## Appendix
|
|
|
|
### File Reference
|
|
|
|
**Backend**:
|
|
- `/piam-api/src/PiamMasterData.Domain/Exceptions/ImmutableDocumentException.cs`
|
|
- `/piam-api/src/PiamMasterData.Api/Controllers/PurchaseRequisitionsController.cs:120-143`
|
|
- `/piam-api/src/PiamMasterData.Infrastructure/Repositories/PurchaseRequisitionRepository.cs:304-311`
|
|
- `/piam-api/src/PiamMasterData.Infrastructure/Services/PurchaseRequisitionAuditService.cs`
|
|
- `/piam-api/src/PiamMasterData.Infrastructure/DependencyInjection.cs:90`
|
|
|
|
**Frontend**:
|
|
- `/piam-web/src/app/features/procurement/components/purchase-requisition-form/purchase-requisition-form.component.ts:186-203`
|
|
- `/piam-web/src/app/features/procurement/components/purchase-requisition-form/purchase-requisition-form.component.html:12-75`
|
|
|
|
**Database**:
|
|
- `/piam-api/src/PiamMasterData.Infrastructure/Migrations/20251029204955_AddPurchaseRequisitionAuditLog.cs`
|
|
|
|
### Related Documentation
|
|
|
|
- **User Guide**: `/docs/user-guide-pr-approval.md`
|
|
- **Process Guide**: `/docs/pr-after-approval-guide.md`
|
|
- **FAQ**: `/docs/pr-approval-faq.md`
|
|
- **Requirements**: `/docs/pr-approval-immutability-requirement.md`
|
|
|
|
### Useful Commands
|
|
|
|
```bash
|
|
# Check application version
|
|
dotnet --version
|
|
|
|
# Check database version
|
|
psql --version
|
|
|
|
# View running processes
|
|
dotnet run --no-build &
|
|
|
|
# Check database connection
|
|
psql -h localhost -U user -d database -c "SELECT version();"
|
|
|
|
# View application logs
|
|
tail -f /var/log/piam-api/application.log
|
|
|
|
# Check disk space
|
|
df -h
|
|
|
|
# Check database size
|
|
psql -h localhost -U user -d database -c "SELECT pg_size_pretty(pg_database_size('database'));"
|
|
```
|
|
|
|
---
|
|
|
|
## Support and Escalation
|
|
|
|
### L1 Support (Help Desk)
|
|
- User cannot edit Draft PR → Clear cache
|
|
- Page won't load → Refresh browser
|
|
- Buttons missing → Check PR status
|
|
|
|
### L2 Support (System Administrator)
|
|
- Audit logs not appearing → Check migration
|
|
- Performance issues → Check database indexes
|
|
- Configuration problems → Review settings
|
|
|
|
### L3 Support (Development Team)
|
|
- Logic bugs → Code review
|
|
- Security issues → Immediate escalation
|
|
- Data integrity issues → Database investigation
|
|
|
|
### Critical Issues (Immediate Escalation)
|
|
- Approved PRs being modified
|
|
- Audit log tampering
|
|
- Security breaches
|
|
- Data corruption
|
|
|
|
---
|
|
|
|
**Document End**
|
|
|
|
*For operational procedures and user assistance, refer to the User Guide and FAQ documents.*
|