mrfirdauss commited on
Commit
cce8d96
·
1 Parent(s): 12a4071

feat; add prefill

Browse files
app/util/housewife_statement_letter_generator.py CHANGED
@@ -1,18 +1,14 @@
1
- # util/housewife_statement_letter.py
2
-
3
  from fpdf import FPDF
4
  import datetime
5
  import logging
6
  import pandas as pd
7
  from .pdf_document_generator import PDFDocumentGenerator
8
- from .db_utils import DBManager # Import the DBManager
9
 
10
  class HousewifeStatementLetterGenerator(PDFDocumentGenerator):
11
  """
12
  Generates a specific PDF for a Housewife's Statement Letter
13
- for a visa application.
14
-
15
- This class now fetches and prioritizes data from the database.
16
  """
17
 
18
  def __init__(self, data: dict):
@@ -21,76 +17,109 @@ class HousewifeStatementLetterGenerator(PDFDocumentGenerator):
21
  self.logger = logging.getLogger(__name__)
22
  self.logger.setLevel(logging.INFO)
23
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
24
  def _prepare_data(self, application_id: int, initial_data: dict) -> dict:
25
  """
26
- Fetches all data from form, smart_upload, and profile,
27
- then combines it with the initial_data using a clear priority logic.
28
  """
29
- db_manager = DBManager()
30
-
31
- # 1. Get Form Data
32
- form_df = db_manager.get_form_data(application_id)
33
- form_data = pd.Series(form_df.value.values, index=form_df.name).to_dict()
34
-
35
- # 2. Get Smart Upload Data
36
- smart_df = db_manager.get_smart_upload_results(application_id)
37
- ktp_data = self._get_smart_doc_data(smart_df, 'ktp')
38
- passport_data = self._get_smart_doc_data(smart_df, 'passport')
39
-
40
- # 3. Get Profile Name
41
- profile_name = db_manager.get_profile_name_by_app_id(application_id)
42
-
43
- # 4. Start with initial_data as the base (lowest priority)
44
- final_data = initial_data.copy()
45
 
46
- # 5. Apply Priority Logic (DB data overrides initial_data)
 
 
 
47
 
48
- # Priority: 1. Profile, 2. Passport, 3. KTP, 4. InitialData
49
- final_data["applicant_name"] = profile_name or \
50
- passport_data.get("name") or \
51
- ktp_data.get("name") or \
52
- final_data.get("applicant_name")
53
-
54
- # Priority: 1. Passport, 2. KTP, 3. Form, 4. InitialData
55
- # (Assuming 'date_of_birth' key from smart_upload and 'Applicant Date of Birth' from form)
56
- final_data["applicant_dob"] = passport_data.get("date_of_birth") or \
57
- ktp_data.get("date_of_birth") or \
58
- form_data.get("Applicant Date of Birth") or \
59
- final_data.get("applicant_dob")
60
-
61
- # Priority: 1. Passport, 2. Form, 3. InitialData
62
- # (Assuming 'passport_number' key from smart_upload and 'Applicant Passport Number' from form)
63
- final_data["applicant_passport"] = passport_data.get("passport_number") or \
64
- form_data.get("Applicant Passport Number") or \
65
- final_data.get("applicant_passport")
66
-
67
- # --- Form-Specific Data (Priority: 1. Form, 2. InitialData) ---
68
-
69
- # (Assuming form field names for the following)
70
- final_data["sponsor_name"] = form_data.get("Sponsor Full Name") or \
71
- final_data.get("sponsor_name")
72
-
73
- final_data["sponsor_bank_details"] = form_data.get("Sponsor Bank Account Details") or \
74
- final_data.get("sponsor_bank_details")
75
-
76
- final_data["destination_country"] = form_data.get("Destination Country") or \
77
- final_data.get("destination_country")
78
-
79
- final_data["visa_type"] = form_data.get("Visa Type") or \
80
- final_data.get("visa_type")
81
-
82
- final_data["visit_end_date"] = form_data.get("Planned Trip End Date") or \
83
- final_data.get("visit_end_date")
84
-
85
- final_data["travel_companions"] = form_data.get("Travel Companions") or \
86
- final_data.get("travel_companions")
87
-
88
- # Priority: 1. KTP, 2. Form, 3. InitialData
89
- final_data["letter_location"] = ktp_data.get("city") or \
90
- form_data.get("City of Residence") or \
91
- final_data.get("letter_location")
92
-
93
- return final_data
94
 
95
  def build_document(self, pdf: FPDF):
96
  """
@@ -127,9 +156,13 @@ class HousewifeStatementLetterGenerator(PDFDocumentGenerator):
127
  travel_companions = prepared_data.get("travel_companions", "[people who join the trip]")
128
 
129
  today = datetime.date.today()
130
- default_date = today.strftime("%B %d, %Y")
131
  letter_location = prepared_data.get("letter_location", "[City]")
132
- letter_date = prepared_data.get("letter_date", default_date)
 
 
 
 
 
133
 
134
  # --- 3. Build the PDF Document (Original logic) ---
135
  pdf.set_font('Times', 'BU', 12)
@@ -185,7 +218,7 @@ class HousewifeStatementLetterGenerator(PDFDocumentGenerator):
185
  )
186
  pdf.multi_cell(0, 5, body_p3)
187
  pdf.ln(10)
188
-
189
  # Body Paragraph 4
190
  body_p4 = "Thus, I have made this statement in all honesty."
191
  pdf.multi_cell(0, 5, body_p4)
@@ -195,4 +228,6 @@ class HousewifeStatementLetterGenerator(PDFDocumentGenerator):
195
  pdf.cell(0, 5, f"{letter_location}, {letter_date}")
196
  pdf.ln(25) # Space for signature
197
  pdf.set_font('Times', 'B', 11)
198
- pdf.cell(0, 5, applicant_name)
 
 
 
 
 
1
  from fpdf import FPDF
2
  import datetime
3
  import logging
4
  import pandas as pd
5
  from .pdf_document_generator import PDFDocumentGenerator
6
+ from .db_utils import DBManager # Import the DBManager
7
 
8
  class HousewifeStatementLetterGenerator(PDFDocumentGenerator):
9
  """
10
  Generates a specific PDF for a Housewife's Statement Letter
