gnunet-svn
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

[libeufin] branch master updated: iso20022


From: gnunet
Subject: [libeufin] branch master updated: iso20022
Date: Mon, 06 Jul 2020 12:56:15 +0200

This is an automated email from the git hooks/post-receive script.

dold pushed a commit to branch master
in repository libeufin.

The following commit(s) were added to refs/heads/master by this push:
     new 94215e3  iso20022
94215e3 is described below

commit 94215e32b2333d507ebbb61a8bfc7f498fd3d187
Author: Florian Dold <florian.dold@gmail.com>
AuthorDate: Mon Jul 6 16:26:02 2020 +0530

    iso20022
---
 .../main/kotlin/tech/libeufin/nexus/Iso20022.kt    | 184 +++++++++++++++++----
 nexus/src/test/kotlin/Iso20022Test.kt              |  42 +++--
 .../camt.053/de.camt.053.001.02.xml                | 171 ++++++++++++++++++-
 3 files changed, 349 insertions(+), 48 deletions(-)

diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/Iso20022.kt 
b/nexus/src/main/kotlin/tech/libeufin/nexus/Iso20022.kt
index 95d3506..16fcf84 100644
--- a/nexus/src/main/kotlin/tech/libeufin/nexus/Iso20022.kt
+++ b/nexus/src/main/kotlin/tech/libeufin/nexus/Iso20022.kt
@@ -67,6 +67,7 @@ data class CamtReport(
 data class GenericId(
     val id: String,
     val schemeName: String?,
+    val proprietarySchemeName: String?,
     val issuer: String?
 )
 
@@ -78,8 +79,19 @@ data class CashAccount(
     val otherId: GenericId?
 )
 
+data class Balance(
+    val type: String?,
+    val subtype: String?,
+    val proprietaryType: String?,
+    val proprietarySubtype: String?,
+    val date: String,
+    val creditDebitIndicator: CreditDebitIndicator,
+    val amount: CurrencyAmount
+)
+
 data class CamtParseResult(
     val reports: List<CamtReport>,
+    val balances: List<Balance>,
     val messageId: String,
     /**
      * Message type in form of the ISO 20022 message name.
@@ -88,13 +100,36 @@ data class CamtParseResult(
     val creationDateTime: String
 )
 
-enum class PartyType(@get:JsonValue val jsonName: String) {
-    PRIVATE("private"), ORGANIZATION("organization")
-}
+@JsonInclude(JsonInclude.Include.NON_NULL)
+data class PrivateIdentification(
+    val birthDate: String?,
+    val provinceOfBirth: String?,
+    val cityOfBirth: String?,
+    val countryOfBirth: String?
+)
+
+@JsonInclude(JsonInclude.Include.NON_NULL)
+data class OrganizationIdentification(
+    val bic: String?,
+    val lei: String?
+)
 
+/**
+ * Identification of a party, which can be a private party
+ * or an organiation.
+ *
+ * Mapping of ISO 20022 PartyIdentification135.
+ */
 @JsonInclude(JsonInclude.Include.NON_NULL)
 data class PartyIdentification(
     val name: String?,
+    val countryOfResidence: String?,
+    val privateId: PrivateIdentification?,
+    val organizationId: OrganizationIdentification?,
+
+    /**
+     * Identification that applies to both private parties and organizations.
+     */
     val otherId: GenericId?
 )
 
@@ -144,7 +179,17 @@ data class TransactionInfo(
      * Unstructured remittance information (=subject line) of the transaction,
      * or the empty string if missing.
      */
-    val unstructuredRemittanceInformation: String
+    val unstructuredRemittanceInformation: String,
+    val returnInfo: ReturnInfo?
+)
+
+@JsonInclude(JsonInclude.Include.NON_NULL)
+data class ReturnInfo(
+    val originalBankTransactionCode: BankTransactionCode?,
+    val originator: PartyIdentification?,
+    val reason: String?,
+    val proprietaryReason: String?,
+    val additionalInfo: String?
 )
 
 @JsonInclude(JsonInclude.Include.NON_NULL)
@@ -349,6 +394,19 @@ private fun XmlElementDestructor.extractAgent(): 
AgentIdentification {
     )
 }
 
+private fun XmlElementDestructor.extractGenericId(): GenericId {
+    return GenericId(
+        id = requireUniqueChildNamed("Id") { it.textContent },
+        schemeName = maybeUniqueChildNamed("SchmeNm") {
+            maybeUniqueChildNamed("Cd") { it.textContent }
+        },
+        issuer = maybeUniqueChildNamed("Issr") { it.textContent },
+        proprietarySchemeName = maybeUniqueChildNamed("SchmeNm") {
+            maybeUniqueChildNamed("Prtry") { it.textContent }
+        }
+    )
+}
+
 private fun XmlElementDestructor.extractAccount(): CashAccount {
     var iban: String? = null
     var otherId: GenericId? = null
@@ -361,11 +419,7 @@ private fun XmlElementDestructor.extractAccount(): 
CashAccount {
                     iban = it.textContent
                 }
                 "Othr" -> {
-                    otherId = GenericId(
-                        id = requireUniqueChildNamed("Id") { it.textContent },
-                        schemeName = maybeUniqueChildNamed("SchmeNm") { 
it.textContent },
-                        issuer = maybeUniqueChildNamed("Issr") { 
it.textContent }
-                    )
+                    otherId = extractGenericId()
                 }
                 else -> throw Error("invalid account identification")
             }
@@ -375,9 +429,43 @@ private fun XmlElementDestructor.extractAccount(): 
CashAccount {
 }
 
 private fun XmlElementDestructor.extractParty(): PartyIdentification {
+    val otherId: GenericId? = maybeUniqueChildNamed("Id") {
+        (maybeUniqueChildNamed("PrvtId") { it } ?: 
maybeUniqueChildNamed("OrgId") { it })?.run {
+            maybeUniqueChildNamed("Othr") {
+                extractGenericId()
+            }
+        }
+    }
+
+    val privateId = maybeUniqueChildNamed("Id") {
+        maybeUniqueChildNamed("PrvtId") {
+            maybeUniqueChildNamed("DtAndPlcOfBirth") {
+                PrivateIdentification(
+                    birthDate = maybeUniqueChildNamed("BirthDt") { 
it.textContent},
+                    cityOfBirth = maybeUniqueChildNamed("CityOfBirth") { 
it.textContent},
+                    countryOfBirth = maybeUniqueChildNamed("CtryOfBirth") { 
it.textContent},
+                    provinceOfBirth = maybeUniqueChildNamed("PrvcOfBirth") { 
it.textContent}
+                )
+            }
+        }
+    }
+
+    val organizationId = maybeUniqueChildNamed("Id") {
+        maybeUniqueChildNamed("OrgId") {
+            OrganizationIdentification(
+                bic = maybeUniqueChildNamed("BICOrBEI") { it.textContent} ?: 
maybeUniqueChildNamed("AnyBIC") { it.textContent},
+                lei = maybeUniqueChildNamed("LEI") { it.textContent}
+            )
+        }
+    }
+
+
     return PartyIdentification(
         name = maybeUniqueChildNamed("Nm") { it.textContent },
-        otherId = null
+        otherId = otherId,
+        privateId = privateId,
+        organizationId = organizationId,
+        countryOfResidence = maybeUniqueChildNamed("CtryOfRes") { 
it.textContent }
     )
 }
 
@@ -486,7 +574,7 @@ private fun XmlElementDestructor.extractTransactionInfos(
                     if (chunks.isEmpty()) {
                         null
                     } else {
-                        chunks.joinToString()
+                        chunks.joinToString(separator = "")
                     }
                 } ?: "",
                 creditorAgent = maybeUniqueChildNamed("CdtrAgt") { 
extractAgent() },
@@ -494,12 +582,45 @@ private fun XmlElementDestructor.extractTransactionInfos(
                 debtorAccount = maybeUniqueChildNamed("DbtrAgt") { 
extractAccount() },
                 creditorAccount = maybeUniqueChildNamed("CdtrAgt") { 
extractAccount() },
                 debtor = maybeUniqueChildNamed("Dbtr") { extractParty() },
-                creditor = maybeUniqueChildNamed("Cdtr") { extractParty() }
+                creditor = maybeUniqueChildNamed("Cdtr") { extractParty() },
+                returnInfo = maybeUniqueChildNamed("RtrInf") {
+                    ReturnInfo(
+                        originalBankTransactionCode = 
maybeUniqueChildNamed("OrgnlBkTxCd") {
+                            extractInnerBkTxCd()
+                        },
+                        originator = maybeUniqueChildNamed("Orgtr") { 
extractParty() },
+                        reason = maybeUniqueChildNamed("Rsn") { 
maybeUniqueChildNamed("Cd") { it.textContent } },
+                        proprietaryReason = maybeUniqueChildNamed("Rsn") { 
maybeUniqueChildNamed("Prtry") { it.textContent } },
+                        additionalInfo = maybeUniqueChildNamed("AddtlInf") { 
it.textContent }
+                    )
+                }
             )
         }
     }
 }
 
