omgy commited on
Commit
f884b40
·
verified ·
1 Parent(s): eb16b01

Update app/services/pdf_service.py

Browse files
Files changed (1) hide show
  1. app/services/pdf_service.py +267 -323
app/services/pdf_service.py CHANGED
@@ -1,323 +1,267 @@
1
- """
2
- PDF service for generating loan sanction letters.
3
- Uses ReportLab to create professional PDF documents.
4
- """
5
-
6
- import os
7
- from datetime import datetime, timedelta
8
- from typing import Any, Dict
9
-
10
- from app.config import settings
11
- from app.utils.logger import default_logger as logger
12
- from reportlab.lib import colors
13
- from reportlab.lib.pagesizes import A4, letter
14
- from reportlab.lib.styles import ParagraphStyle, getSampleStyleSheet
15
- from reportlab.lib.units import inch
16
- from reportlab.platypus import (
17
- Paragraph,
18
- SimpleDocTemplate,
19
- Spacer,
20
- Table,
21
- TableStyle,
22
- )
23
-
24
-
25
- class PdfService:
26
- """Service for generating loan sanction letter PDFs."""
27
-
28
- def __init__(self):
29
- """Initialize PDF service with output directory."""
30
- self.output_dir = settings.PDF_OUTPUT_DIR
31
- self.validity_days = settings.PDF_VALIDITY_DAYS
32
-
33
- # Create output directory if it doesn't exist
34
- os.makedirs(self.output_dir, exist_ok=True)
35
-
36
- def generate_sanction_letter(self, loan_data: Dict[str, Any]) -> Dict[str, str]:
37
- """
38
- Generate a professional sanction letter PDF.
39
-
40
- Args:
41
- loan_data: Loan application data with all details
42
-
43
- Returns:
44
- Dictionary with pdf_path and pdf_url
45
- """
46
- try:
47
- loan_id = loan_data.get("loan_id", "unknown")
48
- filename = f"{loan_id}.pdf"
49
- filepath = os.path.join(self.output_dir, filename)
50
-
51
- # Create PDF document
52
- doc = SimpleDocTemplate(
53
- filepath,
54
- pagesize=A4,
55
- rightMargin=0.75 * inch,
56
- leftMargin=0.75 * inch,
57
- topMargin=1 * inch,
58
- bottomMargin=0.75 * inch,
59
- )
60
-
61
- # Build content
62
- elements = []
63
- styles = getSampleStyleSheet()
64
-
65
- # Custom styles
66
- title_style = ParagraphStyle(
67
- "CustomTitle",
68
- parent=styles["Heading1"],
69
- fontSize=20,
70
- textColor=colors.HexColor("#10b981"),
71
- spaceAfter=30,
72
- alignment=1, # Center
73
- fontName="Helvetica-Bold",
74
- )
75
-
76
- heading_style = ParagraphStyle(
77
- "CustomHeading",
78
- parent=styles["Heading2"],
79
- fontSize=14,
80
- textColor=colors.HexColor("#1f2937"),
81
- spaceAfter=12,
82
- fontName="Helvetica-Bold",
83
- )
84
-
85
- normal_style = ParagraphStyle(
86
- "CustomNormal",
87
- parent=styles["Normal"],
88
- fontSize=11,
89
- textColor=colors.HexColor("#374151"),
90
- spaceAfter=8,
91
- )
92
-
93
- # Header
94
- elements.append(Paragraph("FinAgent", title_style))
95
- elements.append(Paragraph("Personal Loan Sanction Letter", heading_style))
96
- elements.append(Spacer(1, 0.2 * inch))
97
-
98
- # Reference details
99
- sanction_date = datetime.utcnow()
100
- validity_date = sanction_date + timedelta(days=self.validity_days)
101
-
102
- ref_data = [
103
- ["Sanction Reference No:", loan_id],
104
- ["Sanction Date:", sanction_date.strftime("%B %d, %Y")],
105
- [
106
- "Validity Date:",
107
- validity_date.strftime("%B %d, %Y")
108
- + f" ({self.validity_days} days)",
109
- ],
110
- ]
111
-
112
- ref_table = Table(ref_data, colWidths=[2.5 * inch, 3.5 * inch])
113
- ref_table.setStyle(
114
- TableStyle(
115
- [
116
- ("FONTNAME", (0, 0), (0, -1), "Helvetica-Bold"),
117
- ("FONTNAME", (1, 0), (1, -1), "Helvetica"),
118
- ("FONTSIZE", (0, 0), (-1, -1), 10),
119
- ("TEXTCOLOR", (0, 0), (-1, -1), colors.HexColor("#374151")),
120
- ("VALIGN", (0, 0), (-1, -1), "MIDDLE"),
121
- ("BOTTOMPADDING", (0, 0), (-1, -1), 8),
122
- ]
123
- )
124
- )
125
- elements.append(ref_table)
126
- elements.append(Spacer(1, 0.3 * inch))
127
-
128
- # Applicant details
129
- elements.append(Paragraph("Applicant Details", heading_style))
130
-
131
- user_id = loan_data.get("user_id", "N/A")
132
- full_name = loan_data.get("full_name", "Valued Customer")
133
-
134
- applicant_data = [
135
- ["Applicant Name:", full_name],
136
- ["Customer ID:", user_id],
137
- ]
138
-
139
- applicant_table = Table(applicant_data, colWidths=[2.5 * inch, 3.5 * inch])
140
- applicant_table.setStyle(
141
- TableStyle(
142
- [
143
- ("FONTNAME", (0, 0), (0, -1), "Helvetica-Bold"),
144
- ("FONTNAME", (1, 0), (1, -1), "Helvetica"),
145
- ("FONTSIZE", (0, 0), (-1, -1), 10),
146
- ("TEXTCOLOR", (0, 0), (-1, -1), colors.HexColor("#374151")),
147
- ("VALIGN", (0, 0), (-1, -1), "MIDDLE"),
148
- ("BOTTOMPADDING", (0, 0), (-1, -1), 8),
149
- ]
150
- )
151
- )
152
- elements.append(applicant_table)
153
- elements.append(Spacer(1, 0.3 * inch))
154
-
155
- # Loan details
156
- elements.append(Paragraph("Loan Sanction Details", heading_style))
157
-
158
- approved_amount = loan_data.get("approved_amount", 0)
159
- tenure = loan_data.get("tenure_months", 0)
160
- emi = loan_data.get("emi", 0)
161
- interest_rate = loan_data.get("interest_rate", 0)
162
- total_payable = loan_data.get("total_payable", emi * tenure)
163
- processing_fee = loan_data.get("processing_fee", approved_amount * 0.02)
164
-
165
- loan_details_data = [
166
- ["Sanctioned Amount:", f"₹ {approved_amount:,.2f}"],
167
- [
168
- "Tenure:",
169
- f"{tenure} months ({tenure // 12} years {tenure % 12} months)",
170
- ],
171
- ["Interest Rate:", f"{interest_rate}% per annum"],
172
- ["Monthly EMI:", f"₹ {emi:,.2f}"],
173
- ["Total Amount Payable:", f" {total_payable:,.2f}"],
174
- ["Processing Fee (2%):", f"{processing_fee:,.2f}"],
175
- ["Risk Band:", loan_data.get("risk_band", "N/A")],
176
- ]
177
-
178
- loan_table = Table(loan_details_data, colWidths=[2.5 * inch, 3.5 * inch])
179
- loan_table.setStyle(
180
- TableStyle(
181
- [
182
- ("FONTNAME", (0, 0), (0, -1), "Helvetica-Bold"),
183
- ("FONTNAME", (1, 0), (1, -1), "Helvetica"),
184
- ("FONTSIZE", (0, 0), (-1, -1), 10),
185
- ("TEXTCOLOR", (0, 0), (-1, -1), colors.HexColor("#374151")),
186
- ("VALIGN", (0, 0), (-1, -1), "MIDDLE"),
187
- ("BOTTOMPADDING", (0, 0), (-1, -1), 8),
188
- ("BACKGROUND", (0, -2), (-1, -2), colors.HexColor("#f3f4f6")),
189
- ]
190
- )
191
- )
192
- elements.append(loan_table)
193
- elements.append(Spacer(1, 0.3 * inch))
194
-
195
- # Terms and conditions
196
- elements.append(Paragraph("Terms & Conditions", heading_style))
197
-
198
- terms = [
199
- "This sanction is valid for {} days from the date of issue.".format(
200
- self.validity_days
201
- ),
202
- "The loan is subject to verification of all documents submitted.",
203
- "Processing fee is non-refundable and payable upfront.",
204
- "EMI will be deducted on the same date every month.",
205
- "Prepayment charges may apply as per loan agreement.",
206
- "Interest rate is fixed for the entire tenure of the loan.",
207
- "This is a system-generated sanction letter and is valid without signature.",
208
- ]
209
-
210
- for i, term in enumerate(terms, 1):
211
- term_text = f"{i}. {term}"
212
- elements.append(Paragraph(term_text, normal_style))
213
-
214
- elements.append(Spacer(1, 0.3 * inch))
215
-
216
- # Next steps
217
- elements.append(Paragraph("Next Steps", heading_style))
218
- next_steps_text = """
219
- Please submit the following documents to complete your loan processing:
220
- <br/>• PAN Card<br/>
221
- Aadhaar Card<br/>
222
- Last 3 months salary slips<br/>
223
- • Last 6 months bank statements<br/>
224
- • Address proof<br/>
225
- <br/>
226
- Our loan officer will contact you within 2 business days to guide you through the documentation process.
227
- """
228
- elements.append(Paragraph(next_steps_text, normal_style))
229
- elements.append(Spacer(1, 0.3 * inch))
230
-
231
- # Footer
232
- footer_style = ParagraphStyle(
233
- "Footer",
234
- parent=styles["Normal"],
235
- fontSize=9,
236
- textColor=colors.HexColor("#6b7280"),
237
- alignment=1, # Center
238
- )
239
-
240
- elements.append(Spacer(1, 0.5 * inch))
241
- elements.append(
242
- Paragraph(
243
- "This is a system-generated document and does not require a signature.",
244
- footer_style,
245
- )
246
- )
247
- elements.append(
248
- Paragraph(
249
- "For queries, contact us at support@finagent.com | +91-1800-XXX-XXXX",
250
- footer_style,
251
- )
252
- )
253
- elements.append(
254
- Paragraph(
255
- f"Generated on {datetime.utcnow().strftime('%B %d, %Y at %H:%M UTC')}",
256
- footer_style,
257
- )
258
- )
259
-
260
- # Build PDF
261
- doc.build(elements)
262
-
263
- # Generate URL (in production, this would be a cloud storage URL)
264
- pdf_url = f"/api/loan/{loan_id}/sanction-pdf"
265
-
266
- logger.info(f"Generated sanction letter PDF: {filepath}")
267
-
268
- return {"pdf_path": filepath, "pdf_url": pdf_url}
269
-
270
- except Exception as e:
271
- logger.error(f"Error generating PDF: {str(e)}")
272
- raise
273
-
274
- def get_pdf_path(self, loan_id: str) -> str:
275
- """
276
- Get the file path for a sanction letter PDF.
277
-
278
- Args:
279
- loan_id: Loan application ID
280
-
281
- Returns:
282
- Full file path to the PDF
283
- """
284
- filename = f"{loan_id}.pdf"
285
- return os.path.join(self.output_dir, filename)
286
-
287
- def pdf_exists(self, loan_id: str) -> bool:
288
- """
289
- Check if a sanction letter PDF exists.
290
-
291
- Args:
292
- loan_id: Loan application ID
293
-
294
- Returns:
295
- True if PDF exists, False otherwise
296
- """
297
- filepath = self.get_pdf_path(loan_id)
298
- return os.path.exists(filepath)
299
-
300
- def delete_pdf(self, loan_id: str) -> bool:
301
- """
302
- Delete a sanction letter PDF.
303
-
304
- Args:
305
- loan_id: Loan application ID
306
-
307
- Returns:
308
- True if deleted successfully, False otherwise
309
- """
310
- try:
311
- filepath = self.get_pdf_path(loan_id)
312
- if os.path.exists(filepath):
313
- os.remove(filepath)
314
- logger.info(f"Deleted PDF: {filepath}")
315
- return True
316
- return False
317
- except Exception as e:
318
- logger.error(f"Error deleting PDF: {str(e)}")
319
- return False
320
-
321
-
322
- # Singleton instance
323
- pdf_service = PdfService()
 