11
+ for a visa application, with prefill capability.
 
 
12
  """
13
 
14
  def __init__(self, data: dict):
 
17
  self.logger = logging.getLogger(__name__)
18
  self.logger.setLevel(logging.INFO)
19
 
20
+ def get_prefill_data(self) -> dict:
21
+ """
22
+ Fetches all data from form, smart_upload, and profile
23
+ and merges it for the frontend, using priority logic.
24
+ """
25
+ application_id = self.data.get("application_id")
26
+ if not application_id:
27
+ self.logger.error("application_id missing from data for prefill")
28
+ return {"error": "application_id missing"}
29
+
30
+ # Start with initial_data (data passed to the constructor)
31
+ initial_data = self.data.copy()
32
+
33
+ try:
34
+ db_manager = DBManager()
35
+
36
+ # 1. Get Form Data
37
+ form_df = db_manager.get_form_data(application_id)
38
+ form_data = pd.Series(form_df.value.values, index=form_df.name).to_dict()
39
+
40
+ # 2. Get Smart Upload Data
41
+ smart_df = db_manager.get_smart_upload_results(application_id)
42
+ ktp_data = self._get_smart_doc_data(smart_df, 'ktp')
43
+ passport_data = self._get_smart_doc_data(smart_df, 'passport')
44
+
45
+ # 3. Get Profile Name
46
+ profile_name = db_manager.get_profile_name_by_app_id(application_id)
47
+
48
+ # 4. Start with initial_data as the base (lowest priority)
49
+ final_data = initial_data.copy()
50
+
51
+ # 5. Apply Priority Logic (DB data overrides initial_data)
52
+
53
+ # Applicant Details
54
+ # Priority: 1. Profile, 2. Passport, 3. KTP, 4. InitialData
55
+ final_data["applicant_name"] = profile_name or \
56
+ passport_data.get("name") or \
57
+ ktp_data.get("name") or \
58
+ initial_data.get("applicant_name")
59
+
60
+ # Priority: 1. Passport, 2. KTP, 3. Form, 4. InitialData
61
+ final_data["applicant_dob"] = passport_data.get("date_of_birth") or \
62
+ ktp_data.get("date_of_birth") or \
63
+ form_data.get("Applicant Date of Birth") or \
64
+ initial_data.get("applicant_dob")
65
+
66
+ # Priority: 1. Passport, 2. Form, 3. InitialData
67
+ final_data["applicant_passport"] = passport_data.get("passport_number") or \
68
+ form_data.get("Applicant Passport Number") or \
69
+ initial_data.get("applicant_passport")
70
+
71
+ # --- Form-Specific Data (Priority: 1. Form, 2. InitialData) ---
72
+
73
+ final_data["sponsor_name"] = form_data.get("Sponsor Full Name") or \
74
+ initial_data.get("sponsor_name")
75
+
76
+ final_data["sponsor_bank_details"] = form_data.get("Sponsor Bank Account Details") or \
77
+ initial_data.get("sponsor_bank_details")
78
+
79
+ final_data["destination_country"] = form_data.get("Destination Country") or \
80
+ initial_data.get("destination_country")
81
+
82
+ final_data["visa_type"] = form_data.get("Visa Type") or \
83
+ initial_data.get("visa_type")
84
+
85
+ final_data["visit_end_date"] = form_data.get("Planned Trip End Date") or \
86
+ initial_data.get("visit_end_date")
87
+
88
+ final_data["travel_companions"] = form_data.get("Travel Companions") or \
89
+ initial_data.get("travel_companions")
90
+
91
+ # Location Priority: 1. KTP, 2. Form, 3. InitialData
92
+ final_data["letter_location"] = ktp_data.get("city") or \
93
+ form_data.get("City of Residence") or \
94
+ initial_data.get("letter_location")
95
+
96
+ final_data["letter_date"] = initial_data.get("letter_date") # Keep initial data for date if provided manually
97
+
98
+ final_data["application_id"] = application_id
99
+
100
+ return final_data
101
+
102
+ except Exception as e:
103
+ self.logger.error(f"Failed to prepare data for Housewife prefill: {e}", exc_info=True)
104
+ return {"error": str(e)}
105
+
106
  def _prepare_data(self, application_id: int, initial_data: dict) -> dict:
107
  """
108
+ Fetches and merges data using the public get_prefill_data method.
109
+ This is used internally before building the document.
110
  """
111
+ # Call the public method to handle data fetching and merging
112
+ data = self.get_prefill_data()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
113
 
114
+ # If prefill failed (e.g., missing application_id), return the initial_data
115
+ if data.get("error"):
116
+ self.logger.warning(f"Prefill failed, falling back to initial data for App ID {application_id}")
117
+ return initial_data
118
 
119
+ # Ensure 'application_id' is preserved
120
+ data["application_id"] = application_id
121
+ return data
122
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
123
 
124
  def build_document(self, pdf: FPDF):
125
  """
 
156
  travel_companions = prepared_data.get("travel_companions", "[people who join the trip]")
157
 
158
  today = datetime.date.today()
 
159
  letter_location = prepared_data.get("letter_location", "[City]")
160
+
161
+ # Use prepared data for date, falling back to today's date if not set
162
+ letter_date = prepared_data.get("letter_date")
163
+ if not letter_date:
164
+ letter_date = today.strftime("%B %d, %Y")
165
+
166
 
167
  # --- 3. Build the PDF Document (Original logic) ---
168
  pdf.set_font('Times', 'BU', 12)
 
218
  )
219
  pdf.multi_cell(0, 5, body_p3)
220
  pdf.ln(10)
221
+
222
  # Body Paragraph 4
223
  body_p4 = "Thus, I have made this statement in all honesty."
224
  pdf.multi_cell(0, 5, body_p4)
 
228
  pdf.cell(0, 5, f"{letter_location}, {letter_date}")
229
  pdf.ln(25) # Space for signature
230
  pdf.set_font('Times', 'B', 11)
231
+ pdf.cell(0, 5, applicant_name)
232
+
233
+ return pdf
app/util/japan_multientry_visa_letter_generator.py CHANGED
@@ -5,71 +5,106 @@ from .pdf_document_generator import PDFDocumentGenerator # The fpdf2-based one
5
  from .db_utils import DBManager
6
  import pandas as pd
7
  import logging
 
8
  class JapanMultiEntryVisaLetterGenerator(PDFDocumentGenerator):
9
  """
10
  Generates the specific PDF for the Japan Multiple Entry Visa application
11
  using the pure-Python fpdf2 library.
12
 
13
  Follows a clean pattern:
14
- 1. _prepare_data() fetches and prioritizes all data from DB.
15
- 2. build_document() uses that data to draw the PDF.
 
16
  """
17
  def __init__(self, data: dict):
18
  super().__init__(data)
19
  self.logger = logging.getLogger(__name__)
20
  self.logger.setLevel(logging.INFO)
21
- self.logger.info("Init Japan: %s", data)
22
 
23
  def _prepare_data(self, application_id: int, initial_data: dict) -> dict:
24
  """
25
  Fetches all data from form, smart_upload, and profile,
26
  then combines it with the initial_data using a clear priority logic.
 
27
  """
28
- db_manager = DBManager()
29
-
30
- # 1. Get Form Data
31
- form_df = db_manager.get_form_data(application_id)
32
- form_data = pd.Series(form_df.value.values, index=form_df.name).to_dict()
33
-
34
- # 2. Get Smart Upload Data
35
- smart_df = db_manager.get_smart_upload_results(application_id)
36
- ktp_data = self._get_smart_doc_data(smart_df, 'ktp')
37
- passport_data = self._get_smart_doc_data(smart_df, 'passport')
38
-
39
- # 3. Get Profile Name
40
- profile_name = db_manager.get_profile_name_by_app_id(application_id)
41
 
42
- # 4. Start with initial_data as the base (lowest priority)
43
- final_data = initial_data.copy()
 
44
 
45
- # 5. Apply Priority Logic (DB data overrides initial_data)
46
-
47
- # NAME Priority: 1. Profile, 2. Passport, 3. KTP, 4. InitialData
48
- final_data["name"] = profile_name or \
49
- passport_data.get("name") or \
50
- ktp_data.get("name") or \
51
- final_data.get("name") # Fallback to initial data
52
-
53
- # ADDRESS Priority: 1. KTP, 2. Form Data, 3. InitialData
54
- final_data["address"] = ktp_data.get("address") or \
55
- form_data.get("Current Residential Address in Home Country") or \
56
- final_data.get("address") # Fallback to initial data
 
57
 
58
- # EMAIL Priority: 1. Form Data, 2. InitialData
59
- final_data["email"] = form_data.get("E-mail address") or final_data.get("email")
60
 
61
- # PHONE Priority: 1. Form Data, 2. InitialData
62
- phone_raw = form_data.get("Your Phone Number") or final_data.get("phone")
63
- if phone_raw and isinstance(phone_raw, str) and not phone_raw.startswith("+"):
64
- final_data["phone"] = f"+{phone_raw}"
65
- else:
66
- final_data["phone"] = phone_raw
67
-
68
- # Ensure other fields from initial_data are kept
69
- final_data["city"] = final_data.get("city")
70
- final_data["postal_code"] = final_data.get("postal_code")
71
-
72
- return final_data
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
73
 
74
  def build_document(self, pdf: FPDF):
75
  """
@@ -78,12 +113,12 @@ class JapanMultiEntryVisaLetterGenerator(PDFDocumentGenerator):
78
  """
79
 
80
  # --- 1. Data Preparation ---
81
- # Start with the initial data passed to the class
82
  prepared_data = self.data.copy() if self.data else {}
83
 
84
  if prepared_data.get("application_id"):
85
  try:
86
- # Try to get new data, using the initial data as a fallback
87
  prepared_data = self._prepare_data(
88
  prepared_data["application_id"],
89
  prepared_data
@@ -95,13 +130,12 @@ class JapanMultiEntryVisaLetterGenerator(PDFDocumentGenerator):
95
  # --- 2. PDF Drawing ---
96
  # Use the prepared_data (which is either merged or initial)
97
  # with the original hardcoded fallbacks as the final safety net.
98
- self.logger.info("LOL: %s", self.data)
99
- name = prepared_data.get("name", self.data.get("name", "[YOUR NAME]"))
100
- address = prepared_data.get("address", self.data.get("address", "[YOUR ADDRESS]"))
101
- city = prepared_data.get("city", self.data.get("city", "[CITY]"))
102
- postal_code = prepared_data.get("postal_code", self.data.get("postal_code", "[POSTAL CODE]"))
103
- email = prepared_data.get("email", self.data.get("email", "[your.email@example.com]"))
104
- phone = prepared_data.get("phone", self.data.get("phone", "[+123456789]"))
105
 
106
  today = datetime.date.today()
107
  formatted_date = today.strftime("%d, %B, %Y")
@@ -151,7 +185,7 @@ class JapanMultiEntryVisaLetterGenerator(PDFDocumentGenerator):
151
  "Should you require any further information or additional "
152
  "documentation, please feel free to contact me.\n\n"
153
  "Thank you for considering my application. I look forward to your response."
154
- )
155
  pdf.multi_cell(0, 5, body_text)
156
 
157
  pdf.ln(10)
@@ -159,4 +193,6 @@ class JapanMultiEntryVisaLetterGenerator(PDFDocumentGenerator):
159
 
160
  pdf.ln(25)
161
 
162
- pdf.cell(0, 5, name)
 
 
 
5
  from .db_utils import DBManager
6
  import pandas as pd
7
  import logging
8
+
9
  class JapanMultiEntryVisaLetterGenerator(PDFDocumentGenerator):
10
  """
11
  Generates the specific PDF for the Japan Multiple Entry Visa application
12
  using the pure-Python fpdf2 library.
13
 
14
  Follows a clean pattern:
15
+ 1. get_prefill_data() fetches and prioritizes all data from DB for frontend prefill.
16
+ 2. _prepare_data() does the same for PDF generation context.
17
+ 3. build_document() uses that data to draw the PDF.
18
  """
19
  def __init__(self, data: dict):
20
  super().__init__(data)
21
  self.logger = logging.getLogger(__name__)
22
  self.logger.setLevel(logging.INFO)
23
+ # self.logger.info("Init Japan: %s", data)
24
 
25
  def _prepare_data(self, application_id: int, initial_data: dict) -> dict:
26
  """
27
  Fetches all data from form, smart_upload, and profile,
28
  then combines it with the initial_data using a clear priority logic.
29
+ (Internal method used before building the document)
30
  """
31
+ # Call the public method to handle data fetching and merging
32
+ data = self.get_prefill_data()
 
 
 
 
 
 
 
 
 
 
 
33
 
34
+ # If prefill failed to get data (e.g., missing application_id), return initial_data
35
+ if data.get("error"):
36
+ return initial_data
37
 
38
+ # Ensure 'application_id' is preserved
39
+ data["application_id"] = application_id
40
+ return data
41
+
42
+ def get_prefill_data(self) -> dict:
43
+ """
44
+ Fetches all data from form, smart_upload, and profile
45
+ and merges it for the frontend.
46
+ """
47
+ application_id = self.data.get("application_id")
48
+ if not application_id:
49
+ self.logger.error("application_id missing from data for prefill")
50
+ return {"error": "application_id missing"}
51
 
52
+ # Start with initial_data (data passed to the constructor)
53
+ initial_data = self.data.copy()
54
 
55
+ try:
56
+ db_manager = DBManager()
57
+
58
+ # 1. Get Form Data
59
+ form_df = db_manager.get_form_data(application_id)
60
+ form_data = pd.Series(form_df.value.values, index=form_df.name).to_dict()
61
+
62
+ # 2. Get Smart Upload Data
63
+ smart_df = db_manager.get_smart_upload_results(application_id)
64
+ ktp_data = self._get_smart_doc_data(smart_df, 'ktp')
65
+ passport_data = self._get_smart_doc_data(smart_df, 'passport')
66
+
67
+ # 3. Get Profile Name
68
+ profile_name = db_manager.get_profile_name_by_app_id(application_id)
69
+
70
+ # 4. Start with initial_data as the base (lowest priority)
71
+ final_data = initial_data.copy()
72
+
73
+ # 5. Apply Priority Logic (DB data overrides initial_data)
74
+
75
+ # NAME Priority: 1. Profile, 2. Passport, 3. KTP, 4. InitialData
76
+ final_data["name"] = profile_name or \
77
+ passport_data.get("name") or \
78
+ ktp_data.get("name") or \
79
+ initial_data.get("name") # Fallback to initial data
80
+
81
+ # ADDRESS Priority: 1. KTP, 2. Form Data, 3. InitialData
82
+ final_data["address"] = ktp_data.get("address") or \
83
+ form_data.get("Current Residential Address in Home Country") or \
84
+ initial_data.get("address") # Fallback to initial data
85
+
86
+ # CITY/POSTAL CODE Priority: 1. KTP, 2. InitialData (if not already set)
87
+ final_data["city"] = ktp_data.get("city") or initial_data.get("city")
88
+ final_data["postal_code"] = ktp_data.get("postal_code") or initial_data.get("postal_code")
89
+
90
+ # EMAIL Priority: 1. Form Data, 2. InitialData
91
+ final_data["email"] = form_data.get("E-mail address") or initial_data.get("email")
92
+
93
+ # PHONE Priority: 1. Form Data, 2. InitialData (format check)
94
+ phone_raw = form_data.get("Your Phone Number") or initial_data.get("phone")
95
+ if phone_raw and isinstance(phone_raw, str) and not phone_raw.startswith("+"):
96
+ final_data["phone"] = f"+{phone_raw}"
97
+ else:
98
+ final_data["phone"] = phone_raw
99
+
100
+ # Ensure application_id is included
101
+ final_data["application_id"] = application_id
102
+
103
+ return final_data
104
+
105
+ except Exception as e:
106
+ self.logger.error(f"Failed to prepare data for Japan prefill: {e}", exc_info=True)
107
+ return {"error": str(e)}
108
 
109
  def build_document(self, pdf: FPDF):
110
  """
 
113
  """
114
 
115
  # --- 1. Data Preparation ---
116
+ # Get the final prepared data
117
  prepared_data = self.data.copy() if self.data else {}
118
 
119
  if prepared_data.get("application_id"):
120
  try:
121
+ # Use the internal _prepare_data which now calls get_prefill_data
122
  prepared_data = self._prepare_data(
123
  prepared_data["application_id"],
124
  prepared_data
 
130
  # --- 2. PDF Drawing ---
131
  # Use the prepared_data (which is either merged or initial)
132
  # with the original hardcoded fallbacks as the final safety net.
133
+ name = prepared_data.get("name", "[YOUR NAME]")
134
+ address = prepared_data.get("address", "[YOUR ADDRESS]")
135
+ city = prepared_data.get("city", "[CITY]")
136
+ postal_code = prepared_data.get("postal_code", "[POSTAL CODE]")
137
+ email = prepared_data.get("email", "[your.email@example.com]")
138
+ phone = prepared_data.get("phone", "[+123456789]")
 
139
 
140
  today = datetime.date.today()
141
  formatted_date = today.strftime("%d, %B, %Y")
 
185
  "Should you require any further information or additional "
186
  "documentation, please feel free to contact me.\n\n"
187
  "Thank you for considering my application. I look forward to your response."
188
+ )
189
  pdf.multi_cell(0, 5, body_text)
190
 
191
  pdf.ln(10)
 
193
 
194
  pdf.ln(25)
195
 
196
+ pdf.cell(0, 5, name)
197
+
198
+ return pdf
app/util/passport_collection_letter_generator.py CHANGED
@@ -3,14 +3,12 @@ import datetime
3
  import logging
4
  import pandas as pd
5
  from .pdf_document_generator import PDFDocumentGenerator
6
- from .db_utils import DBManager # Import the DBManager
7
 
