Spaces:
Sleeping
Sleeping
Commit ·
cce8d96
1
Parent(s): 12a4071
feat; add prefill
Browse files- app/util/housewife_statement_letter_generator.py +110 -75
- app/util/japan_multientry_visa_letter_generator.py +91 -55
- app/util/passport_collection_letter_generator.py +106 -67
- app/util/pdf_document_generator.py +14 -1
- app/util/schengen_visa_letter_generator.py +119 -126
- app/util/sponsorship_letter_generator.py +100 -65
- server.py +60 -48
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
|
| 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
|
| 27 |
-
|
| 28 |
"""
|
| 29 |
-
|
| 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 |
-
#
|
|
|
|
|
|
|
|
|
|
| 47 |
|
| 48 |
-
#
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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.
|
| 15 |
-
2.
|
|
|
|
| 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 |
-
|
| 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 |
-
#
|
| 43 |
-
|
|
|
|
| 44 |
|
| 45 |
-
#
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
|
|
|
| 57 |
|
| 58 |
-
#
|
| 59 |
-
|
| 60 |
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 73 |
|
| 74 |
def build_document(self, pdf: FPDF):
|
| 75 |
"""
|
|
@@ -78,12 +113,12 @@ class JapanMultiEntryVisaLetterGenerator(PDFDocumentGenerator):
|
|
| 78 |
"""
|
| 79 |
|
| 80 |
# --- 1. Data Preparation ---
|
| 81 |
-
#
|
| 82 |
prepared_data = self.data.copy() if self.data else {}
|
| 83 |
|
| 84 |
if prepared_data.get("application_id"):
|
| 85 |
try:
|
| 86 |
-
#
|
| 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 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 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
|
| 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
|
| 25 |
-
|
| 26 |
"""
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 87 |
"""
|
| 88 |
-
Fetches all data from form, smart_upload, and profile
|
| 89 |
-
|
| 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 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 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 |
-
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 153 |
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
|
| 161 |
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 167 |
|
| 168 |
-
|
| 169 |
-
form_data.get("
|
| 170 |
-
|
| 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 |
-
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 185 |
|
| 186 |
-
|
| 187 |
-
|
| 188 |
-
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
|
| 193 |
-
|
| 194 |
-
|
| 195 |
-
|
| 196 |
-
|
| 197 |
-
|
| 198 |
-
|
| 199 |
-
|
| 200 |
-
|
| 201 |
-
|
| 202 |
-
|
| 203 |
-
|
| 204 |
-
|
| 205 |
-
|
| 206 |
-
|
| 207 |
-
|
| 208 |
-
|
| 209 |
-
|
| 210 |
-
|
| 211 |
-
|
| 212 |
-
|
| 213 |
-
|
| 214 |
-
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 25 |
-
|
| 26 |
"""
|
| 27 |
-
|
| 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 |
-
#
|
| 42 |
-
|
|
|
|
|
|
|
| 43 |
|
| 44 |
-
#
|
| 45 |
-
|
| 46 |
-
|
| 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 |
-
|
| 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 =
|
| 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)
|