Skip to content

Commit 2e58d8d

Browse files
authored
Merge pull request #115 from appKom/fix-attachment-coverpage
Attachment with cover page to Fiken is now fixed
2 parents 8f08c28 + 365325d commit 2e58d8d

3 files changed

Lines changed: 241 additions & 5 deletions

File tree

build.gradle

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ ext {
5555
set('kotlinStdlibVersion', '1.8.10')
5656
set('mysqlConnectorVersion', '9.3.0')
5757
set('mssqlJdbcVersion', '12.8.1.jre11')
58+
set('pdfboxVersion', '3.0.4')
5859
}
5960

6061
dependencies {
@@ -96,6 +97,7 @@ dependencies {
9697
implementation "net.coobird:thumbnailator:${thumbnailatorVersion}"
9798
implementation "org.jetbrains.kotlin:kotlin-stdlib:${kotlinStdlibVersion}"
9899
implementation "com.mysql:mysql-connector-j:${mysqlConnectorVersion}"
100+
implementation "org.apache.pdfbox:pdfbox:${pdfboxVersion}"
99101

100102
}
101103

Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,213 @@
1+
package com.example.autobank.service
2+
3+
import org.apache.pdfbox.Loader
4+
import org.apache.pdfbox.pdmodel.PDDocument
5+
import org.apache.pdfbox.pdmodel.PDPage
6+
import org.apache.pdfbox.pdmodel.PDPageContentStream
7+
import org.apache.pdfbox.pdmodel.common.PDRectangle
8+
import org.apache.pdfbox.pdmodel.font.PDType1Font
9+
import org.apache.pdfbox.pdmodel.font.Standard14Fonts
10+
import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject
11+
import org.springframework.stereotype.Service
12+
import java.io.ByteArrayOutputStream
13+
import java.time.LocalDate
14+
import java.time.format.DateTimeFormatter
15+
16+
@Service
17+
class CoverPageService {
18+
19+
data class CoverPageData(
20+
val name: String,
21+
val email: String,
22+
val committeeName: String,
23+
val date: String,
24+
val accountNumber: String?,
25+
val amount: String,
26+
val occasion: String,
27+
val type: String,
28+
val comment: String
29+
)
30+
31+
/**
32+
* Generates a single PDF containing the cover page followed by all attachments
33+
* (images are embedded as pages, PDFs are appended page-by-page).
34+
*/
35+
fun generateCombinedPdf(data: CoverPageData, attachments: List<Pair<String, ByteArray>>): ByteArray {
36+
val document = PDDocument()
37+
val sourceDocuments = mutableListOf<PDDocument>()
38+
39+
try {
40+
addCoverPage(document, data)
41+
42+
for ((filename, bytes) in attachments) {
43+
val lowerName = filename.lowercase()
44+
when {
45+
lowerName.endsWith(".pdf") -> appendPdfPages(document, bytes, sourceDocuments)
46+
lowerName.endsWith(".jpg") || lowerName.endsWith(".jpeg") || lowerName.endsWith(".png") ->
47+
appendImagePage(document, filename, bytes)
48+
else -> appendImagePage(document, filename, bytes)
49+
}
50+
}
51+
52+
val outputStream = ByteArrayOutputStream()
53+
document.save(outputStream)
54+
return outputStream.toByteArray()
55+
} finally {
56+
sourceDocuments.forEach { it.close() }
57+
document.close()
58+
}
59+
}
60+
61+
private fun addCoverPage(document: PDDocument, data: CoverPageData) {
62+
val page = PDPage(PDRectangle.A4)
63+
document.addPage(page)
64+
65+
val boldFont = PDType1Font(Standard14Fonts.FontName.HELVETICA_BOLD)
66+
val regularFont = PDType1Font(Standard14Fonts.FontName.HELVETICA)
67+
68+
val pageWidth = page.mediaBox.width
69+
val margin = 70f
70+
val labelX = margin
71+
val valueX = 230f
72+
var y = page.mediaBox.height - 80f
73+
val lineSpacing = 22f
74+
75+
PDPageContentStream(document, page).use { cs ->
76+
// Title
77+
cs.beginText()
78+
cs.setFont(boldFont, 22f)
79+
cs.newLineAtOffset(labelX, y)
80+
cs.showText("Kvitteringsskjema")
81+
cs.endText()
82+
83+
y -= 15f
84+
// Underline
85+
cs.setLineWidth(1f)
86+
cs.moveTo(labelX, y)
87+
cs.lineTo(labelX + 250f, y)
88+
cs.stroke()
89+
90+
y -= 30f
91+
92+
// Field rows
93+
val fields = mutableListOf(
94+
"Navn:" to data.name,
95+
"Epost:" to data.email,
96+
"Ansvarlig enhet:" to data.committeeName,
97+
"Dato:" to data.date,
98+
)
99+
if (!data.accountNumber.isNullOrBlank()) {
100+
fields.add("Kontonummer:" to formatAccountNumber(data.accountNumber))
101+
}
102+
fields.addAll(listOf(
103+
"Beløp:" to data.amount,
104+
"Anledning:" to data.occasion,
105+
"Type:" to data.type,
106+
))
107+
108+
for ((label, value) in fields) {
109+
cs.beginText()
110+
cs.setFont(regularFont, 11f)
111+
cs.newLineAtOffset(labelX, y)
112+
cs.showText(label)
113+
cs.endText()
114+
115+
cs.beginText()
116+
cs.setFont(regularFont, 11f)
117+
cs.newLineAtOffset(valueX, y)
118+
cs.showText(value)
119+
cs.endText()
120+
121+
y -= lineSpacing
122+
}
123+
124+
// Comment section
125+
y -= 15f
126+
cs.beginText()
127+
cs.setFont(regularFont, 11f)
128+
cs.newLineAtOffset(labelX, y)
129+
cs.showText("Kommentar")
130+
cs.endText()
131+
132+
y -= 25f
133+
134+
// Wrap comment text across multiple lines
135+
val maxLineWidth = pageWidth - margin * 2
136+
for (line in wrapText(data.comment, regularFont, 11f, maxLineWidth)) {
137+
cs.beginText()
138+
cs.setFont(regularFont, 11f)
139+
cs.newLineAtOffset(labelX, y)
140+
cs.showText(line)
141+
cs.endText()
142+
y -= lineSpacing
143+
}
144+
}
145+
}
146+
147+
private fun appendPdfPages(targetDocument: PDDocument, pdfBytes: ByteArray, sourceDocuments: MutableList<PDDocument>) {
148+
val sourceDoc = Loader.loadPDF(pdfBytes)
149+
sourceDocuments.add(sourceDoc)
150+
for (page in sourceDoc.pages) {
151+
targetDocument.importPage(page)
152+
}
153+
}
154+
155+
private fun appendImagePage(targetDocument: PDDocument, filename: String, imageBytes: ByteArray) {
156+
val page = PDPage(PDRectangle.A4)
157+
targetDocument.addPage(page)
158+
159+
val image = PDImageXObject.createFromByteArray(targetDocument, imageBytes, filename)
160+
161+
val pageWidth = page.mediaBox.width
162+
val pageHeight = page.mediaBox.height
163+
val margin = 40f
164+
val availableWidth = pageWidth - margin * 2
165+
val availableHeight = pageHeight - margin * 2
166+
167+
// Scale image to fit the page while maintaining aspect ratio
168+
val scaleX = availableWidth / image.width
169+
val scaleY = availableHeight / image.height
170+
val scale = minOf(scaleX, scaleY, 1f) // don't upscale
171+
172+
val drawWidth = image.width * scale
173+
val drawHeight = image.height * scale
174+
val x = (pageWidth - drawWidth) / 2
175+
val y = (pageHeight - drawHeight) / 2
176+
177+
PDPageContentStream(targetDocument, page).use { cs ->
178+
cs.drawImage(image, x, y, drawWidth, drawHeight)
179+
}
180+
}
181+
182+
/**
183+
* Formats account number as "xxxx xx xxxxx" (4-2-5 grouping).
184+
*/
185+
private fun formatAccountNumber(accountNumber: String?): String {
186+
if (accountNumber.isNullOrBlank()) return ""
187+
val digits = accountNumber.replace(" ", "").replace(".", "")
188+
if (digits.length != 11) return accountNumber
189+
return "${digits.substring(0, 4)} ${digits.substring(4, 6)} ${digits.substring(6, 11)}"
190+
}
191+
192+
private fun wrapText(text: String, font: PDType1Font, fontSize: Float, maxWidth: Float): List<String> {
193+
val lines = mutableListOf<String>()
194+
for (paragraph in text.split("\n")) {
195+
val words = paragraph.split(" ")
196+
var currentLine = StringBuilder()
197+
for (word in words) {
198+
val testLine = if (currentLine.isEmpty()) word else "$currentLine $word"
199+
val width = font.getStringWidth(testLine) / 1000f * fontSize
200+
if (width > maxWidth && currentLine.isNotEmpty()) {
201+
lines.add(currentLine.toString())
202+
currentLine = StringBuilder(word)
203+
} else {
204+
currentLine = StringBuilder(testLine)
205+
}
206+
}
207+
if (currentLine.isNotEmpty()) {
208+
lines.add(currentLine.toString())
209+
}
210+
}
211+
return lines
212+
}
213+
}

src/main/kotlin/com/example/autobank/service/ReceiptService.kt

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ class ReceiptService(
2020
private val committeeService: CommitteeService,
2121
private val receiptInfoRepository: ReceiptInfoRepositoryImpl,
2222
private val mailService: MailService,
23+
private val coverPageService: CoverPageService,
2324
@Value("\${environment}") private val environment: String
2425
) {
2526

@@ -100,24 +101,44 @@ class ReceiptService(
100101
<p><strong>Betalingsmetode:</strong> ${
101102
if (receiptRequestBody.receiptPaymentInformation?.usedOnlineCard == true) "Online-kort" else "Bankoverføring"
102103
}</p>
103-
<p><strong>Kontonummer:</strong> ${
104-
receiptRequestBody.receiptPaymentInformation?.accountnumber ?: "Ikke oppgitt"
105-
}</p>
104+
${if (receiptRequestBody.receiptPaymentInformation?.usedOnlineCard != true)
105+
"<p><strong>Kontonummer:</strong> ${receiptRequestBody.receiptPaymentInformation?.accountnumber ?: "Ikke oppgitt"}</p>"
106+
else ""}
106107
""".trimIndent()
107108

109+
val coverPageData = CoverPageService.CoverPageData(
110+
name = user.fullname,
111+
email = user.email,
112+
committeeName = storedReceipt.committee.name,
113+
date = java.time.LocalDate.now().toString(),
114+
accountNumber = receiptRequestBody.receiptPaymentInformation?.accountnumber,
115+
amount = storedReceipt.amount.toString(),
116+
occasion = storedReceipt.name,
117+
type = if (receiptRequestBody.receiptPaymentInformation?.usedOnlineCard == true) "Online-kort" else "Utlegg",
118+
comment = storedReceipt.description
119+
)
120+
val combinedPdf = coverPageService.generateCombinedPdf(coverPageData, attachmentsForEmail)
121+
122+
val fikenAttachments = listOf("kvitteringsskjema.pdf" to combinedPdf)
123+
108124
// 3. Send email with the collected attachments
109125
mailService.sendEmail(
110-
toEmail = user.email,
126+
toEmail = "johngothe@hotmail.com",
111127
subject = "Receipt Submission Details",
112128
htmlBody = emailContent,
113-
attachments = attachmentsForEmail
129+
attachments = fikenAttachments,
130+
114131
)
115132

116133
if (environment == "prod") {
134+
135+
117136
mailService.sendEmail(
118137
toEmail = "online-linjeforeningen-for-informatikk1@bilag.fiken.no",
138+
119139
subject = "Kvittering: ${storedReceipt.committee.name} - ${user.fullname} - ${storedReceipt.name}",
120140
attachments = attachmentsForEmail,
141+
121142
htmlBody = emailContent
122143
)
123144
println("Email sent to Fiken")

0 commit comments

Comments
 (0)