+private fun XmlElementDestructor.extractInnerBkTxCd(): BankTransactionCode {
+    return BankTransactionCode(
+        domain = maybeUniqueChildNamed("Domn") { maybeUniqueChildNamed("Cd") { 
it.textContent } },
+        family = maybeUniqueChildNamed("Domn") {
+            maybeUniqueChildNamed("Fmly") {
+                maybeUniqueChildNamed("Cd") { it.textContent }
+            }
+        },
+        subfamily = maybeUniqueChildNamed("Domn") {
+            maybeUniqueChildNamed("Fmly") {
+                maybeUniqueChildNamed("SubFmlyCd") { it.textContent }
+            }
+        },
+        proprietaryCode = maybeUniqueChildNamed("Prtry") {
+            maybeUniqueChildNamed("Cd") { it.textContent }
+        },
+        proprietaryIssuer = maybeUniqueChildNamed("Prtry") {
+            maybeUniqueChildNamed("Issr") { it.textContent }
+        }
+    )
+}
+
 private fun XmlElementDestructor.extractInnerTransactions(): CamtReport {
     val account = requireUniqueChildNamed("Acct") { extractAccount() }
     val entries = mapEachChildNamed("Ntry") {
@@ -512,25 +633,7 @@ private fun 
XmlElementDestructor.extractInnerTransactions(): CamtReport {
             CreditDebitIndicator.valueOf(it)
         }
         val btc = requireUniqueChildNamed("BkTxCd") {
-            BankTransactionCode(
-                domain = maybeUniqueChildNamed("Domn") { 
maybeUniqueChildNamed("Cd") { it.textContent } },
-                family = maybeUniqueChildNamed("Domn") {
-                    maybeUniqueChildNamed("Fmly") {
-                        maybeUniqueChildNamed("Cd") { it.textContent }
-                    }
-                },
-                subfamily = maybeUniqueChildNamed("Domn") {
-                    maybeUniqueChildNamed("Fmly") {
-                        maybeUniqueChildNamed("SubFmlyCd") { it.textContent }
-                    }
-                },
-                proprietaryCode = maybeUniqueChildNamed("Prtry") {
-                    maybeUniqueChildNamed("Cd") { it.textContent }
-                },
-                proprietaryIssuer = maybeUniqueChildNamed("Prtry") {
-                    maybeUniqueChildNamed("Issr") { it.textContent }
-                }
-            )
+            extractInnerBkTxCd()
         }
         val acctSvcrRef = maybeUniqueChildNamed("AcctSvcrRef") { 
it.textContent }
         val entryRef = maybeUniqueChildNamed("NtryRef") { it.textContent }
@@ -575,6 +678,23 @@ fun parseCamtMessage(doc: Document): CamtParseResult {
                     }
                 }
             }
