Skip to content

Commit f7344b5

Browse files
committed
kissflow integration test
1 parent 964d32a commit f7344b5

File tree

4 files changed

+315
-3
lines changed

4 files changed

+315
-3
lines changed

.env.example

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,6 @@ AWS_REGION='us-east-1'
55
SES_FROM_EMAIL=''
66
TURNSTILE_SITE_KEY=''
77
TURNSTILE_SECRET_KEY=''
8+
KISSFLOW_API_KEY=''
9+
KISSFLOW_ACCOUNT_ID=''
10+
KISSFLOW_PROCESS_ID=''

README.md

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,28 @@ Docker Compose.
1818

1919
### Third Party Services
2020

21-
* Sendgrid
22-
* Google reCAPTCHA
21+
* AWS SES (for email delivery)
22+
* Cloudflare Turnstile (for bot protection)
23+
* Kissflow API (optional - for KYC submission tracking)
2324

2425

2526
## New setup
2627

2728
Make a fork of the repository. Set environment variables in `.env` file, using the provided example. Customise the templates and code. Update public keys in [static/js/public-keys.js](static/js/public-keys.js). Deploy to your web server or K8s cluster.
2829

30+
### Kissflow Integration (Optional)
31+
32+
The application now supports automatic integration with Kissflow for KYC submission tracking. When enabled, legal submissions with a Grant ID will automatically update the corresponding AOG (Approval of Grants) item in Kissflow with the submission identifier.
33+
34+
To enable Kissflow integration:
35+
1. Add the following to your `.env` file:
36+
```
37+
KISSFLOW_API_KEY=your_api_key
38+
KISSFLOW_ACCOUNT_ID=your_account_id
39+
KISSFLOW_PROCESS_ID=your_aog_process_id
40+
```
41+
2. Ensure your Kissflow API has permissions to read and update AOG items
42+
3. Test the integration using `python test_kissflow_integration.py`
2943

3044
## Security
3145