8
  class PassportCollectionLetterGenerator(PDFDocumentGenerator):
9
  """
10
  Generates a specific PDF for a Passport Collection Letter (Surat Kuasa)
11
- based on the provided Indonesian template.
12
-
13
- This class now fetches and prioritizes data from the database.
14
  """
15
 
16
  def __init__(self, data: dict):
@@ -19,70 +17,105 @@ class PassportCollectionLetterGenerator(PDFDocumentGenerator):
19
  self.logger = logging.getLogger(__name__)
20
  self.logger.setLevel(logging.INFO)
21
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
22
  def _prepare_data(self, application_id: int, initial_data: dict) -> dict:
23
  """
24
- Fetches all data from form, smart_upload, and profile,
25
- then combines it with the initial_data using a clear priority logic.
26
  """
27
- db_manager = DBManager()
28
-
29
- # 1. Get Form Data
30
- form_df = db_manager.get_form_data(application_id)
31
- form_data = pd.Series(form_df.value.values, index=form_df.name).to_dict()
32
-
33
- # 2. Get Smart Upload Data
34
- smart_df = db_manager.get_smart_upload_results(application_id)
35
- ktp_data = self._get_smart_doc_data(smart_df, 'ktp')
36
- passport_data = self._get_smart_doc_data(smart_df, 'passport')
37
-
38
- # 3. Get Profile Name
39
- profile_name = db_manager.get_profile_name_by_app_id(application_id)
40
-
41
- # 4. Start with initial_data as the base (lowest priority)
42
- final_data = initial_data.copy()
43
-
44
- # 5. Apply Priority Logic (DB data overrides initial_data)
45
-
46
- # --- Applicant Data ---
47
- # Priority: 1. Profile, 2. Passport, 3. KTP, 4. InitialData
48
- final_data["applicant_name"] = profile_name or \
49
- passport_data.get("name") or \
50
- ktp_data.get("name") or \
51
- final_data.get("applicant_name")
52
-
53
- # Priority: 1. KTP, 2. Form, 3. InitialData
54
- final_data["applicant_nik"] = ktp_data.get("nik") or \
55
- form_data.get("Applicant NIK") or \
56
- final_data.get("applicant_nik")
57
-
58
- # Priority: 1. KTP, 2. Form, 3. InitialData
59
- final_data["applicant_address"] = ktp_data.get("address") or \
60
- form_data.get("Applicant Address") or \
61
- final_data.get("applicant_address")
62
-
63
- # --- Agent Data (Must come from form or initial data) ---
64
- # Priority: 1. Form, 2. InitialData
65
- final_data["agent_name"] = form_data.get("Agent Name") or \
66
- final_data.get("agent_name")
67
-
68
- final_data["agent_nik"] = form_data.get("Agent NIK") or \
69
- final_data.get("agent_nik")
70
-
71
- final_data["agent_address"] = form_data.get("Agent Address") or \
72
- final_data.get("agent_address")
73
-
74
- # --- Document/Letter Data ---
75
- # Priority: 1. Passport, 2. Form, 3. InitialData
76
- final_data["passport_no"] = passport_data.get("passport_number") or \
77
- form_data.get("Applicant Passport Number") or \
78
- final_data.get("passport_no")
79
-
80
- # Priority: 1. KTP (City), 2. Form, 3. InitialData
81
- final_data["letter_location"] = ktp_data.get("city") or \
82
- form_data.get("City of Residence") or \
83
- final_data.get("letter_location")
84
-
85
- return final_data
86
 
87
  def build_document(self, pdf: FPDF):
88
  """
@@ -118,7 +151,11 @@ class PassportCollectionLetterGenerator(PDFDocumentGenerator):
118
  today = datetime.date.today()
119
  default_date = today.strftime("%d %B %Y")
120
  letter_location = prepared_data.get("letter_location", "[City]")
121
- letter_date = prepared_data.get("letter_date", default_date)
 
 
 
 
122
 
123
  # --- 3. Build the PDF Document (Original logic) ---
124
  pdf.set_left_margin(25)
@@ -212,4 +249,6 @@ class PassportCollectionLetterGenerator(PDFDocumentGenerator):
212
 
213
  # Agent Name
214
  pdf.set_x(pdf.l_margin + col_width)
215
- pdf.cell(col_width, 5, f"({agent_name})", 0, 1, 'C')
 
 
 
3
  import logging
4
  import pandas as pd
5
  from .pdf_document_generator import PDFDocumentGenerator
6
+ from .db_utils import DBManager # Import the DBManager
7
 
8
  class PassportCollectionLetterGenerator(PDFDocumentGenerator):
9
  """
10
  Generates a specific PDF for a Passport Collection Letter (Surat Kuasa)
11
+ based on the provided Indonesian template, with prefill capability.
 
 
12
  """
13
 
14
  def __init__(self, data: dict):
 
17
  self.logger = logging.getLogger(__name__)
18
  self.logger.setLevel(logging.INFO)
19
 
20
+ def get_prefill_data(self) -> dict:
21
+ """
22
+ Fetches all data from form, smart_upload, and profile
23
+ and merges it for the frontend, using priority logic.
24
+ """
25
+ application_id = self.data.get("application_id")
26
+ if not application_id:
27
+ self.logger.error("application_id missing from data for prefill")
28
+ return {"error": "application_id missing"}
29
+
30
+ # Start with initial_data (data passed to the constructor)
31
+ initial_data = self.data.copy()
32
+
33
+ try:
34
+ db_manager = DBManager()
35
+
36
+ # 1. Get Form Data
37
+ form_df = db_manager.get_form_data(application_id)
38
+ form_data = pd.Series(form_df.value.values, index=form_df.name).to_dict()
39
+
40
+ # 2. Get Smart Upload Data
41
+ smart_df = db_manager.get_smart_upload_results(application_id)
42
+ ktp_data = self._get_smart_doc_data(smart_df, 'ktp')
43
+ passport_data = self._get_smart_doc_data(smart_df, 'passport')
44
+
45
+ # 3. Get Profile Name
46
+ profile_name = db_manager.get_profile_name_by_app_id(application_id)
47
+
48
+ # 4. Start with initial_data as the base (lowest priority)
49
+ final_data = initial_data.copy()
50
+
51
+ # 5. Apply Priority Logic (DB data overrides initial_data)
52
+
53
+ # --- Applicant Data (Pemberi Kuasa) ---
54
+ # Priority: 1. Profile, 2. Passport, 3. KTP, 4. InitialData
55
+ final_data["applicant_name"] = profile_name or \
56
+ passport_data.get("name") or \
57
+ ktp_data.get("name") or \
58
+ initial_data.get("applicant_name")
59
+
60
+ # Priority: 1. KTP, 2. Form, 3. InitialData
61
+ final_data["applicant_nik"] = ktp_data.get("nik") or \
62
+ form_data.get("Applicant NIK") or \
63
+ initial_data.get("applicant_nik")
64
+
65
+ # Priority: 1. KTP, 2. Form, 3. InitialData
66
+ final_data["applicant_address"] = ktp_data.get("address") or \
67
+ form_data.get("Applicant Address") or \
68
+ initial_data.get("applicant_address")
69
+
70
+ # --- Agent Data (Penerima Kuasa) ---
71
+ # Priority: 1. Form, 2. InitialData
72
+ final_data["agent_name"] = form_data.get("Agent Name") or \
73
+ initial_data.get("agent_name")
74
+
75
+ final_data["agent_nik"] = form_data.get("Agent NIK") or \
76
+ initial_data.get("agent_nik")
77
+
78
+ final_data["agent_address"] = form_data.get("Agent Address") or \
79
+ initial_data.get("agent_address")
80
+
81
+ # --- Document/Letter Data ---
82
+ # Passport No. Priority: 1. Passport, 2. Form, 3. InitialData
83
+ final_data["passport_no"] = passport_data.get("passport_number") or \
84
+ form_data.get("Applicant Passport Number") or \
85
+ initial_data.get("passport_no")
86
+
87
+ # Location Priority: 1. KTP (City), 2. Form, 3. InitialData
88
+ final_data["letter_location"] = ktp_data.get("city") or \
89
+ form_data.get("City of Residence") or \
90
+ initial_data.get("letter_location")
91
+
92
+ final_data["letter_date"] = initial_data.get("letter_date") # Keep initial data for date
93
+
94
+ final_data["application_id"] = application_id
95
+
96
+ return final_data
97
+
98
+ except Exception as e:
99
+ self.logger.error(f"Failed to prepare data for Passport Collection prefill: {e}", exc_info=True)
100
+ return {"error": str(e)}
101
+
102
  def _prepare_data(self, application_id: int, initial_data: dict) -> dict:
103
  """
