- Introduced Aggregator.sol for Chainlink-compatible oracle functionality, including round-based updates and access control. - Added OracleWithCCIP.sol to extend Aggregator with CCIP cross-chain messaging capabilities. - Created .gitmodules to include OpenZeppelin contracts as a submodule. - Developed a comprehensive deployment guide in NEXT_STEPS_COMPLETE_GUIDE.md for Phase 2 and smart contract deployment. - Implemented Vite configuration for the orchestration portal, supporting both Vue and React frameworks. - Added server-side logic for the Multi-Cloud Orchestration Portal, including API endpoints for environment management and monitoring. - Created scripts for resource import and usage validation across non-US regions. - Added tests for CCIP error handling and integration to ensure robust functionality. - Included various new files and directories for the orchestration portal and deployment scripts.
244 lines
10 KiB
Python
244 lines
10 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
SWIFT FIN Parser
|
|
Parses SWIFT FIN messages (MT103, MT202, MT940, etc.)
|
|
"""
|
|
|
|
import re
|
|
from typing import Dict, Any, List, Optional
|
|
from datetime import datetime
|
|
import logging
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
class SWIFTFINParser:
|
|
"""Parser for SWIFT FIN messages"""
|
|
|
|
SUPPORTED_MESSAGE_TYPES = [
|
|
'MT103', # Single Customer Credit Transfer
|
|
'MT202', # General Financial Institution Transfer
|
|
'MT940', # Customer Statement Message
|
|
'MT942', # Interim Transaction Report
|
|
'MT950', # Statement Message
|
|
]
|
|
|
|
def parse(self, swift_message: str) -> Dict[str, Any]:
|
|
"""Parse SWIFT FIN message"""
|
|
lines = swift_message.strip().split('\n')
|
|
|
|
message = {
|
|
'type': 'SWIFT_FIN',
|
|
'basicHeader': self._parse_block(lines, 1),
|
|
'applicationHeader': self._parse_block(lines, 2),
|
|
'userHeader': self._parse_block(lines, 3),
|
|
'text': self._parse_text_block(lines),
|
|
'trailer': self._parse_block(lines, 5),
|
|
}
|
|
|
|
# Parse message type
|
|
app_header = message['applicationHeader'].get('raw', '')
|
|
message_type = self._extract_message_type(app_header)
|
|
message['messageType'] = message_type
|
|
|
|
# Parse based on message type
|
|
if message_type == 'MT103':
|
|
message['parsed'] = self._parse_mt103(message['text'])
|
|
elif message_type == 'MT202':
|
|
message['parsed'] = self._parse_mt202(message['text'])
|
|
elif message_type == 'MT940':
|
|
message['parsed'] = self._parse_mt940(message['text'])
|
|
elif message_type == 'MT942':
|
|
message['parsed'] = self._parse_mt942(message['text'])
|
|
elif message_type == 'MT950':
|
|
message['parsed'] = self._parse_mt950(message['text'])
|
|
|
|
return message
|
|
|
|
def _parse_block(self, lines: List[str], block_num: int) -> Dict[str, Any]:
|
|
"""Parse SWIFT block"""
|
|
block_start = f"{block_num}:"
|
|
block_end = f"-{block_num}"
|
|
|
|
block_lines = []
|
|
in_block = False
|
|
for line in lines:
|
|
if line.startswith(block_start):
|
|
in_block = True
|
|
block_lines.append(line[len(block_start):])
|
|
continue
|
|
if line.startswith(block_end):
|
|
break
|
|
if in_block:
|
|
block_lines.append(line)
|
|
|
|
return {'raw': '\n'.join(block_lines)}
|
|
|
|
def _parse_text_block(self, lines: List[str]) -> str:
|
|
"""Parse SWIFT text block (block 4)"""
|
|
text_start = "4:"
|
|
text_end = "-"
|
|
|
|
text_lines = []
|
|
in_text = False
|
|
for line in lines:
|
|
if line.startswith(text_start):
|
|
in_text = True
|
|
text_lines.append(line[2:])
|
|
continue
|
|
if line.startswith(text_end) and not line.startswith("-4"):
|
|
break
|
|
if in_text:
|
|
text_lines.append(line)
|
|
|
|
return '\n'.join(text_lines)
|
|
|
|
def _extract_message_type(self, app_header: str) -> str:
|
|
"""Extract message type from application header"""
|
|
# MT103, MT202, etc. are in the application header
|
|
match = re.search(r'MT\d{3}', app_header)
|
|
return match.group(0) if match else 'UNKNOWN'
|
|
|
|
def _parse_mt103(self, text: str) -> Dict[str, Any]:
|
|
"""Parse MT103 (Single Customer Credit Transfer)"""
|
|
fields = self._parse_swift_fields(text)
|
|
|
|
return {
|
|
'senderReference': fields.get('20', ''),
|
|
'bankOperationCode': fields.get('23B', ''),
|
|
'valueDate': fields.get('32A', '').split()[0] if fields.get('32A') else '',
|
|
'currency': fields.get('32A', '').split()[1] if fields.get('32A') and len(fields.get('32A', '').split()) > 1 else '',
|
|
'amount': fields.get('32A', '').split()[2] if fields.get('32A') and len(fields.get('32A', '').split()) > 2 else '',
|
|
'orderingCustomer': fields.get('50A', '') or fields.get('50K', ''),
|
|
'sendingInstitution': fields.get('52A', '') or fields.get('52D', ''),
|
|
'orderingInstitution': fields.get('56A', '') or fields.get('56C', '') or fields.get('56D', ''),
|
|
'beneficiaryCustomer': fields.get('59', '') or fields.get('59A', ''),
|
|
'remittanceInfo': fields.get('70', ''),
|
|
'senderToReceiverInfo': fields.get('72', ''),
|
|
}
|
|
|
|
def _parse_mt202(self, text: str) -> Dict[str, Any]:
|
|
"""Parse MT202 (General Financial Institution Transfer)"""
|
|
fields = self._parse_swift_fields(text)
|
|
|
|
return {
|
|
'senderReference': fields.get('20', ''),
|
|
'transactionReference': fields.get('21', ''),
|
|
'valueDate': fields.get('32A', '').split()[0] if fields.get('32A') else '',
|
|
'currency': fields.get('32A', '').split()[1] if fields.get('32A') and len(fields.get('32A', '').split()) > 1 else '',
|
|
'amount': fields.get('32A', '').split()[2] if fields.get('32A') and len(fields.get('32A', '').split()) > 2 else '',
|
|
'sendingInstitution': fields.get('52A', '') or fields.get('52D', ''),
|
|
'orderingInstitution': fields.get('56A', '') or fields.get('56C', '') or fields.get('56D', ''),
|
|
'intermediary': fields.get('56A', '') or fields.get('56C', '') or fields.get('56D', ''),
|
|
'accountWithInstitution': fields.get('57A', '') or fields.get('57C', '') or fields.get('57D', ''),
|
|
'beneficiaryInstitution': fields.get('58A', '') or fields.get('58D', ''),
|
|
'remittanceInfo': fields.get('70', ''),
|
|
'senderToReceiverInfo': fields.get('72', ''),
|
|
}
|
|
|
|
def _parse_mt940(self, text: str) -> Dict[str, Any]:
|
|
"""Parse MT940 (Customer Statement Message)"""
|
|
fields = self._parse_swift_fields(text)
|
|
|
|
return {
|
|
'statementNumber': fields.get('28C', ''),
|
|
'accountIdentification': fields.get('25', ''),
|
|
'statementNumberSequence': fields.get('28C', '').split('/')[0] if fields.get('28C') else '',
|
|
'statementNumberPage': fields.get('28C', '').split('/')[1] if fields.get('28C') and '/' in fields.get('28C', '') else '',
|
|
'openingBalance': self._parse_balance(fields.get('60F', '') or fields.get('60M', '')),
|
|
'closingBalance': self._parse_balance(fields.get('62F', '') or fields.get('62M', '')),
|
|
'transactions': self._parse_transactions(text),
|
|
}
|
|
|
|
def _parse_mt942(self, text: str) -> Dict[str, Any]:
|
|
"""Parse MT942 (Interim Transaction Report)"""
|
|
fields = self._parse_swift_fields(text)
|
|
|
|
return {
|
|
'statementNumber': fields.get('28C', ''),
|
|
'accountIdentification': fields.get('25', ''),
|
|
'openingBalance': self._parse_balance(fields.get('60F', '') or fields.get('60M', '')),
|
|
'transactions': self._parse_transactions(text),
|
|
}
|
|
|
|
def _parse_mt950(self, text: str) -> Dict[str, Any]:
|
|
"""Parse MT950 (Statement Message)"""
|
|
fields = self._parse_swift_fields(text)
|
|
|
|
return {
|
|
'statementNumber': fields.get('28C', ''),
|
|
'accountIdentification': fields.get('25', ''),
|
|
'openingBalance': self._parse_balance(fields.get('60F', '')),
|
|
'closingBalance': self._parse_balance(fields.get('62F', '')),
|
|
'transactions': self._parse_transactions(text),
|
|
}
|
|
|
|
def _parse_swift_fields(self, text: str) -> Dict[str, str]:
|
|
"""Parse SWIFT fields from text block"""
|
|
fields = {}
|
|
current_field = None
|
|
current_value = []
|
|
|
|
for line in text.split('\n'):
|
|
# Field starts with colon (e.g., ":20:")
|
|
if re.match(r'^:\d{2}[A-Z]?:', line):
|
|
if current_field:
|
|
fields[current_field] = '\n'.join(current_value).strip()
|
|
match = re.match(r'^:(\d{2}[A-Z]?):', line)
|
|
current_field = match.group(1) if match else None
|
|
current_value = [line.split(':', 2)[2] if ':' in line[3:] else '']
|
|
elif current_field:
|
|
current_value.append(line)
|
|
|
|
if current_field:
|
|
fields[current_field] = '\n'.join(current_value).strip()
|
|
|
|
return fields
|
|
|
|
def _parse_balance(self, balance_str: str) -> Dict[str, Any]:
|
|
"""Parse balance string (e.g., "D230101USD1000,00")"""
|
|
if not balance_str:
|
|
return {}
|
|
|
|
match = re.match(r'^([DC])(\d{6})([A-Z]{3})([\d,]+)$', balance_str)
|
|
if match:
|
|
return {
|
|
'creditDebitIndicator': 'Credit' if match.group(1) == 'C' else 'Debit',
|
|
'date': match.group(2),
|
|
'currency': match.group(3),
|
|
'amount': match.group(4).replace(',', '.'),
|
|
}
|
|
return {'raw': balance_str}
|
|
|
|
def _parse_transactions(self, text: str) -> List[Dict[str, Any]]:
|
|
"""Parse transaction entries from text"""
|
|
transactions = []
|
|
fields = self._parse_swift_fields(text)
|
|
|
|
# MT940/942/950 transactions are in field 61 and 86
|
|
transaction_text = fields.get('61', '')
|
|
if transaction_text:
|
|
for line in transaction_text.split('\n'):
|
|
if line.strip():
|
|
transactions.append(self._parse_transaction_line(line))
|
|
|
|
return transactions
|
|
|
|
def _parse_transaction_line(self, line: str) -> Dict[str, Any]:
|
|
"""Parse a single transaction line (field 61)"""
|
|
# Format: ValueDate EntryDate D/C Mark Currency Amount TransactionType Reference
|
|
match = re.match(r'^(\d{6})(\d{4})?([DC])([A-Z])([N]?)(\d{1,2})([A-Z]{3})([\d,]+)(.*)$', line)
|
|
if match:
|
|
return {
|
|
'valueDate': match.group(1),
|
|
'entryDate': match.group(2),
|
|
'creditDebitIndicator': 'Credit' if match.group(3) == 'C' else 'Debit',
|
|
'fundsCode': match.group(4),
|
|
'supplementaryDetails': match.group(5),
|
|
'transactionType': match.group(6),
|
|
'currency': match.group(7),
|
|
'amount': match.group(8).replace(',', '.'),
|
|
'reference': match.group(9).strip(),
|
|
}
|
|
return {'raw': line}
|
|
|