Files
docs-piam/pr-approval-admin-guide.md
2025-11-10 13:55:48 +07:00

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.*