104
+ Fetches and merges data using the public get_prefill_data method.
105
+ This is used internally before building the document.
106
  """
107
+ # Call the public method to handle data fetching and merging
108
+ data = self.get_prefill_data()
109
+
110
+ # If prefill failed (e.g., missing application_id), return the initial_data
111
+ if data.get("error"):
112
+ self.logger.warning(f"Prefill failed, falling back to initial data for App ID {application_id}")
113
+ return initial_data
114
+
115
+ # Ensure 'application_id' is preserved
116
+ data["application_id"] = application_id
117
+ return data
118
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
119
 
120
  def build_document(self, pdf: FPDF):
121
  """
 
151
  today = datetime.date.today()
152
  default_date = today.strftime("%d %B %Y")
153
  letter_location = prepared_data.get("letter_location", "[City]")
154
+
155
+ # Use prepared data for date, falling back to today's date if not set
156
+ letter_date = prepared_data.get("letter_date")
157
+ if not letter_date:
158
+ letter_date = default_date
159
 
160
  # --- 3. Build the PDF Document (Original logic) ---
161
  pdf.set_left_margin(25)
 
249
 
250
  # Agent Name
251
  pdf.set_x(pdf.l_margin + col_width)
252
+ pdf.cell(col_width, 5, f"({agent_name})", 0, 1, 'C')
253
+
254
+ return pdf
app/util/pdf_document_generator.py CHANGED
@@ -146,4 +146,17 @@ class PDFDocumentGenerator(ABC):
146
 
147
  except Exception as e:
148
  print(f"Error during PDF generation: {e}")
149
- return None, str(e)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
146
 
147
  except Exception as e:
148
  print(f"Error during PDF generation: {e}")
149
+ return None, str(e)
150
+
151
+ def get_prefill_data(self) -> dict:
152
+ """
153
+ Fetches and merges all required data from various DB sources
154
+ based on the 'application_id' in self.data.
155
+
156
+ Subclasses should override this to implement their specific
157
+ data-fetching and merging logic.
158
+ """
159
+ # Base implementation just returns the data it was given.
160
+ # This acts as a placeholder for generators that don't need prefill.
161
+ self.logger.warning("Base get_prefill_data called. No data merging performed.")
162
+ return self.data
app/util/schengen_visa_letter_generator.py CHANGED
@@ -83,141 +83,134 @@ class SchengenVisaLetterGenerator(PDFDocumentGenerator):
83
 
84
  return travel_start, travel_end
85
 
86
- def _prepare_data(self, application_id: int, initial_data: dict) -> dict:
87
  """
88
- Fetches all data from form, smart_upload, and profile,
89
- then combines it with the initial_data using the new priority logic.
90
-
91
- PRIORITY:
92
- 1. Official DB Data (e.g., destination_country)
93
- 2. Initial Data (initial_data)
94
- 3. Form Data (form_data)
95
- 4. High-Trust Smart Upload (smart_upload_data)
96
  """
97
- db_manager = DBManager()
98
-
99
- # --- NEW: Get official data from DB ---
100
- destination_country_name = db_manager.get_destination_country(application_id)
101
- # ---
102
-
103
- # 1. Hit 1: Ambil data form (Priority 3)
104
- form_df = db_manager.get_form_data(application_id)
105
- form_data = pd.Series(form_df.value.values, index=form_df.name).to_dict()
106
-
107
- # 2. Hit 2: Ambil semua hasil JSON (Data Mentah)
108
- smart_df = db_manager.get_smart_upload_results(application_id)
109
- self.logger.info(f"Smart DF (raw): {smart_df}")
110
-
111
- # 3. Ekstrak data "High Trust" AI Data (Priority 4)
112
- passport_data = self._get_smart_doc_data(smart_df, 'passport')
113
- itinerary_data = self._get_smart_doc_data(smart_df, 'travel_itinerary')
114
- employment_data = self._get_smart_doc_data(smart_df, 'employment_letter')
115
- cover_letter_data = self._get_smart_doc_data(smart_df, 'cover_letter')
116
- flight_data = self._get_smart_doc_data(smart_df, 'flight_booking')
117
-
118
- # 4. Ekstrak data nested
119
- nested_start, nested_end = self._get_nested_flight_dates(flight_data)
120
- if not nested_start and not nested_end:
121
- nested_start, nested_end = self._get_nested_travel_dates(itinerary_data)
122
-
123
- # 5. Start with initial_data as the base (Priority 2)
124
- final_data = initial_data.copy()
125
-
126
- # --- MEMBANGUN DICT DATA FINAL DENGAN PRIORITAS ---
127
-
128
- # 5.1 Data Personal
129
- personal = final_data.get("personal_details", {}).copy()
130
 
131
- personal["name"] = personal.get("name") or \
132
- form_data.get("name") or \
133
- passport_data.get("name")
134
-
135
- personal["dob"] = personal.get("dob") or \
136
- form_data.get("date_of_birth") or \
137
- passport_data.get("date_of_birth")
138
-
139
- personal["nationality"] = personal.get("nationality") or \
140
- form_data.get("nationality") or \
141
- passport_data.get("nationality")
142
-
143
- personal["passport_number"] = personal.get("passport_number") or \
144
- form_data.get("passport_number") or \
145
- passport_data.get("passport_number")
146
-
147
- personal["occupation"] = personal.get("occupation") or \
148
- form_data.get("occupation") or \
149
- employment_data.get("position") or \
150
- passport_data.get("occupation")
151
-
152
- final_data["personal_details"] = personal
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
153
 
154
- # 5.2 Data Perjalanan Inti
155
-
156
- # --- UPDATED PRIORITY ---
157
- final_data["country"] = destination_country_name or \
158
- final_data.get("country") or \
159
- form_data.get("schengen_country") or \
160
- cover_letter_data.get("schengen_country")
161
 
162
- final_data["main_dest"] = destination_country_name or \
163
- final_data.get("main_dest") or \
164
- form_data.get("main_destination") or \
165
- itinerary_data.get("main_destination")
166
- # ---
 
 
 
 
 
 
 
 
 
167
 
168
- final_data["purpose"] = final_data.get("purpose") or \
169
- form_data.get("purpose") or \
170
- itinerary_data.get("purpose")
171
-
172
- # 5.3 Data Tanggal
173
- final_data["start"] = final_data.get("start") or \
174
- form_data.get("travel_start") or \
175
- nested_start
176
-
177
- final_data["end"] = final_data.get("end") or \
178
- form_data.get("travel_end") or \
179
- nested_end
180
 
181
- # 5.4 Data Lainnya
182
- final_data["city"] = final_data.get("city") or \
183
- form_data.get("city") or \
184
- cover_letter_data.get("city")
 
 
 
 
185
 
186
- final_data["event"] = final_data.get("event") or \
187
- form_data.get("event") or \
188
- cover_letter_data.get("event")
189
-
190
- final_data["other_dest"] = final_data.get("other_dest", "") or \
191
- form_data.get("other_destinations") or \
192
- cover_letter_data.get("other_destinations")
193
-
194
- duration_smart = itinerary_data.get("duration") or itinerary_data.get("total_days")
195
- final_data["duration"] = final_data.get("duration") or \
196
- form_data.get("duration") or \
197
- (str(duration_smart) if duration_smart else None)
198
-
199
- final_data["trip_highlight"] = final_data.get("trip_highlight") or \
200
- form_data.get("trip_highlight") or \
201
- cover_letter_data.get("trip_highlight")
202
-
203
- final_data["contact_email"] = final_data.get("contact_email") or \
204
- form_data.get("contact_email") or \
205
- cover_letter_data.get("contact_email")
206
-
207
- final_data["contact_phone"] = final_data.get("contact_phone") or \
208
- form_data.get("contact_phone") or \
209
- cover_letter_data.get("contact_phone")
210
-
211
- final_data["job_commitment"] = final_data.get("job_commitment") or \
212
- form_data.get("job_commitment") or \
213
- employment_data.get("job_commitment") or \
214
- cover_letter_data.get("job_commitment")
215
-
216
- final_data["financial_status"] = final_data.get("financial_status") or \
217
- form_data.get("financial_status") or \
218
- cover_letter_data.get("financial_status") or "sound"
219
 