+
+            val balances = requireOnlyChild {
+                mapEachChildNamed("Bal") {
+                    Balance(
+                        type = maybeUniqueChildNamed("Tp") { 
maybeUniqueChildNamed("Cd") { it.textContent } },
+                        proprietaryType = maybeUniqueChildNamed("Tp") { 
maybeUniqueChildNamed("Prtry") { it.textContent } },
+                        date = extractDateOrDateTime(),
+                        creditDebitIndicator = 
requireUniqueChildNamed("CdtDbtInd") { it.textContent }.let {
+                            CreditDebitIndicator.valueOf(it)
+                        },
+                        subtype = maybeUniqueChildNamed("SubTp") { 
maybeUniqueChildNamed("Cd") { it.textContent } },
+                        proprietarySubtype = maybeUniqueChildNamed("SubTp") { 
maybeUniqueChildNamed("Prtry") { it.textContent } },
+                        amount = extractCurrencyAmount()
+                    )
+                }
+            }
+
             val messageId = requireOnlyChild {
                 requireUniqueChildNamed("GrpHdr") {
                     requireUniqueChildNamed("MsgId") { it.textContent }
@@ -594,7 +714,7 @@ fun parseCamtMessage(doc: Document): CamtParseResult {
                     }
                 }
             }
-            CamtParseResult(reports, messageId, messageType, creationDateTime)
+            CamtParseResult(reports, balances, messageId, messageType, 
creationDateTime)
         }
     }
 }