1
+ """
2
+ PDF Service for generating professional sanction letters.
3
+ """
4
+
5
+ import os
6
+ from datetime import datetime, timedelta
7
+ from typing import Optional
8
+
9
+ from reportlab.lib import colors
10
+ from reportlab.lib.pagesizes import A4, letter
11
+ from reportlab.lib.styles import ParagraphStyle, getSampleStyleSheet
12
+ from reportlab.lib.units import inch
13
+ from reportlab.platypus import (
14
+ Paragraph,
15
+ SimpleDocTemplate,
16
+ Spacer,
17
+ Table,
18
+ TableStyle,
19
+ )
20
+
21
+ from app.config import settings
22
+ from app.utils.logger import setup_logger
23
+
24
+ logger = setup_logger("pdf_service")
25
+
26
+
27
+ class PDFService:
28
+ """Service for generating loan sanction letters."""
29
+
30
+ def __init__(self):
31
+ """Initialize PDF service."""
32
+ self.output_dir = settings.PDF_OUTPUT_DIR
33
+ os.makedirs(self.output_dir, exist_ok=True)
34
+
35
+ def generate_sanction_letter(
36
+ self,
37
+ loan_id: str,
38
+ user_name: str,
39
+ loan_amount: float,
40
+ tenure_months: int,
41
+ emi: float,
42
+ interest_rate: float,
43
+ risk_band: str = "A",
44
+ ) -> str:
45
+ """
46
+ Generate professional sanction letter PDF.
47
+
48
+ Args:
49
+ loan_id: Unique loan identifier
50
+ user_name: Borrower's full name
51
+ loan_amount: Approved loan amount
52
+ tenure_months: Loan tenure in months
53
+ emi: Monthly EMI amount
54
+ interest_rate: Annual interest rate
55
+ risk_band: Risk classification (A/B/C)
56
+
57
+ Returns:
58
+ Path to generated PDF file
59
+ """
60
+ try:
61
+ logger.info(f"Generating sanction letter for loan {loan_id}")
62
+
63
+ # Create filename
64
+ filename = f"sanction_{loan_id}_{datetime.now().strftime('%Y%m%d')}.pdf"
65
+ filepath = os.path.join(self.output_dir, filename)
66
+
67
+ # Create PDF document
68
+ doc = SimpleDocTemplate(
69
+ filepath,
70
+ pagesize=A4,
71
+ rightMargin=72,
72
+ leftMargin=72,
73
+ topMargin=72,
74
+ bottomMargin=72,
75
+ )
76
+
77
+ # Build PDF content
78
+ story = []
79
+ styles = getSampleStyleSheet()
80
+
81
+ # Custom styles
82
+ title_style = ParagraphStyle(
83
+ 'CustomTitle',
84
+ parent=styles['Heading1'],
85
+ fontSize=24,
86
+ textColor=colors.HexColor('#1a1a1a'),
87
+ spaceAfter=30,
88
+ alignment=1, # Center
89
+ fontName='Helvetica-Bold'
90
+ )
91
+
92
+ heading_style = ParagraphStyle(
93
+ 'CustomHeading',
94
+ parent=styles['Heading2'],
95
+ fontSize=14,
96
+ textColor=colors.HexColor('#2c5aa0'),
97
+ spaceAfter=12,
98
+ fontName='Helvetica-Bold'
99
+ )
100
+
101
+ body_style = ParagraphStyle(
102
+ 'CustomBody',
103
+ parent=styles['Normal'],
104
+ fontSize=11,
105
+ textColor=colors.HexColor('#333333'),
106
+ spaceAfter=12,
107
+ leading=16
108
+ )
109
+
110
+ # Header with logo placeholder
111
+ story.append(Spacer(1, 0.5 * inch))
112
+
113
+ # Company name/logo
114
+ company_name = Paragraph(
115
+ "<b>FinAgent</b><br/>Personal Loan Division",
116
+ ParagraphStyle('Company', parent=styles['Normal'], fontSize=16, textColor=colors.HexColor('#2c5aa0'), alignment=1)
117
+ )
118
+ story.append(company_name)
119
+ story.append(Spacer(1, 0.3 * inch))
120
+
121
+ # Title
122
+ title = Paragraph("LOAN SANCTION LETTER", title_style)
123
+ story.append(title)
124
+ story.append(Spacer(1, 0.3 * inch))
125
+
126
+ # Date and Reference
127
+ current_date = datetime.now().strftime("%B %d, %Y")
128
+ validity_date = (datetime.now() + timedelta(days=settings.PDF_VALIDITY_DAYS)).strftime("%B %d, %Y")
129
+
130
+ ref_data = [
131
+ ["Reference No:", loan_id],
132
+ ["Date:", current_date],
133
+ ["Valid Until:", validity_date],
134
+ ]
135
+
136
+ ref_table = Table(ref_data, colWidths=[2*inch, 4*inch])
137
+ ref_table.setStyle(TableStyle([
138
+ ('FONTNAME', (0, 0), (0, -1), 'Helvetica-Bold'),
139
+ ('FONTSIZE', (0, 0), (-1, -1), 10),
140
+ ('TEXTCOLOR', (0, 0), (-1, -1), colors.HexColor('#333333')),
141
+ ('ALIGN', (0, 0), (-1, -1), 'LEFT'),
142
+ ('VALIGN', (0, 0), (-1, -1), 'MIDDLE'),
143
+ ]))
144
+
145
+ story.append(ref_table)
146
+ story.append(Spacer(1, 0.4 * inch))
147
+
148
+ # Addressee
149
+ addressee = Paragraph(f"Dear <b>{user_name}</b>,", body_style)
150
+ story.append(addressee)
151
+ story.append(Spacer(1, 0.2 * inch))
152
+
153
+ # Congratulations message
154
+ congrats = Paragraph(
155
+ "We are pleased to inform you that your personal loan application has been <b>APPROVED</b>. "
156
+ "We congratulate you on this milestone and look forward to serving your financial needs.",
157
+ body_style
158
+ )
159
+ story.append(congrats)
160
+ story.append(Spacer(1, 0.3 * inch))
161
+
162
+ # Loan Details Section
163
+ heading = Paragraph("Loan Sanction Details", heading_style)
164
+ story.append(heading)
165
+
166
+ # Calculate total amounts
167
+ total_payable = emi * tenure_months
168
+ processing_fee = loan_amount * 0.02 # 2% processing fee
169
+
170
+ # Loan details table
171
+ loan_data = [
172
+ ["Loan Amount Sanctioned", f"₹{loan_amount:,.2f}"],
173
+ ["Loan Tenure", f"{tenure_months} months ({tenure_months//12} years {tenure_months%12} months)"],
174
+ ["Interest Rate (Annual)", f"{interest_rate}% p.a."],
175
+ ["Monthly EMI", f"₹{emi:,.2f}"],
176
+ ["Total Amount Payable", f"₹{total_payable:,.2f}"],
177
+ ["Processing Fee (2%)", f"₹{processing_fee:,.2f}"],
178
+ ["Risk Classification", risk_band],
179
+ ]
180
+
181
+ loan_table = Table(loan_data, colWidths=[3*inch, 3*inch])
182
+ loan_table.setStyle(TableStyle([
183
+ ('BACKGROUND', (0, 0), (-1, 0), colors.HexColor('#f0f0f0')),
184
+ ('TEXTCOLOR', (0, 0), (-1, -1), colors.HexColor('#333333')),
185
+ ('ALIGN', (0, 0), (-1, -1), 'LEFT'),
186
+ ('FONTNAME', (0, 0), (0, -1), 'Helvetica-Bold'),
187
+ ('FONTNAME', (1, 0), (1, -1), 'Helvetica'),
188
+ ('FONTSIZE', (0, 0), (-1, -1), 11),
189
+ ('GRID', (0, 0), (-1, -1), 1, colors.HexColor('#cccccc')),
190
+ ('VALIGN', (0, 0), (-1, -1), 'MIDDLE'),
191
+ ('ROWBACKGROUNDS', (0, 0), (-1, -1), [colors.white, colors.HexColor('#fafafa')]),
192
+ ('LEFTPADDING', (0, 0), (-1, -1), 12),
193
+ ('RIGHTPADDING', (0, 0), (-1, -1), 12),
194
+ ('TOPPADDING', (0, 0), (-1, -1), 10),
195
+ ('BOTTOMPADDING', (0, 0), (-1, -1), 10),
196
+ ]))
197
+
198
+ story.append(loan_table)
199
+ story.append(Spacer(1, 0.3 * inch))
200
+
201
+ # Terms and Conditions
202
+ terms_heading = Paragraph("Terms and Conditions", heading_style)
203
+ story.append(terms_heading)
204
+
205
+ terms = [
206
+ "This sanction is valid for 7 days from the date of issue.",
207
+ "The loan is subject to verification of all submitted documents.",
208
+ "Processing fee of 2% (non-refundable) will be deducted from the loan amount.",
209
+ "EMI will be auto-debited from your registered bank account on the 5th of every month.",
210
+ "Prepayment is allowed after 6 months with no prepayment charges.",
211
+ "Late payment charges of ₹500 per month will apply for delayed EMI payments.",
212
+ "This is a system-generated letter and does not require a physical signature.",
213
+ ]
214
+
215
+ for i, term in enumerate(terms, 1):
216
+ term_text = Paragraph(f"{i}. {term}", body_style)
217
+ story.append(term_text)
218
+
219
+ story.append(Spacer(1, 0.3 * inch))
220
+
221
+ # Next Steps
222
+ next_steps_heading = Paragraph("Next Steps", heading_style)
223
+ story.append(next_steps_heading)
224
+
225
+ next_steps = Paragraph(
226
+ "Please visit your nearest FinAgent branch with the following documents within 7 days:<br/>"
227
+ "• Original ID proof (Aadhaar/PAN/Passport)<br/>"
228
+ "• Bank statements (last 6 months)<br/>"
229
+ "• Salary slips (last 3 months)<br/>"
230
+ "• This sanction letter<br/><br/>"
231
+ "Our team will verify your documents and disburse the loan within 24 hours of successful verification.",
232
+ body_style
233
+ )
234
+ story.append(next_steps)
235
+ story.append(Spacer(1, 0.4 * inch))
236
+
237
+ # Closing
238
+ closing = Paragraph(
239
+ "Thank you for choosing FinAgent. We wish you success in your financial journey!<br/><br/>"
240
+ "Warm regards,<br/>"
241
+ "<b>FinAgent Loan Processing Team</b><br/>"
242
+ "Email: loans@finagent.com | Phone: 1800-123-4567",
243
+ body_style
244
+ )
245
+ story.append(closing)
246
+
247
+ # Footer
248
+ story.append(Spacer(1, 0.5 * inch))
249
+ footer = Paragraph(
250
+ "<i>This is a computer-generated document and does not require a signature.</i>",
251
+ ParagraphStyle('Footer', parent=styles['Normal'], fontSize=8, textColor=colors.grey, alignment=1)
252
+ )
253
+ story.append(footer)
254
+
255
+ # Build PDF
256
+ doc.build(story)
257
+
258
+ logger.info(f"Generated sanction letter: {filepath}")
259
+ return filepath
260
+
261
+ except Exception as e:
262
+ logger.error(f"Error generating PDF: {str(e)}", exc_info=True)
263
+ raise
264
+
265
+
266
+ # Global instance
267
+ pdf_service = PDFService()