220
- return final_data
 
 
 
 
221
 
222
  def build_document(self, pdf: FPDF):
223
  """
 
83
 
84
  return travel_start, travel_end
85
 
86
+ def get_prefill_data(self) -> dict:
87
  """
88
+ Fetches all data from form, smart_upload, and profile
89
+ and merges it for the frontend.
 
 
 
 
 
 
90
  """
91
+ application_id = self.data.get("application_id")
92
+ if not application_id:
93
+ self.logger.error("application_id missing from data for prefill")
94
+ return {"error": "application_id missing"}
95
+
96
+ # initial_data is just the dict containing the application_id
97
+ initial_data = self.data.copy()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
98
 
99
+ try:
100
+ db_manager = DBManager()
101
+
102
+ # --- This is all your logic from _prepare_data ---
103
+ destination_country_name = db_manager.get_destination_country(application_id)
104
+ form_df = db_manager.get_form_data(application_id)
105
+ form_data = pd.Series(form_df.value.values, index=form_df.name).to_dict()
106
+ smart_df = db_manager.get_smart_upload_results(application_id)
107
+
108
+ passport_data = self._get_smart_doc_data(smart_df, 'passport')
109
+ itinerary_data = self._get_smart_doc_data(smart_df, 'travel_itinerary')
110
+ employment_data = self._get_smart_doc_data(smart_df, 'employment_letter')
111
+ cover_letter_data = self._get_smart_doc_data(smart_df, 'cover_letter')
112
+ flight_data = self._get_smart_doc_data(smart_df, 'flight_booking')
113
+
114
+ nested_start, nested_end = self._get_nested_flight_dates(flight_data)
115
+ if not nested_start and not nested_end:
116
+ nested_start, nested_end = self._get_nested_travel_dates(itinerary_data)
117
+
118
+ final_data = initial_data.copy()
119
+ personal = final_data.get("personal_details", {}).copy()
120
+ personal["name"] = personal.get("name") or \
121
+ form_data.get("name") or \
122
+ passport_data.get("name")
123
+
124
+ personal["dob"] = personal.get("dob") or \
125
+ form_data.get("date_of_birth") or \
126
+ passport_data.get("date_of_birth")
127
+
128
+ personal["nationality"] = personal.get("nationality") or \
129
+ form_data.get("nationality") or \
130
+ passport_data.get("nationality")
131
+
132
+ personal["passport_number"] = personal.get("passport_number") or \
133
+ form_data.get("passport_number") or \
134
+ passport_data.get("passport_number")
135
+
136
+ personal["occupation"] = personal.get("occupation") or \
137
+ form_data.get("occupation") or \
138
+ employment_data.get("position") or \
139
+ passport_data.get("occupation")
140
+
141
+ final_data["personal_details"] = personal
142
 
143
+ # 5.2 Data Perjalanan Inti
144
+
145
+ # --- UPDATED PRIORITY ---
146
+ final_data["country"] = destination_country_name or \
147
+ final_data.get("country") or \
148
+ form_data.get("schengen_country") or \
149
+ cover_letter_data.get("schengen_country")
150
 
151
+ final_data["main_dest"] = destination_country_name or \
152
+ final_data.get("main_dest") or \
153
+ form_data.get("main_destination") or \
154
+ itinerary_data.get("main_destination")
155
+ # ---
156
+
157
+ final_data["purpose"] = final_data.get("purpose") or \
158
+ form_data.get("purpose") or \
159
+ itinerary_data.get("purpose")
160
+
161
+ # 5.3 Data Tanggal
162
+ final_data["start"] = final_data.get("start") or \
163
+ form_data.get("travel_start") or \
164
+ nested_start
165
 
166
+ final_data["end"] = final_data.get("end") or \
167
+ form_data.get("travel_end") or \
168
+ nested_end
 
 
 
 
 
 
 
 
 
169
 
170
+ # 5.4 Data Lainnya
171
+ final_data["city"] = final_data.get("city") or \
172
+ form_data.get("city") or \
173
+ cover_letter_data.get("city")
174
+
175
+ final_data["event"] = final_data.get("event") or \
176
+ form_data.get("event") or \
177
+ cover_letter_data.get("event")
178
 
179
+ final_data["other_dest"] = final_data.get("other_dest", "") or \
180
+ form_data.get("other_destinations") or \
181
+ cover_letter_data.get("other_destinations")
182
+
183
+ duration_smart = itinerary_data.get("duration") or itinerary_data.get("total_days")
184
+ final_data["duration"] = final_data.get("duration") or \
185
+ form_data.get("duration") or \
186
+ (str(duration_smart) if duration_smart else None)
187
+
188
+ final_data["trip_highlight"] = final_data.get("trip_highlight") or \
189
+ form_data.get("trip_highlight") or \
190
+ cover_letter_data.get("trip_highlight")
191
+
192
+ final_data["contact_email"] = final_data.get("contact_email") or \
193
+ form_data.get("E-mail address") or \
194
+ cover_letter_data.get("contact_email")
195
+
196
+ final_data["contact_phone"] = final_data.get("contact_phone") or \
197
+ form_data.get("Your Phone Number") or \
198
+ cover_letter_data.get("contact_phone")
199
+
200
+ final_data["job_commitment"] = final_data.get("job_commitment") or \
201
+ form_data.get("job_commitment") or \
202
+ employment_data.get("job_commitment") or \
203
+ cover_letter_data.get("job_commitment")
204
+
205
+ final_data["financial_status"] = final_data.get("financial_status") or \
206
+ form_data.get("financial_status") or \
207
+ cover_letter_data.get("financial_status") or "sound"
 
 
 
 
208
 
209
+ return final_data
210
+
211
+ except Exception as e:
212
+ self.logger.error(f"Failed to prepare data for Schengen: {e}", exc_info=True)
213
+ return {"error": str(e)}
214
 
215
  def build_document(self, pdf: FPDF):
216
  """
app/util/sponsorship_letter_generator.py CHANGED
@@ -10,7 +10,8 @@ class SponsorshipLetterGenerator(PDFDocumentGenerator):
10
  Generates a specific PDF for a Visa Sponsorship Letter based on
11
  the provided template.
12
 
13
- This class now fetches and prioritizes data from the database.
 
14
  """
15
 
16
  def __init__(self, data: dict):
@@ -19,74 +20,106 @@ class SponsorshipLetterGenerator(PDFDocumentGenerator):
19
  self.logger = logging.getLogger(__name__)
20
  self.logger.setLevel(logging.INFO)
21
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
22
  def _prepare_data(self, application_id: int, initial_data: dict) -> dict:
23
  """