diff --git a/nexus/src/test/kotlin/Iso20022Test.kt 
b/nexus/src/test/kotlin/Iso20022Test.kt
index 24553be..f655414 100644
--- a/nexus/src/test/kotlin/Iso20022Test.kt
+++ b/nexus/src/test/kotlin/Iso20022Test.kt
@@ -19,21 +19,33 @@ class Iso20022Test {
     fun testTransactionsImport() {
         val camt53 = 
loadXmlResource("iso20022-samples/camt.053/de.camt.053.001.02.xml")
         val r = parseCamtMessage(camt53)
-        assertEquals(r.messageId, "msg-001")
-        assertEquals(r.creationDateTime, "2020-07-03T12:44:40+05:30")
-        assertEquals(r.messageType, CashManagementResponseType.Statement)
-        assertEquals(r.reports.size, 1)
-        assertEquals(r.reports[0].entries[0].entryAmount.amount, "100.00")
-        assertEquals(r.reports[0].entries[0].entryAmount.currency, "EUR")
-        assertEquals(r.reports[0].entries[0].status, EntryStatus.BOOK)
-        assertEquals(r.reports[0].entries[0].entryRef, null)
-        assertEquals(r.reports[0].entries[0].accountServicerRef, 
"acctsvcrref-001")
-        assertEquals(r.reports[0].entries[0].bankTransactionCode.domain, 
"PMNT")
-        assertEquals(r.reports[0].entries[0].bankTransactionCode.family, 
"RCDT")
-        assertEquals(r.reports[0].entries[0].bankTransactionCode.subfamily, 
"ESCT")
-        
assertEquals(r.reports[0].entries[0].bankTransactionCode.proprietaryCode, "166")
-        
assertEquals(r.reports[0].entries[0].bankTransactionCode.proprietaryIssuer, 
"DK")
-        assertEquals(r.reports[0].entries[0].transactionInfos.size, 1)
+        assertEquals("msg-001", r.messageId)
+        assertEquals("2020-07-03T12:44:40+05:30", r.creationDateTime)
+        assertEquals(CashManagementResponseType.Statement, r.messageType)
+        assertEquals(1, r.reports.size)
+
+        // First Entry
+        assertEquals("100.00", r.reports[0].entries[0].entryAmount.amount)
+        assertEquals("EUR", r.reports[0].entries[0].entryAmount.currency)
+        assertEquals(CreditDebitIndicator.CRDT, 
r.reports[0].entries[0].creditDebitIndicator)
+        assertEquals(EntryStatus.BOOK, r.reports[0].entries[0].status)
+        assertEquals(null, r.reports[0].entries[0].entryRef)
+        assertEquals("acctsvcrref-001", 
r.reports[0].entries[0].accountServicerRef)
+        assertEquals("PMNT", 
r.reports[0].entries[0].bankTransactionCode.domain)
+        assertEquals("RCDT", 
r.reports[0].entries[0].bankTransactionCode.family)
+        assertEquals("ESCT", 
r.reports[0].entries[0].bankTransactionCode.subfamily)
+        assertEquals("166", 
r.reports[0].entries[0].bankTransactionCode.proprietaryCode)
+        assertEquals("DK", 
r.reports[0].entries[0].bankTransactionCode.proprietaryIssuer)
+        assertEquals(1, r.reports[0].entries[0].transactionInfos.size)
+        assertEquals("EUR", 
r.reports[0].entries[0].transactionInfos[0].amount.currency)
+        assertEquals("100.00", 
r.reports[0].entries[0].transactionInfos[0].amount.amount)
+        assertEquals(CreditDebitIndicator.CRDT, 
r.reports[0].entries[0].transactionInfos[0].creditDebitIndicator)
+        assertEquals("unstructured info one", 
r.reports[0].entries[0].transactionInfos[0].unstructuredRemittanceInformation)
+
+        // Second Entry
+        assertEquals("unstructured info across lines", 
r.reports[0].entries[1].transactionInfos[0].unstructuredRemittanceInformation)
+
+        // Third Entry
 
         // Make sure that round-tripping of entry CamtBankAccountEntry JSON 
works
         for (entry in r.reports.flatMap { it.entries }) {
diff --git 
a/nexus/src/test/resources/iso20022-samples/camt.053/de.camt.053.001.02.xml 
b/nexus/src/test/resources/iso20022-samples/camt.053/de.camt.053.001.02.xml
index 6030482..d297f47 100644
--- a/nexus/src/test/resources/iso20022-samples/camt.053/de.camt.053.001.02.xml
+++ b/nexus/src/test/resources/iso20022-samples/camt.053/de.camt.053.001.02.xml
@@ -28,6 +28,8 @@
                     <Dt>2020-07-03</Dt>
                 </Dt>
             </Bal>
+
+            <!-- Credit due to incoming SCT -->
             <Ntry>
                 <Amt Ccy="EUR">100.00</Amt>
                 <CdtDbtInd>CRDT</CdtDbtInd>
@@ -52,7 +54,6 @@
                         <Issr>DK</Issr>
                     </Prtry>
                 </BkTxCd>
-                <!-- Credit due to incoming SCT -->
                 <NtryDtls>
                     <TxDtls>
                         <Refs>
@@ -100,6 +101,174 @@
                 </NtryDtls>
                 <AddtlNtryInf>SEPA GUTSCHRIFT</AddtlNtryInf>
             </Ntry>
+
+            <!-- Entry to illustrate multiple ustrd elements -->
+            <Ntry>
+                <Amt Ccy="EUR">50.00</Amt>
+                <CdtDbtInd>CRDT</CdtDbtInd>
+                <Sts>BOOK</Sts>
+                <BookgDt>
+                    <Dt>2020-07-02</Dt>
+                </BookgDt>
+                <ValDt>
+                    <Dt>2020-07-04</Dt>
+                </ValDt>
+                <AcctSvcrRef>acctsvcrref-002</AcctSvcrRef>
+                <BkTxCd>
+                    <Domn>
+                        <Cd>PMNT</Cd>
+                        <Fmly>
+                            <Cd>RCDT</Cd>
+                            <SubFmlyCd>ESCT</SubFmlyCd>
+                        </Fmly>
+                    </Domn>
+                    <Prtry>
+                        <Cd>166</Cd>
+                        <Issr>DK</Issr>
+                    </Prtry>
+                </BkTxCd>
+                <!-- Credit due to incoming SCT -->
+                <NtryDtls>
+                    <TxDtls>
+                        <Refs>
+                            <EndToEndId>e2e-002</EndToEndId>
+                        </Refs>
+                        <BkTxCd>
+                            <Domn>
+                                <Cd>PMNT</Cd>
+                                <Fmly>
+                                    <Cd>RCDT</Cd>
+                                    <SubFmlyCd>ESCT</SubFmlyCd>
+                                </Fmly>
+                            </Domn>
+                            <Prtry>
+                                <Cd>NTRF+166</Cd>
+                                <Issr>DK</Issr>
+                            </Prtry>
+                        </BkTxCd>
+                        <RltdPties>
+                            <Dbtr>
+                                <Nm>Debtor One</Nm>
+                            </Dbtr>
+                            <DbtrAcct>
+                                <Id>
+                                    <IBAN>DE52123456789473323175</IBAN>
+                                </Id>
+                            </DbtrAcct>
+                            <Cdtr>
+                                <Nm>Creditor One</Nm>
+                            </Cdtr>
+                        </RltdPties>
+                        <RmtInf>
+                            <Ustrd>unstructured </Ustrd>
+                            <Ustrd>info </Ustrd>
+                            <Ustrd>across </Ustrd>
+                            <Ustrd>lines</Ustrd>
+                        </RmtInf>
+                    </TxDtls>
+                </NtryDtls>
+            </Ntry>
+
+            <!--
+            Credit due to a return resulting from a batch payment initiation 
where only one payment failed.
+            This data was obtained by doing a transaction on a GLS Bank 
account, but we've replaced
+            the account's IBAN with a random one.
+            Note how the original creditor and debtor are preserved and not 
flipped.
+            Unfortunately the original payment didn't have an end-to-end ID, 
so it would be harder
+            to correlate this message to the original payment initiation -->
+            <Ntry>
+                <Amt Ccy="EUR">1.12</Amt>
+                <CdtDbtInd>CRDT</CdtDbtInd>
+                <Sts>BOOK</Sts>
+                <BookgDt>
+                    <Dt>2020-06-30</Dt>
+                </BookgDt>
+                <ValDt>
+                    <Dt>2020-06-30</Dt>
+                </ValDt>
+                <AcctSvcrRef>2020063011423362000</AcctSvcrRef>
+                <BkTxCd>
+                    <Domn>
+                        <Cd>PMNT</Cd>
+                        <Fmly>
+                            <Cd>ICDT</Cd>
+                            <SubFmlyCd>RRTN</SubFmlyCd>
+                        </Fmly>
+                    </Domn>
+                    <Prtry>
+                        <Cd>NRTI+159+00931</Cd>
+                        <Issr>DK</Issr>
+                    </Prtry>
+                </BkTxCd>
+                <NtryDtls>
+                    <TxDtls>
+                        <Refs>
+                            <EndToEndId>NOTPROVIDED</EndToEndId>
+                        </Refs>
+                        <AmtDtls>
+                            <TxAmt>
+                                <Amt Ccy="EUR">1.12</Amt>
+                            </TxAmt>
+                        </AmtDtls>
+                        <BkTxCd>
+                            <Domn>
+                                <Cd>PMNT</Cd>
+                                <Fmly>
+                                    <Cd>ICDT</Cd>
+                                    <SubFmlyCd>RRTN</SubFmlyCd>
+                                </Fmly>
+                            </Domn>
+                            <Prtry>
+                                <Cd>NRTI+159+00931</Cd>
+                                <Issr>DK</Issr>
+                            </Prtry>
+                        </BkTxCd>
+                        <RltdPties>
+                            <Dbtr>
+                                <Nm>Account Owner</Nm>
+                            </Dbtr>
+                            <DbtrAcct>
+                                <Id>
+                                    <IBAN>DE54123456784713474163</IBAN>
+                                </Id>
+                            </DbtrAcct>
+                            <Cdtr>
+                                <Nm>Nonexistant Creditor</Nm>
+                            </Cdtr>
+                            <CdtrAcct>
+                                <Id>
+                                    <IBAN>DE24500105177398216438</IBAN>
+                                </Id>
+                            </CdtrAcct>
+                        </RltdPties>
+                        <RmtInf>
+                            <Ustrd>Retoure SEPA Ueberweisung vom 29.06.2020, 
Rueckgabegrund: AC01 IBAN fehlerhaft und ungültig SVWZ: RETURN, Sammelposten 
Nummer Zwei IBAN: DE2</Ustrd>
+                            <Ustrd>4500105177398216438 BIC: INGDDEFFXXX</Ustrd>
+                        </RmtInf>
+                        <RtrInf>
+                            <OrgnlBkTxCd>
+                                <Prtry>
+                                    <Cd>116</Cd>
+                                    <Issr>DK</Issr>
+                                </Prtry>
+                            </OrgnlBkTxCd>
+                            <Orgtr>
+                                <Id>
+                                    <OrgId>
+                                        <BICOrBEI>GENODEM1GLS</BICOrBEI>
+                                    </OrgId>
+                                </Id>
+                            </Orgtr>
+                            <Rsn>
+                                <Cd>AC01</Cd>
+                            </Rsn>
+                            <AddtlInf>IBAN fehlerhaft und ungültig</AddtlInf>
+                        </RtrInf>
+                    </TxDtls>
+                </NtryDtls>
+                <AddtlNtryInf>Retouren</AddtlNtryInf>
+            </Ntry>
+
             <!-- Credit due to incoming USD transfer -->
             <Ntry>
                 <Amt Ccy="EUR">1000</Amt>

-- 
To stop receiving notification emails like this one, please contact
gnunet@gnunet.org.



reply via email to

[Prev in Thread] Current Thread [Next in Thread]