// SPDX-License-Identifier: MIT pragma solidity ^0.8.20; import "@openzeppelin/contracts/access/Ownable.sol"; /** * @title ApprovalManager * @notice Manages and records approval actions on the blockchain * @dev Provides immutable audit trail for license approvals */ contract ApprovalManager is Ownable { enum ApprovalStatus { PENDING, APPROVED, REJECTED, CHANGES_REQUESTED, INVALIDATED } struct Approval { bytes32 id; string requestId; address departmentAddress; ApprovalStatus status; string remarksHash; string[] documentHashes; uint256 timestamp; bool isValid; } // Mapping from approval ID to Approval struct mapping(bytes32 => Approval) private _approvals; // Mapping from request ID to approval IDs mapping(string => bytes32[]) private _requestApprovals; // Counter for generating unique approval IDs uint256 private _approvalCounter; // Events event ApprovalRecorded( bytes32 indexed approvalId, string indexed requestId, address indexed departmentAddress, ApprovalStatus status, uint256 timestamp ); event ApprovalInvalidated( bytes32 indexed approvalId, string reason ); constructor() Ownable(msg.sender) {} /** * @notice Record an approval action * @param requestId The license request ID * @param departmentAddress The address of the approving department * @param status The approval status * @param remarksHash Hash of the approval remarks * @param documentHashes Array of document hashes that were reviewed * @return approvalId The unique ID of the recorded approval */ function recordApproval( string calldata requestId, address departmentAddress, ApprovalStatus status, string calldata remarksHash, string[] calldata documentHashes ) public onlyOwner returns (bytes32) { require(bytes(requestId).length > 0, "Request ID required"); require(departmentAddress != address(0), "Invalid department address"); _approvalCounter++; bytes32 approvalId = keccak256( abi.encodePacked(requestId, departmentAddress, block.timestamp, _approvalCounter) ); Approval storage approval = _approvals[approvalId]; approval.id = approvalId; approval.requestId = requestId; approval.departmentAddress = departmentAddress; approval.status = status; approval.remarksHash = remarksHash; approval.timestamp = block.timestamp; approval.isValid = true; // Copy document hashes using loop (calldata to storage) for (uint256 i = 0; i < documentHashes.length; i++) { approval.documentHashes.push(documentHashes[i]); } _requestApprovals[requestId].push(approvalId); emit ApprovalRecorded( approvalId, requestId, departmentAddress, status, block.timestamp ); return approvalId; } /** * @notice Get all approvals for a request * @param requestId The license request ID * @return Array of Approval structs */ function getRequestApprovals(string calldata requestId) public view returns (Approval[] memory) { bytes32[] memory approvalIds = _requestApprovals[requestId]; Approval[] memory approvals = new Approval[](approvalIds.length); for (uint256 i = 0; i < approvalIds.length; i++) { approvals[i] = _approvals[approvalIds[i]]; } return approvals; } /** * @notice Invalidate an existing approval * @param approvalId The approval ID to invalidate * @param reason The reason for invalidation */ function invalidateApproval(bytes32 approvalId, string calldata reason) public onlyOwner { require(_approvals[approvalId].isValid, "Approval not found or already invalid"); _approvals[approvalId].isValid = false; _approvals[approvalId].status = ApprovalStatus.INVALIDATED; emit ApprovalInvalidated(approvalId, reason); } /** * @notice Verify if approval remarks match the stored hash * @param approvalId The approval ID * @param remarksHash The hash to verify * @return True if the hashes match */ function verifyApproval(bytes32 approvalId, string calldata remarksHash) public view returns (bool) { Approval memory approval = _approvals[approvalId]; return approval.isValid && keccak256(bytes(approval.remarksHash)) == keccak256(bytes(remarksHash)); } /** * @notice Get details of a specific approval * @param approvalId The approval ID * @return The Approval struct */ function getApprovalDetails(bytes32 approvalId) public view returns (Approval memory) { require(_approvals[approvalId].timestamp > 0, "Approval not found"); return _approvals[approvalId]; } /** * @notice Get the count of approvals for a request * @param requestId The license request ID * @return The number of approvals */ function getApprovalCount(string calldata requestId) public view returns (uint256) { return _requestApprovals[requestId].length; } }