24
- Fetches all data from form, smart_upload, and profile,
25
- then combines it with the initial_data using a clear priority logic.
26
  """
27
- db_manager = DBManager()
28
-
29
- # 1. Get Form Data
30
- form_df = db_manager.get_form_data(application_id)
31
- form_data = pd.Series(form_df.value.values, index=form_df.name).to_dict()
32
-
33
- # 2. Get Smart Upload Data (for Applicant)
34
- smart_df = db_manager.get_smart_upload_results(application_id)
35
- ktp_data = self._get_smart_doc_data(smart_df, 'ktp')
36
- passport_data = self._get_smart_doc_data(smart_df, 'passport')
37
-
38
- # 3. Get Profile Name (for Applicant)
39
- profile_name = db_manager.get_profile_name_by_app_id(application_id)
40
 
41
- # 4. Start with initial_data as the base (lowest priority)
42
- final_data = initial_data.copy()
 
 
43
 
44
- # 5. Apply Priority Logic
45
-
46
- # --- Sponsor Data (Priority: 1. Form, 2. InitialData) ---
47
- # (Assuming form field names match the purpose)
48
- final_data["sponsor_name"] = form_data.get("Sponsor Full Name") or \
49
- final_data.get("sponsor_name")
50
- final_data["sponsor_address"] = form_data.get("Sponsor Address") or \
51
- final_data.get("sponsor_address")
52
- final_data["sponsor_postal_code"] = form_data.get("Sponsor Postal Code") or \
53
- final_data.get("sponsor_postal_code")
54
- final_data["sponsor_email"] = form_data.get("Sponsor Email") or \
55
- final_data.get("sponsor_email")
56
- final_data["sponsor_contact_number"] = form_data.get("Sponsor Phone Number") or \
57
- final_data.get("sponsor_contact_number")
58
-
59
- # --- Applicant Data (Priority: 1. Profile, 2. Passport, 3. KTP, 4. Form, 5. InitialData) ---
60
- final_data["applicant_name"] = profile_name or \
61
- passport_data.get("name") or \
62
- ktp_data.get("name") or \
63
- form_data.get("Applicant Full Name") or \
64
- final_data.get("applicant_name")
65
-
66
- final_data["applicant_dob"] = passport_data.get("date_of_birth") or \
67
- ktp_data.get("date_of_birth") or \
68
- form_data.get("Applicant Date of Birth") or \
69
- final_data.get("applicant_dob")
70
-
71
- final_data["applicant_passport_no"] = passport_data.get("passport_number") or \
72
- form_data.get("Applicant Passport Number") or \
73
- final_data.get("applicant_passport_no")
74
-
75
- # --- Embassy & Visit Details (Priority: 1. Form, 2. InitialData) ---
76
- final_data["officer_title"] = form_data.get("Embassy Officer Title") or final_data.get("officer_title")
77
- final_data["embassy_name"] = form_data.get("Embassy Name") or final_data.get("embassy_name")
78
- final_data["embassy_address"] = form_data.get("Embassy Address") or final_data.get("embassy_address")
79
- final_data["embassy_city_state_postal"] = form_data.get("Embassy City State Postal") or final_data.get("embassy_city_state_postal")
80
-
81
- final_data["visa_name"] = form_data.get("Visa Name") or final_data.get("visa_name")
82
- final_data["sponsor_relationship"] = form_data.get("Sponsor Relationship") or final_data.get("sponsor_relationship")
83
- final_data["visit_reason"] = form_data.get("Visit Reason") or final_data.get("visit_reason")
84
- final_data["destination_country"] = form_data.get("Destination Country") or final_data.get("destination_country")
85
- final_data["visit_start_date"] = form_data.get("Visit Start Date") or final_data.get("visit_start_date")
86
- final_data["visit_end_date"] = form_data.get("Visit End Date") or final_data.get("visit_end_date")
87
- final_data["visit_purpose_details"] = form_data.get("Visit Purpose Details") or final_data.get("visit_purpose_details")
88
-
89
- return final_data
90
 
91
  def build_document(self, pdf: FPDF):
92
  """
@@ -209,4 +242,6 @@ class SponsorshipLetterGenerator(PDFDocumentGenerator):
209
  pdf.ln(15) # Space for signature
210
  pdf.cell(0, 5, sponsor_name)
211
  pdf.ln(5)
212
- pdf.cell(0, 5, sponsor_phone)
 
 
 
10
  Generates a specific PDF for a Visa Sponsorship Letter based on
11
  the provided template.
12
 
13
+ This class now fetches and prioritizes data from the database
14
+ and provides a method for frontend prefill.
15
  """
16
 
17
  def __init__(self, data: dict):
 
20
  self.logger = logging.getLogger(__name__)
21
  self.logger.setLevel(logging.INFO)
22
 
23
+ def get_prefill_data(self) -> dict:
24
+ """
25
+ Fetches all data from form, smart_upload, and profile
26
+ and merges it for the frontend.
27
+ """
28
+ application_id = self.data.get("application_id")
29
+ if not application_id:
30
+ self.logger.error("application_id missing from data for prefill")
31
+ return {"error": "application_id missing"}
32
+
33
+ # Start with initial_data (data passed to the constructor)
34
+ initial_data = self.data.copy()
35
+
36
+ try:
37
+ db_manager = DBManager()
38
+
39
+ # 1. Get Form Data
40
+ form_df = db_manager.get_form_data(application_id)
41
+ form_data = pd.Series(form_df.value.values, index=form_df.name).to_dict()
42
+
43
+ # 2. Get Smart Upload Data (for Applicant)
44
+ smart_df = db_manager.get_smart_upload_results(application_id)
45
+ ktp_data = self._get_smart_doc_data(smart_df, 'ktp')
46
+ passport_data = self._get_smart_doc_data(smart_df, 'passport')
47
+
48
+ # 3. Get Profile Name (for Applicant)
49
+ profile_name = db_manager.get_profile_name_by_app_id(application_id)
50
+
51
+ # 4. Start with initial_data as the base (lowest priority)
52
+ final_data = initial_data.copy()
53
+
54
+ # 5. Apply Priority Logic
55
+
56
+ # --- Sponsor Data (Priority: 1. Form, 2. InitialData) ---
57
+ # (Assuming form field names match the purpose)
58
+ final_data["sponsor_name"] = form_data.get("Sponsor Full Name") or \
59
+ initial_data.get("sponsor_name")
60
+ final_data["sponsor_address"] = form_data.get("Sponsor Address") or \
61
+ initial_data.get("sponsor_address")
62
+ final_data["sponsor_postal_code"] = form_data.get("Sponsor Postal Code") or \
63
+ initial_data.get("sponsor_postal_code")
64
+ final_data["sponsor_email"] = form_data.get("Sponsor Email") or \
65
+ initial_data.get("sponsor_email")
66
+ final_data["sponsor_contact_number"] = form_data.get("Sponsor Phone Number") or \
67
+ initial_data.get("sponsor_contact_number")
68
+
69
+ # --- Applicant Data (Priority: 1. Profile, 2. Passport, 3. KTP, 4. Form, 5. InitialData) ---
70
+ final_data["applicant_name"] = profile_name or \
71
+ passport_data.get("name") or \
72
+ ktp_data.get("name") or \
73
+ form_data.get("Applicant Full Name") or \
74
+ initial_data.get("applicant_name")
75
+
76
+ final_data["applicant_dob"] = passport_data.get("date_of_birth") or \
77
+ ktp_data.get("date_of_birth") or \
78
+ form_data.get("Applicant Date of Birth") or \
79
+ initial_data.get("applicant_dob")
80
+
81
+ final_data["applicant_passport_no"] = passport_data.get("passport_number") or \
82
+ form_data.get("Applicant Passport Number") or \
83
+ initial_data.get("applicant_passport_no")
84
+
85
+ # --- Embassy & Visit Details (Priority: 1. Form, 2. InitialData) ---
86
+ final_data["officer_title"] = form_data.get("Embassy Officer Title") or initial_data.get("officer_title")
87
+ final_data["embassy_name"] = form_data.get("Embassy Name") or initial_data.get("embassy_name")
88
+ final_data["embassy_address"] = form_data.get("Embassy Address") or initial_data.get("embassy_address")
89
+ final_data["embassy_city_state_postal"] = form_data.get("Embassy City State Postal") or initial_data.get("embassy_city_state_postal")
90
+
91
+ final_data["visa_name"] = form_data.get("Visa Name") or initial_data.get("visa_name")
92
+ final_data["sponsor_relationship"] = form_data.get("Sponsor Relationship") or initial_data.get("sponsor_relationship")
93
+ final_data["visit_reason"] = form_data.get("Visit Reason") or initial_data.get("visit_reason")
94
+ final_data["destination_country"] = form_data.get("Destination Country") or initial_data.get("destination_country")
95
+ final_data["visit_start_date"] = form_data.get("Visit Start Date") or initial_data.get("visit_start_date")
96
+ final_data["visit_end_date"] = form_data.get("Visit End Date") or initial_data.get("visit_end_date")
97
+ final_data["visit_purpose_details"] = form_data.get("Visit Purpose Details") or initial_data.get("visit_purpose_details")
98
+
99
+ final_data["application_id"] = application_id
100
+
101
+ return final_data
102
+
103
+ except Exception as e:
104
+ self.logger.error(f"Failed to prepare data for Sponsorship prefill: {e}", exc_info=True)
105
+ return {"error": str(e)}
106
+
107
  def _prepare_data(self, application_id: int, initial_data: dict) -> dict:
108
  """
