Spaces:
Sleeping
Sleeping
Commit ·
fd85f3e
1
Parent(s): 0a55474
feat: schengen
Browse files
app/util/{JapanMultiEntryvisaLetterGenerator.py → japan_multientry_visa_letter_generator.py}
RENAMED
|
@@ -1,6 +1,6 @@
|
|
| 1 |
import datetime
|
| 2 |
from fpdf import FPDF
|
| 3 |
-
from .
|
| 4 |
|
| 5 |
class JapanMultiEntryVisaLetterGenerator(PDFDocumentGenerator):
|
| 6 |
"""
|
|
@@ -26,7 +26,6 @@ class JapanMultiEntryVisaLetterGenerator(PDFDocumentGenerator):
|
|
| 26 |
formatted_date = today.strftime("%d, %B, %Y")
|
| 27 |
|
| 28 |
pdf.set_font('Arial', '', 11)
|
| 29 |
-
pdf.set_margins(25, 25)
|
| 30 |
pdf.set_y(25)
|
| 31 |
pdf.set_left_margin(25)
|
| 32 |
|
|
|
|
| 1 |
import datetime
|
| 2 |
from fpdf import FPDF
|
| 3 |
+
from .pdf_document_generator import PDFDocumentGenerator # The fpdf2-based one
|
| 4 |
|
| 5 |
class JapanMultiEntryVisaLetterGenerator(PDFDocumentGenerator):
|
| 6 |
"""
|
|
|
|
| 26 |
formatted_date = today.strftime("%d, %B, %Y")
|
| 27 |
|
| 28 |
pdf.set_font('Arial', '', 11)
|
|
|
|
| 29 |
pdf.set_y(25)
|
| 30 |
pdf.set_left_margin(25)
|
| 31 |
|
app/util/{PDFDocumentGenerator.py → pdf_document_generator.py}
RENAMED
|
@@ -17,6 +17,7 @@ class PDFDocumentGenerator(ABC):
|
|
| 17 |
"""
|
| 18 |
self.data = data
|
| 19 |
|
|
|
|
| 20 |
@abstractmethod
|
| 21 |
def build_document(self, pdf: FPDF):
|
| 22 |
"""
|
|
@@ -37,24 +38,20 @@ class PDFDocumentGenerator(ABC):
|
|
| 37 |
On failure, (None, error_message).
|
| 38 |
"""
|
| 39 |
try:
|
| 40 |
-
# 1. Initialize the FPDF object
|
| 41 |
pdf = FPDF()
|
| 42 |
pdf.add_page()
|
| 43 |
|
| 44 |
-
# 2. Let the subclass build the document
|
| 45 |
self.build_document(pdf)
|
| 46 |
|
| 47 |
-
# 3. Define file path and save the document
|
| 48 |
pdf_filename = "document.pdf"
|
| 49 |
pdf_filepath = os.path.join(tempdir, pdf_filename)
|
| 50 |
|
| 51 |
pdf.output(pdf_filepath)
|
| 52 |
|
| 53 |
-
# 4. Check if PDF was successfully created
|
| 54 |
if not os.path.exists(pdf_filepath):
|
| 55 |
return None, "PDF file was not created by fpdf2."
|
| 56 |
|
| 57 |
-
|
| 58 |
return pdf_filepath, None
|
| 59 |
|
| 60 |
except Exception as e:
|
|
|
|
| 17 |
"""
|
| 18 |
self.data = data
|
| 19 |
|
| 20 |
+
|
| 21 |
@abstractmethod
|
| 22 |
def build_document(self, pdf: FPDF):
|
| 23 |
"""
|
|
|
|
| 38 |
On failure, (None, error_message).
|
| 39 |
"""
|
| 40 |
try:
|
|
|
|
| 41 |
pdf = FPDF()
|
| 42 |
pdf.add_page()
|
| 43 |
|
|
|
|
| 44 |
self.build_document(pdf)
|
| 45 |
|
|
|
|
| 46 |
pdf_filename = "document.pdf"
|
| 47 |
pdf_filepath = os.path.join(tempdir, pdf_filename)
|
| 48 |
|
| 49 |
pdf.output(pdf_filepath)
|
| 50 |
|
|
|
|
| 51 |
if not os.path.exists(pdf_filepath):
|
| 52 |
return None, "PDF file was not created by fpdf2."
|
| 53 |
|
| 54 |
+
pdf.set_margins(25, 25)
|
| 55 |
return pdf_filepath, None
|
| 56 |
|
| 57 |
except Exception as e:
|
app/util/schengen_visa_letter_generator.py
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import datetime
|
| 2 |
+
from fpdf import FPDF
|
| 3 |
+
from .pdf_document_generator import PDFDocumentGenerator
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
class SchengenVisaLetterGenerator(PDFDocumentGenerator):
|
| 7 |
+
"""
|
| 8 |
+
Generates a Schengen Visa Cover Letter PDF (Individual or Group)
|
| 9 |
+
using the shared PDFDocumentGenerator base for layout consistency.
|
| 10 |
+
"""
|
| 11 |
+
|
| 12 |
+
def __init__(self, data: dict, trip_type: str = "individual"):
|
| 13 |
+
super().__init__(data)
|
| 14 |
+
self.trip_type = trip_type.lower()
|
| 15 |
+
|
| 16 |
+
def build_document(self, pdf):
|
| 17 |
+
"""
|
| 18 |
+
Builds the formatted PDF content.
|
| 19 |
+
This method is called by the base class `compile_pdf()`.
|
| 20 |
+
"""
|
| 21 |
+
personal = self.data.get("personal_details", {})
|
| 22 |
+
members = self.data.get("group_members", [])
|
| 23 |
+
|
| 24 |
+
# --- Extract fields ---
|
| 25 |
+
city = self.data.get("city", "Jakarta")
|
| 26 |
+
date = self.data.get("date", "18th July 2024")
|
| 27 |
+
country = self.data.get("country", "[Schengen Country Name]")
|
| 28 |
+
purpose = self.data.get("purpose", "[tourism/family visit]")
|
| 29 |
+
main_dest = self.data.get("main_destination", "[country]")
|
| 30 |
+
event = self.data.get("event", "[event/activities]")
|
| 31 |
+
other_dest = self.data.get("other_destinations", "")
|
| 32 |
+
start = self.data.get("travel_start", "[start date]")
|
| 33 |
+
end = self.data.get("travel_end", "[end date]")
|
| 34 |
+
duration = self.data.get("duration", "[X days and Y nights]")
|
| 35 |
+
trip_highlight = self.data.get("trip_highlight", "[trip highlight]")
|
| 36 |
+
contact_email = self.data.get("contact_email", "[email]")
|
| 37 |
+
contact_phone = self.data.get("contact_phone", "[phone number]")
|
| 38 |
+
job_commitment = self.data.get("job_commitment", "[your job title or role]")
|
| 39 |
+
financial_status = self.data.get("financial_status", "sound")
|
| 40 |
+
|
| 41 |
+
pdf.set_font("Times", "B", 14)
|
| 42 |
+
# title = (
|
| 43 |
+
# "COVER LETTER FOR GROUP TRIP"
|
| 44 |
+
# if self.trip_type == "group"
|
| 45 |
+
# else "COVER LETTER FOR INDIVIDUAL TRIP"
|
| 46 |
+
# )
|
| 47 |
+
# pdf.cell(0, 10, title, ln=True, align="C")
|
| 48 |
+
# pdf.ln(10)
|
| 49 |
+
|
| 50 |
+
pdf.set_font("Times", "", 12)
|
| 51 |
+
pdf.multi_cell(0, 8, f"{city}, {date}")
|
| 52 |
+
pdf.multi_cell(0, 8, f"To: Embassy of {country} Jakarta")
|
| 53 |
+
pdf.multi_cell(0, 8, "Subject: Application for Schengen Visa")
|
| 54 |
+
pdf.ln(6)
|
| 55 |
+
|
| 56 |
+
# --- Opening Paragraph ---
|
| 57 |
+
body = (
|
| 58 |
+
f"Dear Sir/Madam,\n\n"
|
| 59 |
+
f"I am writing to apply for a Schengen {purpose} visa for the purpose of visiting {main_dest} on {event}. "
|
| 60 |
+
)
|
| 61 |
+
if other_dest:
|
| 62 |
+
body += f"Following this, I plan to continue my journey to {other_dest}. "
|
| 63 |
+
body += (
|
| 64 |
+
f"My travel period is from {start} to {end}, covering a total of {duration}."
|
| 65 |
+
)
|
| 66 |
+
pdf.multi_cell(0, 8, body)
|
| 67 |
+
pdf.ln(8)
|
| 68 |
+
|
| 69 |
+
# --- Personal Details ---
|
| 70 |
+
pdf.set_font("Times", "B", 12)
|
| 71 |
+
pdf.cell(0, 10, "Personal Details:", ln=True)
|
| 72 |
+
pdf.set_font("Times", "", 12)
|
| 73 |
+
pdf.multi_cell(
|
| 74 |
+
0,
|
| 75 |
+
8,
|
| 76 |
+
f"Name\t\t: {personal.get('name', '')}\n"
|
| 77 |
+
f"Date of Birth\t: {personal.get('dob', '')}\n"
|
| 78 |
+
f"Nationality\t: {personal.get('nationality', '')}\n"
|
| 79 |
+
f"Occupation\t: {personal.get('occupation', '')}\n"
|
| 80 |
+
f"Passport Number\t: {personal.get('passport_number', '')}",
|
| 81 |
+
)
|
| 82 |
+
pdf.ln(6)
|
| 83 |
+
|
| 84 |
+
# --- Group Section ---
|
| 85 |
+
if self.trip_type == "group" and members:
|
| 86 |
+
pdf.multi_cell(0, 8, f"I will be attending the {event} with my family members:\n")
|
| 87 |
+
for m in members:
|
| 88 |
+
pdf.multi_cell(
|
| 89 |
+
0,
|
| 90 |
+
8,
|
| 91 |
+
f"Relationship\t: {m.get('relationship', '')}\n"
|
| 92 |
+
f"Name\t\t: {m.get('name', '')}\n"
|
| 93 |
+
f"Date of Birth\t: {m.get('dob', '')}\n"
|
| 94 |
+
f"Occupation\t: {m.get('occupation', '')}\n"
|
| 95 |
+
f"Nationality\t: {m.get('nationality', '')}\n"
|
| 96 |
+
f"Passport Number\t: {m.get('passport_number', '')}\n",
|
| 97 |
+
)
|
| 98 |
+
pdf.ln(2)
|
| 99 |
+
pdf.multi_cell(
|
| 100 |
+
0,
|
| 101 |
+
8,
|
| 102 |
+
f"We will travel together to {trip_highlight}, with a detailed itinerary attached.\n",
|
| 103 |
+
)
|
| 104 |
+
pdf.ln(6)
|
| 105 |
+
|
| 106 |
+
# --- Supporting Documents ---
|
| 107 |
+
pdf.set_font("Times", "B", 12)
|
| 108 |
+
pdf.cell(0, 10, "We will also provide the following supporting documents:", ln=True)
|
| 109 |
+
pdf.set_font("Times", "", 12)
|
| 110 |
+
pdf.multi_cell(
|
| 111 |
+
0,
|
| 112 |
+
8,
|
| 113 |
+
"- Completed visa application form\n"
|
| 114 |
+
"- Recent photographs\n"
|
| 115 |
+
"- Copy of passport\n"
|
| 116 |
+
"- Proof of travel insurance\n"
|
| 117 |
+
"- Round-trip flight itinerary\n"
|
| 118 |
+
"- Hotel accommodation reservation\n"
|
| 119 |
+
"- Bank statements\n"
|
| 120 |
+
"- Statement letter and proof of current employment\n"
|
| 121 |
+
"- Copy of family documents or marriage certificate\n"
|
| 122 |
+
"- Copy of passport",
|
| 123 |
+
)
|
| 124 |
+
pdf.ln(6)
|
| 125 |
+
|
| 126 |
+
# --- Commitment & Return ---
|
| 127 |
+
pdf.multi_cell(
|
| 128 |
+
0,
|
| 129 |
+
8,
|
| 130 |
+
f"Commitment and Return: I assure you that I/we will adhere to all Schengen visa regulations and return to Indonesia as planned. "
|
| 131 |
+
f"I am a {job_commitment}, committed to my duties in Indonesia. "
|
| 132 |
+
f"My financial status is {financial_status}, and sufficient proof of funds is attached to cover all expenses for the trip. "
|
| 133 |
+
f"I guarantee our prompt return upon completion of the journey.\n\n"
|
| 134 |
+
f"We would be grateful if you could consider our application favorably and issue the necessary visa for the trip. "
|
| 135 |
+
f"Should you require any further details, please contact me at {contact_email} or {contact_phone}.",
|
| 136 |
+
)
|
| 137 |
+
pdf.ln(8)
|
| 138 |
+
|
| 139 |
+
# --- Closing ---
|
| 140 |
+
pdf.multi_cell(0, 8, "Thank you for your time and kind attention.\n\nBest regards,\n\n")
|
| 141 |
+
pdf.multi_cell(0, 8, personal.get("name", ""))
|
| 142 |
+
|
| 143 |
+
return pdf
|
server.py
CHANGED
|
@@ -8,12 +8,12 @@ from dotenv import load_dotenv
|
|
| 8 |
import json
|
| 9 |
import requests
|
| 10 |
import uuid
|
| 11 |
-
import
|
| 12 |
import io
|
| 13 |
|
| 14 |
from app.util.gen_ai_base import GenAIBaseClient
|
| 15 |
from app.util.browser_agent import BrowserAgent
|
| 16 |
-
from app.util.
|
| 17 |
import sys
|
| 18 |
sys.stdout.reconfigure(line_buffering=True)
|
| 19 |
API = "https://api-dev.spun.global"
|
|
@@ -112,25 +112,43 @@ def create_app() -> Flask:
|
|
| 112 |
# else:
|
| 113 |
# return jsonify({"error": "Failed to retrieve visa information"}), 404
|
| 114 |
# return jsonify({"error": "Unexpected error"}), 500
|
| 115 |
-
|
| 116 |
-
|
|
|
|
| 117 |
"""
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
"address": "Your Address Line 1",
|
| 123 |
-
"city_postal": "City, Postal Code",
|
| 124 |
-
"email": "your.email@example.com",
|
| 125 |
-
"phone": "+123456789"
|
| 126 |
-
}
|
| 127 |
"""
|
| 128 |
try:
|
| 129 |
data = request.get_json()
|
| 130 |
if not data:
|
| 131 |
return jsonify({"error": "No JSON payload provided"}), 400
|
| 132 |
|
| 133 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 134 |
|
| 135 |
with tempfile.TemporaryDirectory() as tempdir:
|
| 136 |
pdf_filepath, error_msg = letter_generator.compile_pdf(tempdir)
|
|
@@ -147,11 +165,11 @@ def create_app() -> Flask:
|
|
| 147 |
pdf_bytes,
|
| 148 |
mimetype="application/pdf",
|
| 149 |
as_attachment=True,
|
| 150 |
-
download_name="
|
| 151 |
)
|
| 152 |
|
| 153 |
except Exception as e:
|
| 154 |
-
print(f"Error in
|
| 155 |
return jsonify({"error": str(e)}), 500
|
| 156 |
|
| 157 |
@app.route('/', methods=['GET'])
|
|
|
|
| 8 |
import json
|
| 9 |
import requests
|
| 10 |
import uuid
|
| 11 |
+
import importlib
|
| 12 |
import io
|
| 13 |
|
| 14 |
from app.util.gen_ai_base import GenAIBaseClient
|
| 15 |
from app.util.browser_agent import BrowserAgent
|
| 16 |
+
from app.util.japan_multientry_visa_letter_generator import JapanMultiEntryVisaLetterGenerator
|
| 17 |
import sys
|
| 18 |
sys.stdout.reconfigure(line_buffering=True)
|
| 19 |
API = "https://api-dev.spun.global"
|
|
|
|
| 112 |
# else:
|
| 113 |
# return jsonify({"error": "Failed to retrieve visa information"}), 404
|
| 114 |
# return jsonify({"error": "Unexpected error"}), 500
|
| 115 |
+
|
| 116 |
+
@app.route('/generate/<visa_type>', methods=['POST'])
|
| 117 |
+
def generate_visa_letter(visa_type):
|
| 118 |
"""
|
| 119 |
+
Dynamically generates visa letters based on <visa_type>.
|
| 120 |
+
Example:
|
| 121 |
+
POST /generate/schengen
|
| 122 |
+
POST /generate/japan-multientry-tourist
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 123 |
"""
|
| 124 |
try:
|
| 125 |
data = request.get_json()
|
| 126 |
if not data:
|
| 127 |
return jsonify({"error": "No JSON payload provided"}), 400
|
| 128 |
|
| 129 |
+
# ✅ Map visa types to their module and class
|
| 130 |
+
generator_map = {
|
| 131 |
+
"japan-multientry-tourist": (
|
| 132 |
+
"app.util.japan_multientry_visa_letter_generator",
|
| 133 |
+
"JapanMultiEntryVisaLetterGenerator",
|
| 134 |
+
),
|
| 135 |
+
"schengen": (
|
| 136 |
+
"app.util.schengen_visa_letter_generator",
|
| 137 |
+
"SchengenVisaLetterGenerator",
|
| 138 |
+
),
|
| 139 |
+
}
|
| 140 |
+
|
| 141 |
+
if visa_type.lower() not in generator_map:
|
| 142 |
+
return jsonify({"error": f"Unsupported visa type: {visa_type}"}), 400
|
| 143 |
+
|
| 144 |
+
module_path, class_name = generator_map[visa_type.lower()]
|
| 145 |
+
|
| 146 |
+
# ✅ Import dynamically using importlib
|
| 147 |
+
module = importlib.import_module(module_path)
|
| 148 |
+
generator_class = getattr(module, class_name)
|
| 149 |
+
|
| 150 |
+
# ✅ Generate the PDF
|
| 151 |
+
letter_generator = generator_class(data)
|
| 152 |
|
| 153 |
with tempfile.TemporaryDirectory() as tempdir:
|
| 154 |
pdf_filepath, error_msg = letter_generator.compile_pdf(tempdir)
|
|
|
|
| 165 |
pdf_bytes,
|
| 166 |
mimetype="application/pdf",
|
| 167 |
as_attachment=True,
|
| 168 |
+
download_name=f"{visa_type.replace('-', '_')}_visa_letter.pdf",
|
| 169 |
)
|
| 170 |
|
| 171 |
except Exception as e:
|
| 172 |
+
print(f"Error in /generate/{visa_type}: {e}")
|
| 173 |
return jsonify({"error": str(e)}), 500
|
| 174 |
|
| 175 |
@app.route('/', methods=['GET'])
|