diff --git a/README.md b/README.md new file mode 100644 index 0000000..9c7ee45 --- /dev/null +++ b/README.md @@ -0,0 +1,30 @@ +#v11 +This is a php library to read ESR v11 files, Swiss banking payments.
+(BESR/VESR/ESR - Einzahlungsscheine mit Referenznummer einlesen) + +## Installation + +Add v11 in your composer.json: + +```js +{ + "require": { + "ticketpark/v11": "~0.1" + } +} +``` + +Now tell composer to download the bundle by running the command: + +``` bash +$ php composer.phar update ticketpark/v11 +``` + +## Example +See [example.php](example/example.php) + + +## License +This bundle is under the MIT license. See the complete license in the bundle: + +[Resources/meta/LICENSE](Resources/meta/LICENSE) \ No newline at end of file diff --git a/Resources/docs/specifications/zkb-vesr-handbuch.pdf b/Resources/docs/specifications/zkb-vesr-handbuch.pdf new file mode 100644 index 0000000..c7a782e Binary files /dev/null and b/Resources/docs/specifications/zkb-vesr-handbuch.pdf differ diff --git a/Resources/meta/LICENSE b/Resources/meta/LICENSE new file mode 100644 index 0000000..bd63d47 --- /dev/null +++ b/Resources/meta/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2014 Ticketpark GmbH + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/Tests/v11/Tests/v11Test.php b/Tests/v11/Tests/v11Test.php new file mode 100644 index 0000000..d32ff83 --- /dev/null +++ b/Tests/v11/Tests/v11Test.php @@ -0,0 +1,150 @@ +assertTrue($v11->validate()); + } + + public function testValidateTransactions() + { + $v11 = new v11(array( + "002012000002123456000000001291146290519 7500000000000014102414102514102600000000000000000000120", + "012012000002123456000000001290507716881 50000008 000014102314102414102400552073900000000000000", + "002012000002123456000000001288602003992 2550000000000014102414102414102400000000000000000000000", + "002012000002123456000000001290068583973 50000008 000014102314102414102400489435900000000000000", + "999012000002999999999999999999999999999000000020050000000000004141027000000120000000004 ", + )); + $v11->setParticipantIdentifier('123456'); + + $this->assertSame('002', $v11->getTransactionRecords()[0]->getTransactionCode()->getTransactionCode()); + $this->assertSame('credit', $v11->getTransactionRecords()[0]->getTransactionCode()->getTransactionType()); + $this->assertSame('esr', $v11->getTransactionRecords()[0]->getTransactionCode()->getReceiptType()); + $this->assertSame('banking', $v11->getTransactionRecords()[0]->getTransactionCode()->getPaymentType()); + + $this->assertSame('012000002', $v11->getTransactionRecords()[0]->getBankingAccount()); + $this->assertSame('123456000000001291146290519', $v11->getTransactionRecords()[0]->getReferenceNumber()); + $this->assertSame('12345600000000129114629051', $v11->getTransactionRecords()[0]->getReferenceNumberWithoutCheckDigit()); + $this->assertSame('00000000129114629051', $v11->getTransactionRecords()[0]->getCustomReferenceNumber()); + $this->assertSame(129114629051, $v11->getTransactionRecords()[0]->getCustomReferenceNumber(true)); + $this->assertSame(75, $v11->getTransactionRecords()[0]->getAmount()); + $this->assertSame('0000000000', $v11->getTransactionRecords()[0]->getInternalBankReference()); + $this->assertSame('2014-10-24', $v11->getTransactionRecords()[0]->getDatePaid()->format('Y-m-d')); + $this->assertSame('2014-10-25', $v11->getTransactionRecords()[0]->getDateProcessed()->format('Y-m-d')); + $this->assertSame('2014-10-26', $v11->getTransactionRecords()[0]->getDateCreditNote()->format('Y-m-d')); + $this->assertSame('000000000', $v11->getTransactionRecords()[0]->getMicrofilmReference()); + $this->assertSame('0', $v11->getTransactionRecords()[0]->getRejectCode()); + $this->assertNull($v11->getTransactionRecords()[0]->getDateValuta()); + $this->assertSame(1.2, $v11->getTransactionRecords()[0]->getFee()); + + $this->assertSame('999', $v11->getTotalRecord()->getTransactionCode()->getTransactionCode()); + $this->assertSame('012000002', $v11->getTotalRecord()->getBankingAccount()); + $this->assertSame('999999999999999999999999999', $v11->getTotalRecord()->getSortingKey()); + $this->assertSame(4, $v11->getTotalRecord()->getNumberOfTransactions()); + $this->assertSame('2014-10-27', $v11->getTotalRecord()->getDateFileCreation()->format('Y-m-d')); + $this->assertSame(1.2, $v11->getTotalRecord()->getTotalFees()); + } + + + public function testValidateTotalLineTooShort() + { + $v11 = new v11(array( + "002012000002123456000000001291146290519 7500000000000014102414102414102400000000000000000000000", + "012012000002123456000000001290507716881 50000008 000014102314102414102400552073900000000000120", + "002012000002123456000000001288602003992 2550000000000014102414102414102400000000000000000000000", + "002012000002123456000000001290068583973 50000008 000014102314102414102400489435900000000000000", + "99901200000299999999999999999999999999900000020050000000000004141027000000120000000004 ", + )); + $this->assertFalse($v11->validate()); + $this->assertSame('The total line contains 86 instead of 87 characters', $v11->getError()); + } + + public function testValidateTotalLineBadTransactionCode() + { + $v11 = new v11(array( + "002012000002123456000000001291146290519 7500000000000014102414102414102400000000000000000000000", + "012012000002123456000000001290507716881 50000008 000014102314102414102400552073900000000000120", + "002012000002123456000000001288602003992 2550000000000014102414102414102400000000000000000000000", + "002012000002123456000000001290068583973 50000008 000014102314102414102400489435900000000000000", + "111012000002999999999999999999999999999000000020050000000000004141027000000120000000004 ", + )); + $this->assertFalse($v11->validate()); + $this->assertSame('The total line does not contain a valid transaction code', $v11->getError()); + } + + public function testValidateTotalLineInvalidContents() + { + $v11 = new v11(array( + "002012000002123456000000001291146290519 7500000000000014102414102414102400000000000000000000000", + "012012000002123456000000001290507716881 50000008 000014102314102414102400552073900000000000120", + "002012000002123456000000001288602003992 2550000000000014102414102414102400000000000000000000000", + "002012000002123456000000001290068583973 50000008 000014102314102414102400489435900000000000000", + "99901200000299999999999999999999999999900A000020050000000000004141027000000120000000004 ", + )); + $this->assertFalse($v11->validate()); + $this->assertSame('The total line contains invalid characters. It may only contain digits and spaces', $v11->getError()); + } + + public function testValidateTransactionLineTooShort() + { + $v11 = new v11(array( + "002012000002123456000000001291146290519 7500000000000014102414102414102400000000000000000000000", + "012012000002123456000000001290507716881 50000008 000014102314102414102400552073900000000000120", + "002012000002123456000000001288602003992 255000000000001410241410241410240000000000000000000000", + "002012000002123456000000001290068583973 50000008 000014102314102414102400489435900000000000000", + "999012000002999999999999999999999999999000000020050000000000004141027000000120000000004 ", + )); + $this->assertFalse($v11->validate()); + $this->assertSame('Line number 3 contains 99 instead of 100 characters', $v11->getError()); + } + + public function testValidateTransactionLineBadTransactionCode() + { + $v11 = new v11(array( + "002012000002123456000000001291146290519 7500000000000014102414102414102400000000000000000000000", + "012012000002123456000000001290507716881 50000008 000014102314102414102400552073900000000000120", + "007012000002123456000000001288602003992 2550000000000014102414102414102400000000000000000000000", + "002012000002123456000000001290068583973 50000008 000014102314102414102400489435900000000000000", + "999012000002999999999999999999999999999000000020050000000000004141027000000120000000004 ", + )); + $this->assertFalse($v11->validate()); + $this->assertSame('Line number 3 contains does not contain a valid transaction code', $v11->getError()); + } + + public function testValidateTransactionLineInvalidContents() + { + $v11 = new v11(array( + "002012000002123456000000001291146290519 7500000000000014102414102414102400000000000000000000000", + "012012000002123456000000001290507716881 50000008 000014102314102414102400552073900000000000120", + "002012000002123456000000001288602003992 255000000A000014102414102414102400000000000000000000000", + "002012000002123456000000001290068583973 50000008 000014102314102414102400489435900000000000000", + "999012000002999999999999999999999999999000000020050000000000004141027000000120000000004 ", + )); + $this->assertFalse($v11->validate()); + $this->assertSame('Line number 3 contains invalid characters. It may only contain digits and spaces', $v11->getError()); + } + + public function testValidateNotMatchingNumberOfTransactions() + { + $v11 = new v11(array( + "002012000002123456000000001291146290519 7500000000000014102414102414102400000000000000000000000", + "012012000002123456000000001290507716881 50000008 000014102314102414102400552073900000000000120", + "002012000002123456000000001290068583973 50000008 000014102314102414102400489435900000000000000", + "999012000002999999999999999999999999999000000020050000000000004141027000000120000000004 ", + )); + $this->assertFalse($v11->validate()); + $this->assertSame('The number of 3 transactions does not match the number of 4 transactions according to the total line', $v11->getError()); + } +} diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..4df008a --- /dev/null +++ b/composer.json @@ -0,0 +1,17 @@ +{ + "name": "ticketpark/v11", + "type": "library", + "description": "A php library to process ESR/BESR v11 files (Swiss payment slips / Einzahlungsscheine mit Referenznummer)", + "keywords": ["besr", "esr", "v11", "einzahlungsschein", "payment slip"], + "homepage": "https://github.com/Ticketpark/v11", + "license": "MIT", + "authors": [ + {"name": "Manuel Reinhard", "email": "manu@sprain.ch"} + ], + "require": { + "php": ">=5.4" + }, + "autoload": { + "psr-4": { "Ticketpark\\v11\\": "lib/v11" } + } +} diff --git a/example/example.php b/example/example.php new file mode 100644 index 0000000..b85c155 --- /dev/null +++ b/example/example.php @@ -0,0 +1,49 @@ +getCustomReferenceNumber() later on. +$v11->setParticipantIdentifier('123456'); + +// Set content - by file or array +#$v11->setFile('/path/to/file/besr.v11'); +$v11->setLines(array( + "002012000002123456000000001291146290519 7500000000000014102414102414102400000000000000000000000", + "012012000002123456000000001290507716881 50000008 000014102314102414102400552073900000000000120", + "002012000002123456000000001288602003992 2550000000000014102414102414102400000000000000000000000", + "002012000002123456000000001290068583973 50000008 000014102314102414102400489435900000000000000", + "999012000002999999999999999999999999999000000020050000000000004141027000000120000000004 ", +)); + +// Validate the contents +if(!$v11->validate()) { + print $v11->getError(); +} + +// Get the transaction records - see Ticketpark\v11\Record\TransactionRecord for more methods +foreach($v11->getTransactionRecords() as $record){ + var_dump( + array( + 'Transaction type' => $record->getTransactionCode()->getTransactionType(), + 'Payment type' => $record->getTransactionCode()->getPaymentType(), + 'Amount' => $record->getSignedAmount(), + 'Full Reference number' => $record->getReferenceNumber(), + 'Customer numeric reference number' => $record->getCustomReferenceNumber(true), + 'Fee' => $record->getFee(), + ) + ); +} + +// Get the total record - see Ticketpark\v11\Record\TotalRecord for more methods +$total = $v11->getTotalRecord(); +var_dump( + array( + 'Transaction type' => $total->getTransactionCode()->getTransactionType(), + 'Amount' => $total->getSignedAmount(), + 'Number of transactions' => $total->getNumberOfTransactions(), + 'Total Fees' => $total->getTotalFees(), + ) +); diff --git a/lib/v11/Record/TotalRecord.php b/lib/v11/Record/TotalRecord.php new file mode 100644 index 0000000..df8523e --- /dev/null +++ b/lib/v11/Record/TotalRecord.php @@ -0,0 +1,116 @@ +transactionCode = $transactionCode; + + return $this; + } + + public function getTransactionCode() + { + return $this->transactionCode; + } + + public function setBankingAccount($bankingAccount) + { + $this->bankingAccount = $bankingAccount; + + return $this; + } + + public function getBankingAccount() + { + return $this->bankingAccount; + } + + public function setSortingKey($sortingKey) + { + $this->sortingKey = $sortingKey; + + return $this; + } + + public function getSortingKey() + { + return $this->sortingKey; + } + + public function setAmount($amount) + { + $this->amount = $amount; + + return $this; + } + + public function getAmount() + { + return $this->amount; + } + + public function getSignedAmount() + { + if ($this->getTransactionCode()->getTransactionType() == 'storno') { + + return $this->amount * -1; + } + + return $this->amount; + } + + public function setNumberOfTransactions($numberOfTransactions) + { + $this->numberOfTransactions = $numberOfTransactions; + + return $this; + } + + public function getNumberOfTransactions() + { + return $this->numberOfTransactions; + } + + public function setDateFileCreation(\DateTime $dateFileCreation = null) + { + $this->dateFileCreation = $dateFileCreation; + + return $this; + } + + public function getDateFileCreation() + { + return $this->dateFileCreation; + } + + public function setTotalFees($totalFees) + { + $this->totalFees = $totalFees; + + return $this; + } + + public function getTotalFees() + { + return $this->totalFees; + } +} \ No newline at end of file diff --git a/lib/v11/Record/TransactionCode/TransactionCode.php b/lib/v11/Record/TransactionCode/TransactionCode.php new file mode 100644 index 0000000..15d06fc --- /dev/null +++ b/lib/v11/Record/TransactionCode/TransactionCode.php @@ -0,0 +1,32 @@ +transactionCode = $transactionCode; + + return $this; + } + + public function getTransactionCode() + { + return $this->transactionCode; + } + + public function setParticipantIdentifier($participantIdentifier) + { + $this->participantIdentifier = $participantIdentifier; + + return $this; + } + + public function getParticipantIdentifier() + { + return $this->participantIdentifier; + } + + public function setBankingAccount($bankingAccount) + { + $this->bankingAccount = $bankingAccount; + + return $this; + } + + public function getBankingAccount() + { + return $this->bankingAccount; + } + + public function setReferenceNumber($referenceNumber) + { + $this->referenceNumber = $referenceNumber; + + return $this; + } + + public function getReferenceNumber() + { + return $this->referenceNumber; + } + + public function getReferenceNumberWithoutCheckDigit() + { + return substr($this->referenceNumber, 0, -1); + } + + public function getCustomReferenceNumber($numeric = false) + { + $customReferenceNumber = substr($this->getReferenceNumberWithoutCheckDigit(), strlen($this->getParticipantIdentifier())); + + if ($numeric) { + + return $customReferenceNumber * 1; + } + + return $customReferenceNumber; + } + + public function setAmount($amount) + { + $this->amount = $amount; + + return $this; + } + + public function getAmount() + { + return $this->amount; + } + + public function getSignedAmount() + { + if ($this->getTransactionCode()->getTransactionType() == 'storno') { + + return $this->amount * -1; + } + + return $this->amount; + } + + public function setInternalBankReference($internalBankReference) + { + $this->internalBankReference = $internalBankReference; + + return $this; + } + + public function getInternalBankReference() + { + return $this->internalBankReference; + } + + public function setDatePaid(\DateTime $datePaid = null) + { + $this->datePaid = $datePaid; + + return $this; + } + + public function getDatePaid() + { + return $this->datePaid; + } + + public function setDateProcessed(\DateTime $dateProcessed = null) + { + $this->dateProcessed = $dateProcessed; + + return $this; + } + + public function getDateProcessed() + { + return $this->dateProcessed; + } + + public function setDateCreditNote(\DateTime $dateCreditNote = null) + { + $this->dateCreditNote = $dateCreditNote; + + return $this; + } + + public function getDateCreditNote() + { + return $this->dateCreditNote; + } + + public function setMicrofilmReference($microfilmReference) + { + $this->microfilmReference = $microfilmReference; + + return $this; + } + + public function getMicrofilmReference() + { + return $this->microfilmReference; + } + + public function setRejectCode($rejectCode) + { + $this->rejectCode = $rejectCode; + + return $this; + } + + public function getRejectCode() + { + return $this->rejectCode; + } + + public function setDateValuta(\DateTime $dateValuta = null) + { + $this->dateValuta = $dateValuta; + + return $this; + } + + public function getDateValuta() + { + return $this->dateValuta; + } + + public function setFee($fee) + { + $this->fee = $fee; + + return $this; + } + + public function getFee() + { + return $this->fee; + } +} \ No newline at end of file diff --git a/lib/v11/v11.php b/lib/v11/v11.php new file mode 100644 index 0000000..3301438 --- /dev/null +++ b/lib/v11/v11.php @@ -0,0 +1,303 @@ +setLines($contents); + } elseif (null !== $contents) { + $this->setFile($contents); + } + } + + public function setParticipantIdentifier($participantIdentifier) + { + $this->participantIdentifier = $participantIdentifier; + } + + public function getParticipantIdentifier() + { + return $this->participantIdentifier; + } + + /** + * Get the transaction records + * + * @return array + */ + public function getTransactionRecords() + { + $records = array(); + + foreach ($this->getTransactionLines() as $line) { + $records[] = $this->getTransactionRecord($line); + } + + return $records; + } + + /** + * Get the total record + * + * @return TotalRecord + */ + public function getTotalRecord() + { + $record = new TotalRecord(); + $line = $this->getTotalLine(); + + if ($transactionCode = TransactionCodeFactory::create(substr($line, 0, 3))) { + $record->setTransactionCode($transactionCode); + } + + $record + ->setBankingAccount(substr($line, 3, 9)) + ->setSortingKey(substr($line, 12, 27)) + ->setAmount(substr($line, 39, 12) / 100) + ->setNumberOfTransactions(substr($line, 51, 12) * 1) + ->setDateFileCreation($this->createDate(substr($line, 63, 6))) + ->setTotalFees((substr($line, 69, 9)) / 100); + + return $record; + } + + /** + * Set path to file containing v11 contents + * + * @param string $file + */ + public function setFile($file) + { + $this->file = $file; + + return $this; + } + + /** + * Get path to file + * + * @return string + */ + public function getFile() + { + return $this->file; + } + + /** + * Set the raw lines + * + * @param array $lines + */ + public function setLines(array $lines) + { + $newLines = array(); + foreach($lines as $line) { + $newLines[] = trim($line); + } + + $this->lines = $newLines; + + return $this; + } + + /** + * Get the raw lines + * + * @return array + */ + public function getLines() + { + return $this->lines; + } + + /** + * Get the raw transaction lines + * + * @return array + */ + public function getTransactionLines() + { + return array_slice($this->getLines(), 0, -1); + } + + /** + * Get the raw total line + * + * @return string + */ + public function getTotalLine() + { + $lines = $this->getLines(); + + return array_pop($lines); + } + + /** + * Creates a transaction record + * + * @param $line + * @return Record + */ + protected function getTransactionRecord($line) + { + $record = new TransactionRecord(); + + if ($transactionCode = TransactionCodeFactory::create(substr($line, 0, 3))) { + $record->setTransactionCode($transactionCode); + } + + $record->setParticipantIdentifier($this->getParticipantIdentifier()) + ->setBankingAccount(substr($line, 3, 9)) + ->setReferenceNumber(substr($line, 12, 27)) + ->setAmount(substr($line, 39, 10) / 100) + ->setInternalBankReference(substr($line, 49, 10)) + ->setDatePaid($this->createDate(substr($line, 59, 6))) + ->setDateProcessed($this->createDate(substr($line, 65, 6))) + ->setDateCreditNote($this->createDate(substr($line, 71, 6))) + ->setMicrofilmReference(substr($line, 77, 9)) + ->setRejectCode(substr($line, 86, 1)) + ->setDateValuta($this->createDate(substr($line, 87, 6))) + ->setFee((substr($line, 96, 4)) / 100); + + return $record; + } + + /** + * Validate the v11 contents + * + * @return bool + */ + public function validate() + { + + if (null !== $this->getFile()) { + if (is_readable($this->getFile())) { + $this->setLines(file($this->getFile(), FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES)); + } else { + $this->setError(sprintf('The provided file %s is not readable', $this->getFile())); + + return false; + } + } + + + if (count($this->getLines()) == 0) { + + return true; + } + + $lines = $this->getLines(); + + // Last line is the total record + // It must have 87 characters and only consist of digits and spaces + $lastLine = array_pop($lines); + if (strlen($lastLine) != 87) { + $this->setError(sprintf('The total line contains %s instead of 87 characters', strlen($lastLine))); + + return false; + } + + if (!preg_match('/^[\d ]+$/', $lastLine)) { + $this->setError('The total line contains invalid characters. It may only contain digits and spaces'); + + return false; + } + + // Transaction records must have 100 characters and only consist of digits and spaces + $i=1; + foreach ($lines as $line) { + if (strlen($line) != 100) { + $this->setError(sprintf('Line number %s contains %s instead of 100 characters', $i, strlen($line))); + + return false; + } + + if (!preg_match('/^[\d ]+$/', $line)) { + $this->setError(sprintf('Line number %s contains invalid characters. It may only contain digits and spaces', $i)); + + return false; + } + + $i++; + } + + //Number of transactions must match + if ($this->getTotalRecord()->getNumberOfTransactions() != count($lines)) { + $this->setError(sprintf('The number of %s transactions does not match the number of %s transactions according to the total line', count($lines), $this->getTotalRecord()->getNumberOfTransactions())); + + return false; + } + + + // All records must have a transaction code + if (null == $this->getTotalRecord()->getTransactionCode()) { + $this->setError(sprintf('The total line does not contain a valid transaction code')); + + return false; + } + + $i=1; + foreach($this->getTransactionRecords() as $record){ + if (null == $record->getTransactionCode()) { + $this->setError(sprintf('Line number %s contains does not contain a valid transaction code', $i)); + + return false; + } + $i++; + } + + return true; + } + + /** + * Get the last error message + * + * @return string + */ + public function getError() + { + return $this->error; + } + + /** + * Set error message + * + * @param string $error + */ + protected function setError($error) + { + $this->error = $error; + } + + /** + * Converts a 6-character string (YYMMDD) to a DateTime + * @param $string + * @return \DateTime|null + */ + protected function createDate($string) + { + if ('000000' == $string) { + + return null; + } + + return new \DateTime(substr($string, 0, 2) . '-' . substr($string, 2, 2) . '-' . substr($string, 4, 2)); + } +} \ No newline at end of file diff --git a/phpunit.xml b/phpunit.xml new file mode 100644 index 0000000..4386ce8 --- /dev/null +++ b/phpunit.xml @@ -0,0 +1,13 @@ + + + + + ./Tests + + + + + ./Tests + + + \ No newline at end of file