109
+ Fetches and merges data using the public get_prefill_data method.
110
+ This is used internally before building the document.
111
  """
112
+ # Call the public method to handle data fetching and merging
113
+ data = self.get_prefill_data()
 
 
 
 
 
 
 
 
 
 
 
114
 
115
+ # If prefill failed (e.g., missing application_id), return the initial_data
116
+ if data.get("error"):
117
+ self.logger.warning(f"Prefill failed, falling back to initial data for App ID {application_id}")
118
+ return initial_data
119
 
120
+ # Ensure 'application_id' is preserved
121
+ data["application_id"] = application_id
122
+ return data
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
123
 
124
  def build_document(self, pdf: FPDF):
125
  """
 
242
  pdf.ln(15) # Space for signature
243
  pdf.cell(0, 5, sponsor_name)
244
  pdf.ln(5)
245
+ pdf.cell(0, 5, sponsor_phone)
246
+
247
+ return pdf
server.py CHANGED
@@ -36,9 +36,67 @@ def create_app() -> Flask:
36
  logging.exception(f"Could not load secrets from SSM: {e}")
37
  print("ENV VARS:", dict(os.environ))
38
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
39
  WORKER_API_KEY = os.getenv("WORKER_API_KEY")
40
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
41
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
42
  @app.route('/scrape', methods=['POST'])
43
  async def scrape():
44
  try:
@@ -102,28 +160,6 @@ def create_app() -> Flask:
102
  traceback.print_exc()
103
  return jsonify({"error": str(e)}), 500
104
 
105
- # @app.route('/visa-checker', methods=['POST'])
106
- # async def visa_checker():
107
- # raw = request.get_data(as_text=True)
108
- # body = json.loads(raw)
109
- # source = body.get('source')
110
- # logging.info(f"Received visa check request: {body}")
111
- # destination = body.get('destination')
112
- # async with PassportIndexVisaScraper(debug=True) as scraper:
113
- # if not await scraper.initialize_session():
114
- # return jsonify({"error": "Failed to initialize session"}), 500
115
- # result = await scraper.check_visa_requirement_browser(source, destination)
116
-
117
- # if result:
118
- # return jsonify(result), 200
119
- # else:
120
- # result = await scraper.check_visa_interactive(source, destination)
121
- # if result:
122
- # return jsonify(result), 200
123
- # else:
124
- # return jsonify({"error": "Failed to retrieve visa information"}), 404
125
- # return jsonify({"error": "Unexpected error"}), 500
126
-
127
  @app.route('/generate/<visa_type>', methods=['POST'])
128
  def generate_visa_letter(visa_type):
129
  """
@@ -139,34 +175,10 @@ def create_app() -> Flask:
139
  if not data:
140
  return jsonify({"error": "No JSON payload provided"}), 400
141
 
142
- # Map visa types to their module and class
143
- generator_map = {
144
- "japan-multientry-tourist": (
145
- "app.util.japan_multientry_visa_letter_generator",
146
- "JapanMultiEntryVisaLetterGenerator",
147
- ),
148
- "schengen": (
149
- "app.util.schengen_visa_letter_generator",
150
- "SchengenVisaLetterGenerator",
151
- ),
152
- "sponsorship": (
153
- "app.util.sponsorship_letter_generator",
154
- "SponsorshipLetterGenerator"
155
- ),
156
- "housewife-statement": (
157
- "app.util.housewife_statement_letter_generator",
158
- "HousewifeStatementLetterGenerator"
159
- ),
160
- "passport-collection": (
161
- "app.util.passport_collection_letter_generator",
162
- "PassportCollectionLetterGenerator"
163
- )
164
- }
165
-
166
- if visa_type.lower() not in generator_map:
167
  return jsonify({"error": f"Unsupported visa type: {visa_type}"}), 400
168
 
169
- module_path, class_name = generator_map[visa_type.lower()]
170
 
171
  module = importlib.import_module(module_path)
172
  generator_class = getattr(module, class_name)
 
36
  logging.exception(f"Could not load secrets from SSM: {e}")
37
  print("ENV VARS:", dict(os.environ))
38
 
39
+ GENERATOR_MAP = {
40
+ "japan-multientry-tourist": (
41
+ "app.util.japan_multientry_visa_letter_generator",
42
+ "JapanMultiEntryVisaLetterGenerator",
43
+ ),
44
+ "schengen": (
45
+ "app.util.schengen_visa_letter_generator",
46
+ "SchengenVisaLetterGenerator",
47
+ ),
48
+ "sponsorship": (
49
+ "app.util.sponsorship_letter_generator",
50
+ "SponsorshipLetterGenerator"
51
+ ),
52
+ "housewife-statement": (
53
+ "app.util.housewife_statement_letter_generator",
54
+ "HousewifeStatementLetterGenerator"
55
+ ),
56
+ "passport-collection": (
57
+ "app.util.passport_collection_letter_generator",
58
+ "PassportCollectionLetterGenerator"
59
+ )
60
+ }
61
+
62
  WORKER_API_KEY = os.getenv("WORKER_API_KEY")
63
 
64
+ @app.route('/prefill/<visa_type>/<int:application_id>', methods=['GET'])
65
+ def get_prefill_data(visa_type, application_id):
66
+ """
67
+ Dynamically fetches and merges data for a specific visa type
68
+ to pre-fill the frontend form.
69
+ """
70
+ try:
71
+ if visa_type.lower() not in GENERATOR_MAP:
72
+ return jsonify({"error": f"Unsupported visa type: {visa_type}"}), 400
73
+
74
+ module_path, class_name = GENERATOR_MAP[visa_type.lower()]
75
+
76
+ module = importlib.import_module(module_path)
77
+ generator_class = getattr(module, class_name)
78
+
79
+ # Instantiate the generator, passing the application_id
80
+ letter_generator = generator_class(data={"application_id": application_id})
81
+
82
+ # Call the new public method to fetch and merge data
83
+ # This method MUST exist on the generator class (see step 2)
84
+ prefill_data = letter_generator.get_prefill_data()
85
 
86
+ if not prefill_data:
87
+ return jsonify({"error": "Failed to prepare data or no data found."}), 500
88
+
89
+ return jsonify(prefill_data), 200
90
+
91
+ except AttributeError as e:
92
+ # This error happens if the class doesn't have "get_prefill_data"
93
+ msg = f"Generator {class_name} does not implement 'get_prefill_data'. {e}"
94
+ logging.error(msg)
95
+ return jsonify({"error": msg}), 501 # 501 Not Implemented
96
+ except Exception as e:
97
+ logging.error(f"Error in /prefill/{visa_type}/{application_id}: {e}", exc_info=True)
98
+ return jsonify({"error": str(e)}), 500
99
+
100
  @app.route('/scrape', methods=['POST'])
101
  async def scrape():
102
  try:
 
160
  traceback.print_exc()
161
  return jsonify({"error": str(e)}), 500
162
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
163
  @app.route('/generate/<visa_type>', methods=['POST'])
164
  def generate_visa_letter(visa_type):
165
  """
 
175
  if not data:
176
  return jsonify({"error": "No JSON payload provided"}), 400
177
 
178
+ if visa_type.lower() not in GENERATOR_MAP:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
179
  return jsonify({"error": f"Unsupported visa type: {visa_type}"}), 400
180
 
181
+ module_path, class_name = GENERATOR_MAP[visa_type.lower()]
182
 
183
  module = importlib.import_module(module_path)
184
  generator_class = getattr(module, class_name)