Implemented parsing EPC QR codes (but not all checks have been implemented yet)
This commit is contained in:
parent
c47c1709c7
commit
6ed52967a1
|
@ -0,0 +1,127 @@
|
|||
package net.codinux.banking.epcqrcode.parser
|
||||
|
||||
import net.codinux.banking.epcqrcode.EpcQrCodeCharacterSet
|
||||
import net.codinux.banking.epcqrcode.EpcQrCodeValues
|
||||
import net.codinux.banking.epcqrcode.EpcQrCodeVersion
|
||||
|
||||
|
||||
open class EpcQrCodeParser {
|
||||
|
||||
open fun parseEpcQrCode(decodedQrCode: String): ParseEpcQrCodeResult {
|
||||
try {
|
||||
val lines = decodedQrCode.split("\n", "\r\n")
|
||||
|
||||
if (lines.size < 10 || lines.size > 12) {
|
||||
return createInvalidFormatResult(decodedQrCode, "A EPC-QR-Code consists of 10 to 12 lines, but passed string has ${lines.size} lines")
|
||||
}
|
||||
if (lines[0] != "BCD") {
|
||||
return createInvalidFormatResult(decodedQrCode, "A EPC-QR-Code's first lines has to be exactly 'BCD'")
|
||||
}
|
||||
|
||||
return parseEpcQrAfterSuccessfulFormatCheck(decodedQrCode, lines)
|
||||
} catch (e: Exception) {
|
||||
return ParseEpcQrCodeResult(decodedQrCode, ParseEpcQrCodeResultCode.UnpredictedErrorOccurred, null, e.message)
|
||||
}
|
||||
}
|
||||
|
||||
protected open fun parseEpcQrAfterSuccessfulFormatCheck(decodedQrCode: String, lines: List<String>): ParseEpcQrCodeResult {
|
||||
val validVersionCodes = EpcQrCodeVersion.values().map { it.code }
|
||||
if (lines[1].length != 3 || validVersionCodes.contains(lines[1]) == false) {
|
||||
return createInvalidFormatResult(decodedQrCode, "The second line has to be exactly one of these values: ${validVersionCodes.joinToString(", ")}")
|
||||
}
|
||||
|
||||
val version = EpcQrCodeVersion.values().first { it.code == lines[1] }
|
||||
|
||||
val codingCode = lines[2].toIntOrNull()
|
||||
if (lines[2].length != 1 || codingCode == null || codingCode < 1 || codingCode > 8) {
|
||||
return createInvalidFormatResult(decodedQrCode, "The third line has to be exactly one of these values: ${EpcQrCodeCharacterSet.values().map { it.code }.joinToString(", ")}")
|
||||
}
|
||||
|
||||
val coding = EpcQrCodeCharacterSet.values().first { it.code == codingCode }
|
||||
|
||||
if (lines[3].length != 3) {
|
||||
return createInvalidFormatResult(decodedQrCode, "The fourth line, ${lines[3]}, has to be exactly three characters long.")
|
||||
}
|
||||
|
||||
if (version == EpcQrCodeVersion.Version1 && isNotSet(lines[4])) {
|
||||
return createInvalidFormatResult(decodedQrCode, "The BIC line 5 may only be omitted in EPC-QR-Code version 002")
|
||||
}
|
||||
|
||||
val bic = lines[4].replace(" ", "") // not correct, but out there in the wild: sometimes BIC contains whitespaces
|
||||
if (bic.length != 8 && bic.length != 11 && (version == EpcQrCodeVersion.Version1 || isNotSet(bic) == false)) {
|
||||
return createInvalidFormatResult(decodedQrCode, "The BIC line 5, ${lines[4]}, must be either 8 or 11 characters long.")
|
||||
}
|
||||
|
||||
if (isNotSet(lines[5])) {
|
||||
return createInvalidFormatResult(decodedQrCode, "The receiver in line 6 may not be omitted.")
|
||||
}
|
||||
|
||||
val receiver = parseWithCoding(lines[5], coding)
|
||||
if (receiver.length > 70) { // omit check for parsing
|
||||
return createInvalidFormatResult(decodedQrCode, "The receiver in line 6, ${lines[5]}, may not be longer than 70 characters.")
|
||||
}
|
||||
|
||||
if (isNotSet(lines[6])) {
|
||||
return createInvalidFormatResult(decodedQrCode, "The IBAN in line 7 may not be omitted.")
|
||||
}
|
||||
// if (lines[6].length > 34) { // omit check for parsing
|
||||
// return createInvalidFormatResult("The IBAN in line 7, ${lines[6]}, may not be longer than 70 characters.")
|
||||
// }
|
||||
|
||||
var currencyCode: String? = null
|
||||
var amount: String? = null
|
||||
val currencyCodeAndAmount = lines[7]
|
||||
if (currencyCodeAndAmount.length > 3) {
|
||||
// TODO: check if the first three characters are letter
|
||||
// if (currencyCodeAndAmount.length > 15) {
|
||||
// return createInvalidFormatResult("The eighth line has to start with three upper case letters currency code and amount may not have more than 12 digits (including a dot as decimal separator).")
|
||||
// }
|
||||
|
||||
currencyCode = currencyCodeAndAmount.substring(0, 3)
|
||||
amount = currencyCodeAndAmount.substring(3)
|
||||
}
|
||||
|
||||
// if (lines[8].length > 4) { // omit check for parsing
|
||||
// return createInvalidFormatResult("The purpose code in line 9, ${lines[8]}, may not be longer than 4 characters.")
|
||||
// }
|
||||
|
||||
// if (lines[9].length > 35) { // omit check for parsing
|
||||
// return createInvalidFormatResult("The reconciliation reference in line 10, ${lines[9]}, may not be longer than 35 characters.")
|
||||
// }
|
||||
|
||||
val reconciliationText = if (lines.size < 11) null else parseToNullableString(lines[10])?.let { parseWithCoding(it, coding) }
|
||||
// if ((reconciliationText?.length ?: 0) > 140) { // omit check for parsing
|
||||
// return createInvalidFormatResult("The reconciliation text in line 11, ${lines[10]}, may not be longer than 140 characters.")
|
||||
// }
|
||||
|
||||
val displayText = if (lines.size < 12) null else lines[11]
|
||||
// if (displayText != null && displayText.length > 70) { // omit check for parsing
|
||||
// return createInvalidFormatResult("The display text in line 12, ${displayText}, may not be longer than 70 characters.")
|
||||
// }
|
||||
|
||||
return ParseEpcQrCodeResult(
|
||||
decodedQrCode, ParseEpcQrCodeResultCode.Success, EpcQrCodeValues(
|
||||
lines[0], version, coding, lines[3], parseToNullableString(bic), lines[5], lines[6],
|
||||
currencyCode, amount, parseToNullableString(lines[8]), parseToNullableString(lines[9]), reconciliationText, displayText
|
||||
)
|
||||
, null
|
||||
)
|
||||
}
|
||||
|
||||
protected open fun parseWithCoding(line: String, coding: EpcQrCodeCharacterSet): String {
|
||||
return line // TODO: does encoding work out of the box? // TODO: are there any encodings with more than one byte per characters allowed per specification
|
||||
}
|
||||
|
||||
protected open fun parseToNullableString(line: String): String? {
|
||||
return if (isNotSet(line)) null else line
|
||||
}
|
||||
|
||||
protected open fun isNotSet(line: String): Boolean {
|
||||
return line.isBlank()
|
||||
}
|
||||
|
||||
protected open fun createInvalidFormatResult(decodedQrCode: String, error: String): ParseEpcQrCodeResult {
|
||||
return ParseEpcQrCodeResult(decodedQrCode, ParseEpcQrCodeResultCode.NotAValidEpcQrCode, null, error)
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
package net.codinux.banking.epcqrcode.parser
|
||||
|
||||
import net.codinux.banking.epcqrcode.EpcQrCodeValues
|
||||
|
||||
|
||||
open class ParseEpcQrCodeResult(
|
||||
open val decodedQrCode: String,
|
||||
open val resultCode: ParseEpcQrCodeResultCode,
|
||||
open val epcQrCode: EpcQrCodeValues?,
|
||||
open val error: String?
|
||||
) {
|
||||
|
||||
open val successful: Boolean
|
||||
get() = resultCode == ParseEpcQrCodeResultCode.Success
|
||||
|
||||
|
||||
override fun toString(): String {
|
||||
return if (successful) {
|
||||
"Success: $epcQrCode"
|
||||
}
|
||||
else {
|
||||
"Error: $error"
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
package net.codinux.banking.epcqrcode.parser
|
||||
|
||||
|
||||
enum class ParseEpcQrCodeResultCode {
|
||||
|
||||
Success,
|
||||
|
||||
NotAValidEpcQrCode,
|
||||
|
||||
UnpredictedErrorOccurred
|
||||
|
||||
}
|
|
@ -0,0 +1,199 @@
|
|||
package net.codinux.banking.epcqrcode.parser
|
||||
|
||||
import net.codinux.banking.epcqrcode.EpcQrCodeCharacterSet
|
||||
import net.codinux.banking.epcqrcode.EpcQrCodeVersion
|
||||
import kotlin.test.*
|
||||
|
||||
|
||||
class EpcQrCodeParserTest {
|
||||
|
||||
private val underTest = EpcQrCodeParser()
|
||||
|
||||
|
||||
@Test
|
||||
fun basicData() {
|
||||
val decodedQrCode = """
|
||||
BCD
|
||||
002
|
||||
1
|
||||
SCT
|
||||
|
||||
Liebe
|
||||
DE01234567890123456789
|
||||
|
||||
CHAR
|
||||
|
||||
|
||||
""".trimIndent()
|
||||
|
||||
val result = underTest.parseEpcQrCode(decodedQrCode)
|
||||
|
||||
assertParsingSuccessful(result)
|
||||
|
||||
assertEquals("Liebe", result.epcQrCode!!.receiverName)
|
||||
assertEquals("DE01234567890123456789", result.epcQrCode!!.iban)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun wikipediaExampleBelgianRedCross() {
|
||||
|
||||
// when
|
||||
val result = underTest.parseEpcQrCode("""
|
||||
BCD
|
||||
001
|
||||
1
|
||||
SCT
|
||||
BPOTBEB1
|
||||
Red Cross
|
||||
BE72000000001616
|
||||
EUR1
|
||||
CHAR
|
||||
|
||||
Urgency fund
|
||||
Sample EPC QR Code
|
||||
""".trim())
|
||||
|
||||
// then
|
||||
assertParsingSuccessful(result)
|
||||
|
||||
assertEpcQrCode(result, EpcQrCodeVersion.Version1, EpcQrCodeCharacterSet.UTF_8, "SCT", "BPOTBEB1", "Red Cross",
|
||||
"BE72000000001616", "EUR", "1", "CHAR", null,
|
||||
"Urgency fund", "Sample EPC QR Code")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun spendeAnAerzteOhneGrenzen() {
|
||||
|
||||
// when
|
||||
val result = underTest.parseEpcQrCode("""
|
||||
BCD
|
||||
001
|
||||
1
|
||||
SCT
|
||||
BFSWDE33XXX
|
||||
Ärzte ohne Grenzen e.V.
|
||||
DE72370205000009709700
|
||||
EUR100
|
||||
|
||||
|
||||
Spende
|
||||
Danke für Ihre Spende
|
||||
""".trim())
|
||||
|
||||
// then
|
||||
assertParsingSuccessful(result)
|
||||
|
||||
assertEpcQrCode(result, EpcQrCodeVersion.Version1, EpcQrCodeCharacterSet.UTF_8, "SCT", "BFSWDE33XXX", "Ärzte ohne Grenzen e.V.",
|
||||
"DE72370205000009709700", "EUR", "100", null, null,
|
||||
"Spende", "Danke für Ihre Spende")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun stuzzaExample01() {
|
||||
|
||||
// when
|
||||
val result = underTest.parseEpcQrCode("""
|
||||
BCD
|
||||
001
|
||||
1
|
||||
SCT
|
||||
BICVXXDD123
|
||||
35 Zeichen langer Empfängername zum
|
||||
XX17LandMitLangerIBAN2345678901234
|
||||
EUR12345689.01
|
||||
|
||||
35ZeichenLangeREFzurZuordnungBeimBe
|
||||
|
||||
Netter Text für den Zahlenden, damit dieser weiß, was er zahlt und auc
|
||||
""".trim())
|
||||
|
||||
// then
|
||||
assertParsingSuccessful(result)
|
||||
|
||||
assertEpcQrCode(result, EpcQrCodeVersion.Version1, EpcQrCodeCharacterSet.UTF_8, "SCT", "BICVXXDD123", "35 Zeichen langer Empfängername zum",
|
||||
"XX17LandMitLangerIBAN2345678901234", "EUR", "12345689.01", null, "35ZeichenLangeREFzurZuordnungBeimBe",
|
||||
null, "Netter Text für den Zahlenden, damit dieser weiß, was er zahlt und auc")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun stuzzaExample02() {
|
||||
|
||||
// when
|
||||
val result = underTest.parseEpcQrCode("""
|
||||
BCD
|
||||
001
|
||||
1
|
||||
SCT
|
||||
GIBAATWW
|
||||
Max Mustermann
|
||||
AT682011131032423628
|
||||
EUR1456.89
|
||||
|
||||
457845789452
|
||||
|
||||
Diverse Autoteile, Re 789452 KN 457845
|
||||
""".trim())
|
||||
|
||||
// then
|
||||
assertParsingSuccessful(result)
|
||||
|
||||
assertEpcQrCode(result, EpcQrCodeVersion.Version1, EpcQrCodeCharacterSet.UTF_8, "SCT", "GIBAATWW", "Max Mustermann",
|
||||
"AT682011131032423628", "EUR", "1456.89", null, "457845789452",
|
||||
null, "Diverse Autoteile, Re 789452 KN 457845")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun stuzzaExample07() {
|
||||
|
||||
// when
|
||||
val result = underTest.parseEpcQrCode("""
|
||||
BCD
|
||||
002
|
||||
2
|
||||
SCT
|
||||
|
||||
35 Zeichen langer Empfängername zum
|
||||
XX17LandMitLangerIBAN2345678901234
|
||||
EUR12345689.01
|
||||
|
||||
35ZeichenLangeREFzurZuordnungBeimBe
|
||||
|
||||
Netter Text für den Zahlenden, damit dieser weiß, was er zahlt und auc
|
||||
""".trim())
|
||||
|
||||
// then
|
||||
assertParsingSuccessful(result)
|
||||
|
||||
assertEpcQrCode(result, EpcQrCodeVersion.Version2, EpcQrCodeCharacterSet.ISO_8895_1, "SCT", null, "35 Zeichen langer Empfängername zum",
|
||||
"XX17LandMitLangerIBAN2345678901234", "EUR", "12345689.01", null, "35ZeichenLangeREFzurZuordnungBeimBe",
|
||||
null, "Netter Text für den Zahlenden, damit dieser weiß, was er zahlt und auc")
|
||||
}
|
||||
|
||||
|
||||
private fun assertParsingSuccessful(result: ParseEpcQrCodeResult) {
|
||||
assertTrue(result.successful)
|
||||
assertNull(result.error)
|
||||
assertNotNull(result.epcQrCode)
|
||||
}
|
||||
|
||||
private fun assertEpcQrCode(result: ParseEpcQrCodeResult, version: EpcQrCodeVersion, coding: EpcQrCodeCharacterSet, function: String,
|
||||
bic: String?, receiver: String, iban: String, currency: String?, amount: String?,
|
||||
purposeCode: String?, reference: String?, text: String?, displayText: String?) {
|
||||
|
||||
result.epcQrCode?.let { epcQrCode ->
|
||||
assertEquals(version, epcQrCode.version)
|
||||
assertEquals(coding, epcQrCode.coding)
|
||||
assertEquals(function, epcQrCode.function)
|
||||
assertEquals(bic, epcQrCode.bic)
|
||||
assertEquals(receiver, epcQrCode.receiverName)
|
||||
assertEquals(iban, epcQrCode.iban)
|
||||
assertEquals(currency, epcQrCode.currencyCode)
|
||||
assertEquals(amount, epcQrCode.amount)
|
||||
assertEquals(purposeCode, epcQrCode.purposeCode)
|
||||
assertEquals(reference, epcQrCode.remittanceReference)
|
||||
assertEquals(text, epcQrCode.remittanceText)
|
||||
assertEquals(displayText, epcQrCode.originatorInformation)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in New Issue