@@ -39,4 +53,4 @@ A server operator should follow best practises for security when setting up and
3953
docker compose up
4054
```
4155

42-
The server will be listening on 4200 port.
56+
The server will be listening on 4200 port.

server.py

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from random import Random
55
import requests
66
import base64
7+
import json
78

89
from flask import Flask, render_template, request, jsonify
910
from flask_limiter import Limiter
@@ -184,6 +185,108 @@ def get_forwarded_address():
184185
# Otherwise use the default function
185186
return get_remote_address()
186187

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+
Returns the item ID if found, None otherwise.
192+
"""
193+
try:
194+
api_key = os.getenv('KISSFLOW_API_KEY')
195+
account_id = os.getenv('KISSFLOW_ACCOUNT_ID')
196+
process_id = os.getenv('KISSFLOW_PROCESS_ID')
197+
198+
if not all([api_key, account_id, process_id]):
199+
logging.error("Missing Kissflow configuration")
200+
return None
201+
202+
# Kissflow API endpoint to search for items
203+
url = f"https://api.kissflow.com/api/v1/accounts/{account_id}/processes/{process_id}/items"
204+
205+
headers = {
206+
'Authorization': f'Bearer {api_key}',
207+
'Content-Type': 'application/json'
208+
}
209+
210+
# Search for items with matching Grant ID in Request_number field
211+
params = {
212+
'filter': f'Request_number eq "{grant_id}"',
213+
'limit': 1
214+
}
215+
216+
response = requests.get(url, headers=headers, params=params)
217+
218+
if response.status_code == 200:
219+
data = response.json()
220+
items = data.get('data', [])
221+
if items:
222+
return items[0].get('_id')
223+
else:
224+
logging.error(f"Kissflow API error: {response.status_code} - {response.text}")
225+
226+
except Exception as e:
227+
logging.error(f"Error finding AOG item: {str(e)}")
228+
229+
return None
230+
231+
def update_aog_kyc_comments(item_id, legal_identifier):
232+
"""
233+
Updates the KYC_Comments field in a Kissflow AOG item with the legal identifier.
234+
"""
235+
try:
236+
api_key = os.getenv('KISSFLOW_API_KEY')
237+
account_id = os.getenv('KISSFLOW_ACCOUNT_ID')
238+
process_id = os.getenv('KISSFLOW_PROCESS_ID')
239+
240+
if not all([api_key, account_id, process_id]):
241+
logging.error("Missing Kissflow configuration")
242+
return False
243+
244+
# Kissflow API endpoint to update an item
245+
url = f"https://api.kissflow.com/api/v1/accounts/{account_id}/processes/{process_id}/items/{item_id}"
246+
247+
headers = {
248+
'Authorization': f'Bearer {api_key}',
249+
'Content-Type': 'application/json'
250+
}
251+
252+
# Update the KYC_Comments field
253+
payload = {
254+
'KYC_Comments': legal_identifier
255+
}
256+
257+
response = requests.patch(url, headers=headers, json=payload)
258+
259+
if response.status_code in [200, 204]:
260+
logging.info(f"Successfully updated AOG item {item_id} with legal identifier {legal_identifier}")
261+
return True
262+
else:
263+
logging.error(f"Kissflow API error: {response.status_code} - {response.text}")
264+
265+
except Exception as e:
266+
logging.error(f"Error updating AOG item: {str(e)}")
267+
268+
return False
269+
270+
def send_identifier_to_kissflow(grant_id, legal_identifier):
271+
"""
272+
Sends the legal identifier to the Kissflow AOG item based on Grant ID.
273+
"""
274+
if not grant_id:
275+
logging.warning("No Grant ID provided, skipping Kissflow update")
276+
return False
277+
278+
# Find the AOG item by Grant ID
279+
item_id = find_aog_item_by_grant_id(grant_id)
280+
281+
if not item_id:
282+
logging.warning(f"No AOG item found for Grant ID: {grant_id}")
283+
return False
284+
285+
# Update the KYC_Comments field
286+
success = update_aog_kyc_comments(item_id, legal_identifier)
287+
288+
return success
289+
187290
# Validate required environment variables
188291
required_env_vars = ['TURNSTILE_SITE_KEY', 'TURNSTILE_SECRET_KEY', 'AWS_ACCESS_KEY_ID', 'AWS_SECRET_ACCESS_KEY', 'AWS_REGION', 'SES_FROM_EMAIL']
189292
validate_env_vars(required_env_vars)
@@ -264,6 +367,16 @@ def submit():
264367

265368
send_email(message)
266369

370+
# If this is a legal submission with a Grant ID (reference), send to Kissflow
371+
if recipient == 'legal' and reference:
372+
kissflow_success = send_identifier_to_kissflow(reference, identifier)
373+
if kissflow_success:
374+
logging.info(f"Successfully sent identifier {identifier} to Kissflow for Grant ID {reference}")
375+
else:
376+
logging.warning(f"Failed to send identifier {identifier} to Kissflow for Grant ID {reference}")
377+
# Note: We don't fail the submission if Kissflow update fails
378+
# The email has already been sent successfully
379+
267380
notice = f'Thank you! The relevant team was notified of your submission. Please record the identifier and refer to it in correspondence: {identifier}'
268381

269382
return jsonify({'status': 'success', 'message': notice})

test_kissflow_integration.py

Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
"""
2+
Test script for Kissflow API integration
3+
This script helps verify that the Kissflow API integration is working correctly
4+
"""
5+
6+
import os
7+
import sys
8+
from datetime import datetime
9+
from random import Random
10+
from dotenv import load_dotenv
11+
12+
# Load environment variables
13+
load_dotenv()
14+
15+
# Add the current directory to the Python path
16+
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
17+
18+
# Import the functions we want to test
19+
from server import (
20+
get_identifier,
21+
find_aog_item_by_grant_id,
22+
update_aog_kyc_comments,
23+
send_identifier_to_kissflow
24+
)
25+
26+
27+
def test_environment_variables():
28+
"""Test that all required Kissflow environment variables are set"""
29+
print("Testing environment variables...")
30+
31+
required_vars = ['KISSFLOW_API_KEY', 'KISSFLOW_ACCOUNT_ID', 'KISSFLOW_PROCESS_ID']
32+
missing_vars = []
33+
34+
for var in required_vars:
35+
value = os.getenv(var)
36+
if value:
37+
# Mask the value for security
38+
masked_value = value[:4] + '...' + value[-4:] if len(value) > 8 else '***'
39+
print(f"✓ {var}: {masked_value}")
40+
else:
41+
print(f"✗ {var}: NOT SET")
42+
missing_vars.append(var)
43+
44+
if missing_vars:
45+
print(f"\nERROR: Missing environment variables: {', '.join(missing_vars)}")
46+
print("Please set these variables in your .env file")
47+
return False
48+
49+
print("\nAll environment variables are set!")
50+
return True
51+
52+
53+
def test_identifier_generation():
54+
"""Test the identifier generation function"""
55+
print("\nTesting identifier generation...")
56+
57+
# Test with default parameters
58+
identifier1 = get_identifier('legal')
59+
print(f"Generated identifier: {identifier1}")
60+
61+
# Test with custom parameters
62+
custom_date = datetime(2025, 1, 15, 14, 30, 45)
63+
identifier2 = get_identifier('legal', now=custom_date, randint=1234)
64+
expected = 'legal:2025:01:15:14:30:45:1234'
65+
66+
if identifier2 == expected:
67+
print(f"✓ Custom identifier matches expected: {identifier2}")
68+
else:
69+
print(f"✗ Custom identifier mismatch!")
70+
print(f" Expected: {expected}")
71+
print(f" Got: {identifier2}")
72+
73+
return True
74+
75+
76+
def test_find_aog_item(grant_id):
77+
"""Test finding an AOG item by Grant ID"""
78+
print(f"\nTesting AOG item lookup for Grant ID: {grant_id}")
79+
80+
try:
81+
item_id = find_aog_item_by_grant_id(grant_id)
82+
83+
if item_id:
84+
print(f"✓ Found AOG item with ID: {item_id}")
85+
return item_id
86+
else:
87+
print(f"✗ No AOG item found for Grant ID: {grant_id}")
88+
return None
89+
90+
except Exception as e:
91+
print(f"✗ Error finding AOG item: {str(e)}")
92+
return None
93+
94+
95+
def test_update_kyc_comments(item_id, identifier):
96+
"""Test updating the KYC_Comments field"""
97+
print(f"\nTesting KYC_Comments update...")
98+
print(f"Item ID: {item_id}")
99+
print(f"Identifier: {identifier}")
100+
101+
try:
102+
success = update_aog_kyc_comments(item_id, identifier)
103+
104+
if success:
105+
print("✓ Successfully updated KYC_Comments field")
106+
return True
107+
else:
108+
print("✗ Failed to update KYC_Comments field")
109+
return False
110+
111+
except Exception as e:
112+
print(f"✗ Error updating KYC_Comments: {str(e)}")
113+
return False
114+
115+
116+
def test_full_integration(grant_id):
117+
"""Test the full integration flow"""
118+
print(f"\nTesting full integration flow for Grant ID: {grant_id}")
119+
120+
# Generate a test identifier
121+
identifier = get_identifier('legal')
122+
print(f"Generated identifier: {identifier}")
123+
124+
try:
125+
success = send_identifier_to_kissflow(grant_id, identifier)
126+
127+
if success:
128+
print("✓ Full integration test passed!")
129+
print(f" - Grant ID: {grant_id}")
130+
print(f" - Identifier: {identifier}")
131+
print(" - Successfully updated in Kissflow")
132+
return True
133+
else:
134+
print("✗ Full integration test failed")
135+
return False
136+
137+
except Exception as e:
138+
print(f"✗ Error in full integration: {str(e)}")
139+
return False
140+
141+
142+
def main():
143+
"""Main test function"""
144+
print("=" * 60)
145+
print("Kissflow Integration Test Suite")
146+
print("=" * 60)
147+
148+
# Test 1: Environment variables
149+
if not test_environment_variables():
150+
print("\nCannot proceed without proper environment variables.")
151+
return
152+
153+
# Test 2: Identifier generation
154+
test_identifier_generation()
155+
156+
# Get Grant ID for testing
157+
print("\n" + "-" * 60)
158+
grant_id = input("Enter a Grant ID to test (or press Enter to skip API tests): ").strip()
159+
160+
if not grant_id:
161+
print("Skipping API tests.")
162+
return
163+
164+
# Test 3: Find AOG item
165+
item_id = test_find_aog_item(grant_id)
166+
167+
if item_id:
168+
# Test 4: Update KYC Comments
169+
test_identifier = f"TEST:{get_identifier('legal')}"
170+
test_update_kyc_comments(item_id, test_identifier)
171+
172+
# Test 5: Full integration
173+
print("\n" + "-" * 60)
174+
test_full_integration(grant_id)
175+
176+
print("\n" + "=" * 60)
177+
print("Test suite completed!")
178+
print("=" * 60)
179+
180+
181+
if __name__ == "__main__":
182+
main()

0 commit comments

Comments
 (0)