diff --git a/src/Payutc/Bom/Purchase.php b/src/Payutc/Bom/Purchase.php index 702c0b2..344f38f 100644 --- a/src/Payutc/Bom/Purchase.php +++ b/src/Payutc/Bom/Purchase.php @@ -32,6 +32,7 @@ public static function getNbSell($obj_id, $fun_id, $start=null, $end=null, $tick ->innerJoin('pur', 't_transaction_tra', 'tra', 'pur.tra_id = tra.tra_id') ->where('pur.obj_id = :obj_id')->setParameter('obj_id', $obj_id) ->andWhere('tra.fun_id = :fun_id')->setParameter('fun_id', $fun_id) + ->andWhere("tra.tra_status = 'V'") ->andWhere('pur.pur_removed = 0'); if($start != null) { @@ -73,6 +74,7 @@ public static function getRevenue($fun_id, $app_id=null, $start=null, $end=null, ->from('t_purchase_pur', 'pur') ->innerJoin('pur', 't_transaction_tra', 'tra', 'pur.tra_id = tra.tra_id') ->andWhere('tra.fun_id = :fun_id')->setParameter('fun_id', $fun_id) + ->andWhere("tra.tra_status = 'V'") ->andWhere('pur.pur_removed = 0'); if($app_id != null) { @@ -105,10 +107,11 @@ public static function getRevenue($fun_id, $app_id=null, $start=null, $end=null, public static function getPurchaseById($pur_id) { $qb = Dbal::createQueryBuilder(); - $qb->select('*', 'tra.tra_date') - ->from('t_purchase_pur', 'pur') - ->innerJoin('pur', 't_transaction_tra', 'tra', 'pur.tra_id = tra.tra_id') - ->where('pur.pur_id = :pur_id') + $qb->select('*', 'tra.tra_date') + ->from('t_purchase_pur', 'pur') + ->innerJoin('pur', 't_transaction_tra', 'tra', 'pur.tra_id = tra.tra_id') + ->where('pur.pur_id = :pur_id') + ->andWhere("tra.tra_status = 'V'") ->setParameter('pur_id', $pur_id); return $qb->execute()->fetch(); } @@ -158,7 +161,8 @@ public static function transaction($usr_id_buyer, $items, $app_id, $fun_id, $usr 'usr_id_seller' => $usr_id_seller, 'app_id' => $app_id, 'fun_id' => $fun_id, - 'tra_ip' => $pur_ip, + 'tra_status' => 'V', + 'tra_ip' => $pur_ip, )); $transactionId = $conn->lastInsertId(); @@ -204,6 +208,7 @@ public static function getRank($fun_id, $obj_id, $start, $end, $top, $sort_by) { ->innerJoin('pur', 't_transaction_tra', 'tra', 'pur.tra_id = tra.tra_id') ->where('usr.usr_id = tra.usr_id_buyer') ->andWhere('tra.fun_id = :fun_id')->setParameter('fun_id', $fun_id) + ->andWhere("tra.tra_status = 'V'") ->andWhere('pur.pur_removed = 0'); if($obj_id != null) { @@ -247,6 +252,7 @@ public static function getPurchasesForUser($usr_id, $time_limit=null) ->from('t_purchase_pur', 'pur') ->innerJoin('pur', 't_transaction_tra', 'tra', 'pur.tra_id = tra.tra_id') ->Where('usr_id_buyer = :usr_id') + ->andWhere("tra.tra_status = 'V'") ->andWhere('pur_removed = 0') ->setParameter('usr_id', $usr_id); if ($time_limit) { diff --git a/src/Payutc/Bom/Transaction.php b/src/Payutc/Bom/Transaction.php index 7983121..a31c5ab 100644 --- a/src/Payutc/Bom/Transaction.php +++ b/src/Payutc/Bom/Transaction.php @@ -22,22 +22,79 @@ namespace Payutc\Bom; use \Payutc\Log; +use \Payutc\Bom\User; use \Payutc\Db\Dbal; +use \Payutc\Exception\NotEnoughMoney; +use \Payutc\Exception\TransactionAborted; use \Payutc\Exception\TransactionNotFound; +use \Payutc\Exception\TransactionAlreadyValidated; class Transaction { protected $id; protected $date; + protected $validatedDate; protected $buyerId; protected $sellerId; protected $appId; protected $funId; protected $purchases; + protected $status; + protected $callbackUrl; + protected $returnUrl; + protected $token; + + // --- Simple getters + + public function getId(){ + return $this->id; + } + + public function getDate(){ + return $this->date; + } + + public function getValidatedDate(){ + return $this->validatedDate; + } + + public function getBuyerId(){ + return $this->buyerId; + } + + public function getSellerId(){ + return $this->sellerId; + } + + public function getFunId(){ + return $this->funId; + } public function getPurchases(){ return $this->purchases; } + public function getStatus(){ + return $this->status; + } + + public function getCallbackUrl(){ + return $this->callbackUrl; + } + + public function getReturnUrl(){ + return $this->returnUrl; + } + + public function getToken(){ + // TODO + } + + public function setEmail(){ + // TODO + throw exception if invalid + } + + // --- Helpers + public function getMontantTotal(){ $total = 0; foreach($this->purchases as $purchase){ @@ -45,12 +102,78 @@ public function getMontantTotal(){ } return $total; } + + public function validate(){ + if($this->status == 'V'){ + throw new TransactionAlreadyValidated(); + } + + if($this->status == 'A'){ + throw new TransactionAborted(); + } + + $conn = Dbal::conn(); + $conn->beginTransaction(); + + try { + // Set the status to validated + $now = new \DateTime(); + $rows = $conn->update('t_transaction_tra', + array('tra_status' => 'V', + 'tra_validated' => $now), + array('tra_id' => $this->id), + array("string", "datetime", "integer") + ); + + // Check that the transaction exists (this should never happen as object is instanciated) + if ($rows == 0) { + Log::error("Transaction: Transaction $idTrans not found when validating"); + throw new TransactionNotFound("La transaction $idTrans n'existe pas"); + } + + // Remove stock for each product + foreach($this->purchases as $purchase){ + Product::decStockById($purchase['obj_id'], $purchase['pur_qte']); + } + + // Remove money from user + if($this->buyerId != null){ + $total = $this->getMontantTotal(); + $buyer = User::getById($this->buyerId); + + if($total > User::getCreditById($this->buyerId)){ + throw new NotEnougMoney(); + } + + User::decCreditById($this->buyerId, $this->getMontantTotal()); + } + else { + // TODO Check that payline payment equals transaction total amount + } + + $conn->commit(); + } + catch (\Exception $e) { + $conn->rollback(); + throw $e; + } + + // Updated the BOM + $this->validatedDate = $now; + $this->status = 'V'; + + // Send callback if any + // TODO + } + + // --- Generators static public function getById($idTrans){ Log::debug("Transaction: getById($idTrans)"); $query = Dbal::createQueryBuilder() - ->select('tra.tra_id', 'tra.tra_date', 'tra.usr_id_buyer', 'tra.usr_id_seller', 'tra.poi_id', + ->select('tra.tra_id', 'tra.tra_date', 'tra.tra_validated', 'tra.usr_id_buyer', 'tra.usr_id_seller', 'tra.app_id', + 'tra.tra_status', 'tra.tra_callback_url', 'tra.tra_return_url', 'tra.tra_token', 'tra.fun_id', 'pur.pur_id', 'pur.obj_id', 'pur.pur_qte', 'pur.pur_unit_price', 'pur.pur_price', 'pur.pur_removed') ->from('t_transaction_tra', 'tra') @@ -61,7 +184,7 @@ static public function getById($idTrans){ // Check that the transaction exists if ($query->rowCount() == 0) { - Log::debug("Transaction: Transaction $idTrans not found"); + Log::warn("Transaction: Transaction $idTrans not found"); throw new TransactionNotFound("La transaction $idTrans n'existe pas"); } @@ -71,9 +194,15 @@ static public function getById($idTrans){ $transaction = new Transaction(); $transaction->id = $don['tra_id']; $transaction->date = $don['tra_date']; + $transaction->validatedDate = $don['tra_validated']; $transaction->buyerId = $don['usr_id_buyer']; $transaction->sellerId = $don['usr_id_seller']; $transaction->appId = $don['app_id']; + $transaction->funId = $don['fun_id']; + $transaction->status = $don['tra_status']; + $transaction->callbackUrl = $don['tra_callback_url']; + $transaction->returnUrl = $don['tra_return_url']; + $transaction->token = $don['tra_token']; $transaction->purchases = array(); do { $transaction->purchases[] = array( @@ -89,5 +218,66 @@ static public function getById($idTrans){ return $transaction; } + static public function create($buyerId, $sellerId, $appId, $funId, $items, $callbackUrl = null, $returnUrl = null){ + $conn = Dbal::conn(); + // TODO check que les articles existent et que leurs prix sont ok (cf poss3) + + try { + $conn->beginTransaction(); + + // Insert the transaction + $conn->insert('t_transaction_tra', array( + 'tra_date' => new \DateTime(), + 'usr_id_buyer' => $buyerId, + 'usr_id_seller' => $sellerId, + 'app_id' => $appId, + 'fun_id' => $funId, + 'tra_status' => 'W', + 'tra_callback_url' => $callbackUrl, + 'tra_return_url' => $returnUrl + ), array("datetime", "integer", "integer", "integer", "integer", "string", "string", "string", "string")); + + $transactionId = $conn->lastInsertId(); + + // Build the purchases (transaction ID is required here) + foreach ($items as $itm) { + $conn->insert('t_purchase_pur', array( + 'tra_id' => $transactionId, + 'obj_id' => $itm['id'], + 'pur_qte' => $itm['qte'], + 'pur_price' => $itm['price'] * $itm['qte'], + 'pur_unit_price' => $itm['price'], + ), array("integer", "integer", "integer", "integer", "integer")); + } + + $conn->commit(); + } + catch (\Exception $e) { + $conn->rollback(); + throw $e; + } + + return self::getById($transactionId); + } + static public function createAndValidate($buyerId, $sellerId, $appId, $funId, $items, $callbackUrl = null, $returnUrl = null){ + $conn = Dbal::conn(); + + try { + // This is the only real transaction, all other nested transactions are virtual + // See http://docs.doctrine-project.org/projects/doctrine-dbal/en/latest/reference/transactions.html + $conn->beginTransaction(); + + $transaction = self::create($buyerId, $sellerId, $appId, $funId, $items, $callbackUrl, $returnUrl); + $transaction->validate(); + + $conn->commit(); + } + catch (\Exception $e){ + $conn->rollback(); + throw $e; + } + + return $transaction; + } } \ No newline at end of file diff --git a/src/Payutc/Exception/NotEnoughMoney.php b/src/Payutc/Exception/NotEnoughMoney.php new file mode 100644 index 0000000..6ddc88f --- /dev/null +++ b/src/Payutc/Exception/NotEnoughMoney.php @@ -0,0 +1,5 @@ +addSql("ALTER TABLE `t_transaction_tra` + ADD `tra_validated` DATETIME NULL COMMENT 'Time when transaction was validated' AFTER `tra_date`, ADD `tra_status` ENUM('W', 'V', 'A') NOT NULL DEFAULT 'W' COMMENT 'Transaction status' AFTER `fun_id`, ADD `tra_callback_url` VARCHAR(200) NULL COMMENT 'URL to call when status changes' AFTER `tra_status`, ADD `tra_return_url` VARCHAR(200) NULL COMMENT 'Return URL at the end of the transaction' AFTER `tra_callback_url`, ADD `tra_token` VARCHAR(32) NULL COMMENT 'Transaction token for WEBSALE' AFTER `tra_return_url`, CHANGE `poi_id` `app_id` INT(11) unsigned NOT NULL COMMENT 'ID of the application that created this transaction', + CHANGE `usr_id_buyer` `usr_id_buyer` INT(11) UNSIGNED NULL COMMENT 'Identifiant de l\'acheteur', + CHANGE `usr_id_seller` `usr_id_seller` INT(11) UNSIGNED NULL COMMENT 'Identifiant du vendeur', ADD INDEX (`tra_status`)"); $this->addSql("ALTER TABLE `t_paybox_pay` @@ -49,10 +52,13 @@ public function down(Schema $schema) $this->addSql("ALTER TABLE `t_transaction_tra` DROP INDEX `tra_status`, + DROP `tra_validated`, DROP `tra_status`, DROP `tra_callback_url`, DROP `tra_return_url`, DROP `tra_token`, - CHANGE `app_id` `poi_id` int(11) unsigned NOT NULL COMMENT 'Identifiant du point de vente'"); + CHANGE `app_id` `poi_id` int(11) unsigned NOT NULL COMMENT 'Identifiant du point de vente', + CHANGE `usr_id_buyer` `usr_id_buyer` INT(11) UNSIGNED NOT NULL COMMENT 'Identifiant de l\'acheteur', + CHANGE `usr_id_seller` `usr_id_seller` INT(11) UNSIGNED NOT NULL COMMENT 'Identifiant du vendeur'"); } } diff --git a/tests/TransactionRodbTest.php b/tests/TransactionRodbTest.php index f41cf4c..57154d5 100644 --- a/tests/TransactionRodbTest.php +++ b/tests/TransactionRodbTest.php @@ -49,7 +49,7 @@ public function testRetrieve(){ } /** - * Test the user is not blocked in all Payutc + * Test retrieving an unknown transaction * * @expectedException \Payutc\Exception\TransactionNotFound * @expectedExceptionMessage La transaction 742 n'existe pas diff --git a/tests/TransactionRwdbTest.php b/tests/TransactionRwdbTest.php new file mode 100644 index 0000000..b44b4a3 --- /dev/null +++ b/tests/TransactionRwdbTest.php @@ -0,0 +1,131 @@ +computeDataset(array( + 'products.yml', + 'users.yml', + 'fundations.yml', + 'purchase.yml' + )); + } + + public function testCreate(){ + $items = array( + array( + 'id' => 4, + 'qte' => 1, + 'price' => 150, + ), + array( + 'id' => 4, + 'qte' => 3, + 'price' => 150, + ), + ); + $transaction = Transaction::create(9447, 9447, 51, 1, $items, null, null); + + $this->assertEquals(600, $transaction->getMontantTotal()); + } + + public function testCreateWithNoBuyer(){ + $items = array( + array( + 'id' => 4, + 'qte' => 1, + 'price' => 150, + ), + array( + 'id' => 4, + 'qte' => 3, + 'price' => 150, + ), + ); + $transaction = Transaction::create(null, null, 51, 1, $items, null, null); + + $this->assertEquals(600, $transaction->getMontantTotal()); + } + + public function testValidate(){ + $transaction = Transaction::getById(12); + $transaction->validate(); + + $this->assertEquals('V', $transaction->getStatus()); + + $u = new User("mguffroy"); + $this->assertEquals(4660, $u->getCredit()); + + $p = Product::getOne(5, 1); + $this->assertEquals(40, $p['stock']); + + $r = Purchase::getNbSell(5, 1); + $this->assertEquals(4, $r); + } + + public function testCreateAndValidate(){ + $items = array( + array( + 'id' => 5, + 'qte' => 1, + 'price' => 170, + ), + array( + 'id' => 5, + 'qte' => 1, + 'price' => 170, + ), + ); + + $transaction = Transaction::createAndValidate(9447, 9447, 51, 1, $items, null, null); + + $this->assertEquals('V', $transaction->getStatus()); + + $u = new User("mguffroy"); + $this->assertEquals(4660, $u->getCredit()); + + $p = Product::getOne(5,1); + $this->assertEquals(40, $p['stock']); + + $r = Purchase::getNbSell(5, 1); + $this->assertEquals(4, $r); + } + + /** + * Test validating an already validated transaction + * + * @expectedException \Payutc\Exception\TransactionAlreadyValidated + * @requires PHP 5.4 + */ + public function testAlreadyValidatedTransaction() + { + $transaction = Transaction::getById(1); + $transaction->validate(); + } + + /** + * Test validating an aborted transaction + * + * @expectedException \Payutc\Exception\TransactionAborted + * @requires PHP 5.4 + */ + public function testAbortedTransaction() + { + $transaction = Transaction::getById(13); + $transaction->validate(); + } + + // TODO try buying with not enough credit, validating with not enough credit, adding wrong articles +} + diff --git a/tests/seed/purchase.yml b/tests/seed/purchase.yml index 301798a..4e35ca6 100644 --- a/tests/seed/purchase.yml +++ b/tests/seed/purchase.yml @@ -7,96 +7,137 @@ t_transaction_tra: app_id: 51 fun_id: 1 tra_ip: 1.2.3.4 + tra_status: 'V' - tra_id: 2 tra_date: "2013-04-07 18:32:25" + tra_validated: "2013-04-07 18:32:25" usr_id_buyer: 9447 usr_id_seller: 9447 app_id: 51 fun_id: 1 tra_ip: 1.2.3.4 + tra_status: 'V' - tra_id: 3 tra_date: "2013-04-07 18:32:25" + tra_validated: "2013-04-07 18:32:25" usr_id_buyer: 9447 usr_id_seller: 9447 app_id: 51 fun_id: 1 tra_ip: 1.2.3.4 + tra_status: 'V' - tra_id: 4 tra_date: "2013-04-07 19:32:25" + tra_validated: "2013-04-07 19:32:25" usr_id_buyer: 9447 usr_id_seller: 9447 app_id: 51 fun_id: 1 tra_ip: 1.2.3.4 + tra_status: 'V' - tra_id: 5 tra_date: "2013-04-07 20:32:25" + tra_validated: "2013-04-07 20:32:25" usr_id_buyer: 9447 usr_id_seller: 9447 app_id: 51 fun_id: 1 tra_ip: 1.2.3.4 + tra_status: 'V' - tra_id: 6 tra_date: "2013-04-08 16:32:25" + tra_validated: "2013-04-08 16:32:25" usr_id_buyer: 9447 usr_id_seller: 9447 app_id: 51 fun_id: 1 tra_ip: 1.2.3.4 + tra_status: 'V' - tra_id: 7 tra_date: "2013-04-08 18:32:25" + tra_validated: "2013-04-08 18:32:25" usr_id_buyer: 9447 usr_id_seller: 9447 app_id: 51 fun_id: 1 tra_ip: 1.2.3.4 + tra_status: 'V' - tra_id: 8 tra_date: "2013-04-08 19:32:25" + tra_validated: "2013-04-08 19:32:25" usr_id_buyer: 9447 usr_id_seller: 9447 app_id: 51 fun_id: 1 tra_ip: 1.2.3.4 + tra_status: 'V' - tra_id: 9 tra_date: "2013-04-08 20:32:25" + tra_validated: "2013-04-08 20:32:25" usr_id_buyer: 9447 usr_id_seller: 9447 app_id: 51 fun_id: 1 tra_ip: 1.2.3.4 + tra_status: 'V' - tra_id: 10 tra_date: "2013-04-08 20:34:25" + tra_validated: "2013-04-08 20:34:25" usr_id_buyer: 9447 usr_id_seller: 9447 app_id: 51 fun_id: 1 tra_ip: 1.2.3.4 + tra_status: 'V' - tra_id: 11 tra_date: "2013-04-07 18:32:25" + tra_validated: "2013-04-07 18:32:25" usr_id_buyer: 9447 usr_id_seller: 9447 app_id: 51 fun_id: 1 tra_ip: 1.2.3.4 + tra_status: 'V' + + - + tra_id: 12 + tra_date: "2013-04-07 18:32:25" + usr_id_buyer: 9447 + usr_id_seller: 9447 + app_id: 51 + fun_id: 1 + tra_ip: 1.2.3.4 + tra_status: W + + - + tra_id: 13 + tra_date: "2013-04-07 18:32:25" + usr_id_buyer: 9447 + usr_id_seller: 9447 + app_id: 51 + fun_id: 1 + tra_ip: 1.2.3.4 + tra_status: A t_purchase_pur: @@ -208,6 +249,33 @@ t_purchase_pur: pur_price: 170 pur_removed: 0 + - + pur_id: 13 + tra_id: 12 + obj_id: 5 + pur_qte: 1 + pur_unit_price: 170 + pur_price: 170 + pur_removed: 0 + + - + pur_id: 14 + tra_id: 12 + obj_id: 5 + pur_qte: 1 + pur_unit_price: 170 + pur_price: 170 + pur_removed: 0 + + - + pur_id: 15 + tra_id: 13 + obj_id: 5 + pur_qte: 1 + pur_unit_price: 170 + pur_price: 170 + pur_removed: 0 + t_application_app: - app_id: 51