|
4 | 4 | from random import Random |
5 | 5 | import requests |
6 | 6 | import base64 |
| 7 | +import json |
7 | 8 |
|
8 | 9 | from flask import Flask, render_template, request, jsonify |
9 | 10 | from flask_limiter import Limiter |
@@ -184,6 +185,176 @@ def get_forwarded_address(): |
184 | 185 | # Otherwise use the default function |
185 | 186 | return get_remote_address() |
186 | 187 |
|
| 188 | +def find_aog_item_by_grant_id(grant_id): |
| 189 | + """ |
| 190 | + Finds an AOG (Approval of Grants) item in Kissflow by Grant ID. |
| 191 | + Uses the admin endpoint to get all items and searches through them. |
| 192 | + Returns the item ID if found, None otherwise. |
| 193 | + """ |
| 194 | + try: |
| 195 | + subdomain = os.getenv('KISSFLOW_SUBDOMAIN', 'ethereum') |
| 196 | + access_key_id = os.getenv('KISSFLOW_ACCESS_KEY_ID') |
| 197 | + access_key_secret = os.getenv('KISSFLOW_ACCESS_KEY_SECRET') |
| 198 | + account_id = os.getenv('KISSFLOW_ACCOUNT_ID') |
| 199 | + process_id = os.getenv('KISSFLOW_PROCESS_ID') |
| 200 | + |
| 201 | + if not all([access_key_id, access_key_secret, account_id, process_id]): |
| 202 | + logging.error("Missing Kissflow configuration") |
| 203 | + return None |
| 204 | + |
| 205 | + headers = { |
| 206 | + 'Accept': 'application/json', |
| 207 | + 'X-Access-Key-Id': access_key_id, |
| 208 | + 'X-Access-Key-Secret': access_key_secret |
| 209 | + } |
| 210 | + |
| 211 | + # Use admin endpoint to get all items |
| 212 | + page_number = 1 |
| 213 | + page_size = 100 # Get 100 items per page |
| 214 | + |
| 215 | + while True: |
| 216 | + # Kissflow admin API endpoint to get all items |
| 217 | + url = f"https://{subdomain}.kissflow.com/process/2/{account_id}/admin/{process_id}/item" |
| 218 | + |
| 219 | + params = { |
| 220 | + 'page_number': page_number, |
| 221 | + 'page_size': page_size, |
| 222 | + 'apply_preference': False |
| 223 | + } |
| 224 | + |
| 225 | + response = requests.get(url, headers=headers, params=params) |
| 226 | + |
| 227 | + if response.status_code != 200: |
| 228 | + logging.error(f"Kissflow API error: {response.status_code} - {response.text}") |
| 229 | + return None |
| 230 | + |
| 231 | + data = response.json() |
| 232 | + |
| 233 | + # The response structure contains table data with items |
| 234 | + # Look for items in the response structure |
| 235 | + items_found = [] |
| 236 | + |
| 237 | + # Check if there's a table structure in the response |
| 238 | + for key, val in data.items(): |
| 239 | + if key != "Data": |
| 240 | + continue |
| 241 | + |
| 242 | + if isinstance(val, list): |
| 243 | + for page_data in val: |
| 244 | + if isinstance(page_data, dict) and '_created_by' in page_data: |
| 245 | + items_found.append(page_data) |
| 246 | + |
| 247 | + #print(items_found) |
| 248 | + # Search through the items for matching Grant ID |
| 249 | + for item in items_found: |
| 250 | + # Check various possible field names for the Grant ID |
| 251 | + grant_id_fields = ['Request_number', 'GrantId', 'Grant_ID', 'grant_id', 'PONumber'] |
| 252 | + |
| 253 | + for field in grant_id_fields: |
| 254 | + if field in item and str(item[field]) == str(grant_id): |
| 255 | + logging.info(f"Found AOG item with ID {item.get('_id')} for Grant ID {grant_id}") |
| 256 | + return item.get('_id') |
| 257 | + |
| 258 | + # If we found fewer items than page_size, we've reached the end |
| 259 | + if len(items_found) < page_size: |
| 260 | + break |
| 261 | + |
| 262 | + page_number += 1 |
| 263 | + |
| 264 | + # Safety check to prevent infinite loops |
| 265 | + if page_number > 100: # Max 10,000 items (100 pages * 100 items) |
| 266 | + logging.warning("Reached maximum page limit while searching for Grant ID") |
| 267 | + break |
| 268 | + |
| 269 | + logging.warning(f"No AOG item found for Grant ID: {grant_id}") |
| 270 | + return None |
| 271 | + |
| 272 | + except Exception as e: |
| 273 | + logging.error(f"Error finding AOG item: {str(e)}") |
| 274 | + |
| 275 | + return None |
| 276 | + |
| 277 | +def update_aog_kyc_comments(item_id, legal_identifier): |
| 278 | + """ |
| 279 | + Updates the KYC_Comments field in a Kissflow AOG item with the legal identifier. |
| 280 | + Uses the admin PUT endpoint to update item details. |
| 281 | + """ |
| 282 | + try: |
| 283 | + subdomain = os.getenv('KISSFLOW_SUBDOMAIN', 'ethereum') |
| 284 | + access_key_id = os.getenv('KISSFLOW_ACCESS_KEY_ID') |
| 285 | + access_key_secret = os.getenv('KISSFLOW_ACCESS_KEY_SECRET') |
| 286 | + account_id = os.getenv('KISSFLOW_ACCOUNT_ID') |
| 287 | + process_id = os.getenv('KISSFLOW_PROCESS_ID') |
| 288 | + |
| 289 | + if not all([access_key_id, access_key_secret, account_id, process_id]): |
| 290 | + logging.error("Missing Kissflow configuration") |
| 291 | + return False |
| 292 | + |
| 293 | + # First, get the current item details to preserve existing data |
| 294 | + headers = { |
| 295 | + 'Accept': 'application/json', |
| 296 | + 'Content-Type': 'application/json', |
| 297 | + 'X-Access-Key-Id': access_key_id, |
| 298 | + 'X-Access-Key-Secret': access_key_secret |
| 299 | + } |
| 300 | + |
| 301 | + # Get current item details using admin endpoint |
| 302 | + get_url = f"https://{subdomain}.kissflow.com/process/2/{account_id}/admin/{process_id}/{item_id}" |
| 303 | + get_response = requests.get(get_url, headers=headers) |
| 304 | + |
| 305 | + if get_response.status_code != 200: |
| 306 | + logging.error(f"Failed to get current item details: {get_response.status_code} - {get_response.text}") |
| 307 | + return False |
| 308 | + |
| 309 | + current_item = get_response.json() |
| 310 | + |
| 311 | + # Update the KYC_Comments field while preserving other fields |
| 312 | + current_kyc = current_item['KYC_Comments'] |
| 313 | + |
| 314 | + if current_kyc != "": |
| 315 | + current_item['KYC_Comments'] = current_kyc + "\n" + legal_identifier |
| 316 | + else: |
| 317 | + current_item['KYC_Comments'] = legal_identifier |
| 318 | + |
| 319 | + # Remove all fields starting with '_' before sending to Kissflow |
| 320 | + filtered_item = {k: v for k, v in current_item.items() if not k.startswith('_')} |
| 321 | + |
| 322 | + # Use admin PUT endpoint to update the item |
| 323 | + put_url = f"https://{subdomain}.kissflow.com/process/2/{account_id}/admin/{process_id}/{item_id}" |
| 324 | + |
| 325 | + response = requests.put(put_url, headers=headers, json=filtered_item) |
| 326 | + |
| 327 | + if response.status_code == 200: |
| 328 | + logging.info(f"Successfully updated AOG item {item_id} with legal identifier {legal_identifier}") |
| 329 | + return True |
| 330 | + else: |
| 331 | + logging.error(f"Kissflow API error: {response.status_code} - {response.text}") |
| 332 | + |
| 333 | + except Exception as e: |
| 334 | + logging.error(f"Error updating AOG item: {str(e)}") |
| 335 | + |
| 336 | + return False |
| 337 | + |
| 338 | +def send_identifier_to_kissflow(grant_id, legal_identifier): |
| 339 | + """ |
| 340 | + Sends the legal identifier to the Kissflow AOG item based on Grant ID. |
| 341 | + """ |
| 342 | + if not grant_id: |
| 343 | + logging.warning("No Grant ID provided, skipping Kissflow update") |
| 344 | + return False |
| 345 | + |
| 346 | + # Find the AOG item by Grant ID |
| 347 | + item_id = find_aog_item_by_grant_id(grant_id) |
| 348 | + |
| 349 | + if not item_id: |
| 350 | + logging.warning(f"No AOG item found for Grant ID: {grant_id}") |
| 351 | + return False |
| 352 | + |
| 353 | + # Update the KYC_Comments field |
| 354 | + success = update_aog_kyc_comments(item_id, legal_identifier) |
| 355 | + |
| 356 | + return success |
| 357 | + |
187 | 358 | # Validate required environment variables |
188 | 359 | required_env_vars = ['TURNSTILE_SITE_KEY', 'TURNSTILE_SECRET_KEY', 'AWS_ACCESS_KEY_ID', 'AWS_SECRET_ACCESS_KEY', 'AWS_REGION', 'SES_FROM_EMAIL'] |
189 | 360 | validate_env_vars(required_env_vars) |
@@ -264,6 +435,16 @@ def submit(): |
264 | 435 |
|
265 | 436 | send_email(message) |
266 | 437 |
|
| 438 | + # If this is a legal submission with a Grant ID (reference), send to Kissflow |
| 439 | + if recipient == 'legal' and reference: |
| 440 | + kissflow_success = send_identifier_to_kissflow(reference, identifier) |
| 441 | + if kissflow_success: |
| 442 | + logging.info(f"Successfully sent identifier {identifier} to Kissflow for Grant ID {reference}") |
| 443 | + else: |
| 444 | + logging.warning(f"Failed to send identifier {identifier} to Kissflow for Grant ID {reference}") |
| 445 | + # Note: We don't fail the submission if Kissflow update fails |
| 446 | + # The email has already been sent successfully |
| 447 | + |
267 | 448 | notice = f'Thank you! The relevant team was notified of your submission. Please record the identifier and refer to it in correspondence: {identifier}' |
268 | 449 |
|
269 | 450 | return jsonify({'status': 'success', 'message': notice}) |
|
0 commit comments