PageRenderTime 26ms CodeModel.GetById 25ms RepoModel.GetById 1ms app.codeStats 0ms

/WikiZam/extensions/Transactions/model/TMRecord.php

https://github.com/Seizam/seizamcore
PHP | 415 lines | 231 code | 76 blank | 108 comment | 50 complexity | 8660736ef839de0825fec043c27a5652 MD5 | raw file
  1. <?php
  2. if (!defined('MEDIAWIKI')) {
  3. die(-1);
  4. }
  5. /*
  6. * Transaction Manager Record Main Class
  7. */
  8. class TMRecord {
  9. private $id; #int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT 'Primary key'
  10. # The DB Record
  11. private $tmr = array(
  12. # Params related to Message
  13. 'tmr_type' => null, # varchar(8) NOT NULL COMMENT 'Type of message (Payment, Sale, Plan)',
  14. 'tmr_date_created' => null, # datetime NOT NULL COMMENT 'DateTime of creation',
  15. 'tmr_date_modified' => null, # datetime NOT NULL COMMENT 'DateTime of last modification',
  16. # Paramas related to User
  17. 'tmr_user_id' => null, # int(10) unsigned NOT NULL DEFAULT '0' COMMENT 'Foreign key to user.user_id',
  18. 'tmr_mail' => null, # tinyblob COMMENT 'User''s Mail',
  19. 'tmr_ip' => null, # tinyblob COMMENT 'User''s IP'
  20. # Params related to Record
  21. 'tmr_amount' => null, # decimal(9,2) NOT NULL COMMENT 'Record Amount',
  22. 'tmr_currency' => null, # varchar(3) NOT NULL DEFAULT 'EUR' COMMENT 'Record Currency',
  23. 'tmr_mac' => null, # varchar(40) COMMENT 'Record Verification Sum',
  24. 'tmr_desc' => null, # varchar(64) NOT NULL COMMENT 'Record Description',
  25. 'tmr_status' => null, # varchar(2) NOT NULL COMMENT 'Record status (OK, KO, PEnding, TEst)',
  26. 'tmr_tmb_id' => null #int(10) unsigned COMMENT 'Foreign key to tm_bill'
  27. );
  28. private function __construct($id, $tmr) {
  29. $this->id = $id;
  30. # Keeping only the field that we want
  31. $tmr = array_intersect_key($tmr, $this->tmr);
  32. $this->tmr = $tmr;
  33. }
  34. public function getId() {
  35. return $this->id;
  36. }
  37. public function getTMR() {
  38. return array_merge($this->tmr, array('tmr_id' => $this->id));
  39. }
  40. public function getStatus() {
  41. return $this->tmr['tmr_status'];
  42. }
  43. public function getUserId() {
  44. if (!isset($this->tmr['tmr_user_id'])) {
  45. throw new MWException('No user ID related to Record ' . $this->id . '.');
  46. }
  47. return $this->tmr['tmr_user_id'];
  48. }
  49. public function getTMBId() {
  50. return $this->tmr['tmr_tmb_id'];
  51. }
  52. /**
  53. * Get the TMRecord instance from a SQL row
  54. * @param ResultWrapper $row
  55. * @return self
  56. */
  57. private static function constructFromDatabaseRow($row) {
  58. if ($row === null) {
  59. throw new MWException('Cannot construct the TMRecord from the supplied row (null given)');
  60. }
  61. if (!isset($row->tmr_id)) {
  62. throw new MWException('Cannot construct the TMRecord from the supplied row (missing id field)');
  63. }
  64. return new self(intval($row->tmr_id), (array) $row);
  65. }
  66. /**
  67. * Restore from DB, using id
  68. * @param int $id
  69. * @return TMRecord
  70. */
  71. public static function getById($id) {
  72. if (($id === null) || !is_int($id) || ($id < 1)) {
  73. throw new MWException('Cannot fectch TMRecord matching the identifier (invalid identifier)');
  74. }
  75. # We need to read, but with money issues, best read from master.
  76. $dbr = wfGetDB(DB_MASTER);
  77. $result = $dbr->selectRow('tm_record', '*', array('tmr_id' => $id), __METHOD__);
  78. if ($result === false) {
  79. // not found, so return null
  80. return null;
  81. }
  82. return self::constructFromDatabaseRow($result);
  83. }
  84. public static function newFromBillId($id) {
  85. if (($id === null) || !is_int($id) || ($id < 1)) {
  86. throw new MWException('Cannot fectch TMRecord matching the identifier (invalid identifier)');
  87. }
  88. # We need to read, but with money issues, best read from master.
  89. $dbr = wfGetDB(DB_MASTER);
  90. $result = $dbr->selectRow('tm_record', '*', array('tmr_tmb_id' => $id), __METHOD__);
  91. if ($result === false) {
  92. // not found, so return null
  93. return null;
  94. }
  95. return self::constructFromDatabaseRow($result);
  96. }
  97. /**
  98. *
  99. * @param array $tmr
  100. * @return TMRecord the newly created TMRecord or null if an error occured
  101. */
  102. public static function create($tmr) {
  103. if (!is_array($tmr) ||
  104. !isset($tmr['tmr_type']) ||
  105. !isset($tmr['tmr_user_id']) ||
  106. !isset($tmr['tmr_amount']) ||
  107. !isset($tmr['tmr_currency']) ||
  108. !isset($tmr['tmr_desc']) ||
  109. !isset($tmr['tmr_status'])) {
  110. throw new MWException('Cannot create TMRecord (missing argument)');
  111. }
  112. if (!is_string($tmr['tmr_type']) ||
  113. !is_int($tmr['tmr_user_id']) ||
  114. !is_numeric($tmr['tmr_amount']) ||
  115. $tmr['tmr_currency'] !== 'EUR' ||
  116. !is_string($tmr['tmr_desc']) ||
  117. !in_array($tmr['tmr_status'], array('OK', 'KO', 'PE', 'TE'))) {
  118. throw new MWException('Cannot create TMRecord (invalid argument)');
  119. }
  120. if ($tmr['tmr_amount'] <= 0 && $tmr['tmr_status'] !== 'PE') {
  121. throw new MWException('Cannot create TMRecord (expense should be PEnding)' . print_r($tmr, true));
  122. }
  123. # Setting the date of update
  124. $tmr['tmr_date_created'] = $tmr['tmr_date_modified'] = wfTimestamp(TS_DB);
  125. # We need to write, therefore we need the master
  126. $dbw = wfGetDB(DB_MASTER);
  127. $dbw->begin();
  128. # PostgreSQL, null for MySQL
  129. $id = $dbw->nextSequenceValue('tm_record_tmr_id_seq');
  130. # Writing...
  131. $success = $dbw->insert('tm_record', $tmr);
  132. # Setting tmr_id from auto incremented id in DB
  133. $id = $dbw->insertId();
  134. $dbw->commit();
  135. if (!$success) {
  136. return null;
  137. }
  138. return new self($id, $tmr);
  139. }
  140. /**
  141. *
  142. * @param array $tmr
  143. * @return TMRecord the newly created TMRecord or null if an error occured
  144. */
  145. private static function constructExistingFromArray($tmr) {
  146. if (!is_array($tmr) ||
  147. isset($tmr['tmr_id'])) {
  148. throw new MWException('Cannot create TMRecord (missing argument)');
  149. }
  150. if (is_int($tmr['tmr_id'])) {
  151. throw new MWException('Cannot create TMRecord (invalid argument)');
  152. }
  153. # Setting the record Id
  154. $id = $tmr['tmr_id'];
  155. # We don't want these fields to be changed
  156. unset($tmr['tmr_type']);
  157. unset($tmr['tmr_user_id']);
  158. unset($tmr['tmr_amount']);
  159. unset($tmr['tmr_currency']);
  160. # Validating arguments
  161. if ((isset($tmr['tmr_mail']) && !is_string($tmr['tmr_mail'])) ||
  162. (isset($tmr['tmr_ip']) && !is_string($tmr['tmr_ip'])) ||
  163. (isset($tmr['tmr_mac']) && !is_string($tmr['tmr_mac'])) ||
  164. (isset($tmr['tmr_desc']) && !is_string($tmr['tmr_desc'])) ||
  165. (isset($tmr['tmr_status']) && !in_array($tmr['tmr_status'], array('OK', 'KO', 'PE', 'TE')))) {
  166. throw new MWException('Cannot create TMRecord (invalid argument)');
  167. }
  168. return new self($id, $tmr);
  169. }
  170. /**
  171. *
  172. * @return boolean
  173. */
  174. private function updateDB() {
  175. # Setting the date of update
  176. unset($this->tmr['tmr_date_created']);
  177. $this->tmr['tmr_date_modified'] = wfTimestamp(TS_DB);
  178. # We need to write, therefore we need the master
  179. $dbw = wfGetDB(DB_MASTER);
  180. $dbw->begin();
  181. # Writing...
  182. $return = $dbw->update('tm_record', $this->tmr, array('tmr_id' => $this->id));
  183. $dbw->commit();
  184. return $return;
  185. }
  186. /**
  187. * Get array of TMRecord /!\ONLY FOR EUROS
  188. * @TODO Make it work for every currency
  189. * @param int $user_id
  190. * @return TMRecord[] ("array()" if no record)
  191. */
  192. public static function getAllOwnedByUserId($user_id, $conditions = null) {
  193. if (($user_id === null) || !is_int($user_id) || ($user_id < 1)) {
  194. throw new MWException('Cannot fetch TMRecord owned by the specified user (invalid user identifier)');
  195. }
  196. if (isset($conditions))
  197. $conditions = array_merge(array('tmr_user_id' => $user_id, 'tmr_currency' => 'EUR'), $conditions);
  198. # We need to read, but with money issues, best read from master.
  199. $dbr = wfGetDB(DB_MASTER);
  200. $result = $dbr->select('tm_record', '*', $conditions, __METHOD__);
  201. $tmrecords = array();
  202. foreach ($result as $row) {
  203. $tmrecords[] = self::constructFromDatabaseRow($row);
  204. }
  205. $dbr->freeResult($result);
  206. return $tmrecords;
  207. }
  208. /**
  209. * Calculate account balance /!\ONLY FOR EUROS
  210. * @TODO Make it work for every currency
  211. * @return float
  212. */
  213. private static function getBalanceFromDB($user_id, $conditions = null) {
  214. $conditions = array_merge(array('tmr_user_id' => $user_id, 'tmr_currency' => 'EUR'), $conditions);
  215. # We need to read, but with money issues, best read from master.
  216. $dbr = wfGetDB(DB_MASTER);
  217. $dbr->begin();
  218. $result = $dbr->select('tm_record', 'SUM(tmr_amount) AS balance', $conditions);
  219. $dbr->commit();
  220. # Returning an Int
  221. return is_null($result->current()->balance) ? 0 : $result->current()->balance;
  222. }
  223. /**
  224. * Calculate true account balance (status=PE+OK) /!\ONLY FOR EUROS
  225. * @TODO Make it work for every currency
  226. * @return float
  227. */
  228. public static function getTrueBalanceFromDB($user_id) {
  229. return self::getBalanceFromDB($user_id, array("tmr_status='OK' OR (tmr_amount<0 AND tmr_status='PE')"));
  230. }
  231. /**
  232. * Do the necessary after record logic /!\ONLY FOR EUROS
  233. * @TODO Make it work for every currency
  234. * @return boolean
  235. */
  236. public function react() {
  237. // No reaction if not in Euro
  238. if ($this->tmr['tmr_currency'] !== 'EUR') {
  239. return false;
  240. }
  241. // No reaction if status ===KO
  242. if ($this->getStatus() === 'KO') {
  243. return false;
  244. }
  245. // If we have money incoming, perhaps some PEnding expenses can be Validated
  246. if ($this->tmr['tmr_amount'] > 0 && $this->getStatus() === 'OK') {
  247. return $this->reactToIncome();
  248. }
  249. // If we have money outgoing, it's PEnding and could be Validated
  250. if ($this->tmr['tmr_amount'] <= 0 && $this->getStatus() === 'PE') {
  251. return $this->reactToExpense();
  252. }
  253. }
  254. /**
  255. * Do the necessary after income logic /!\ONLY FOR EUROS
  256. * @TODO Make it work for every currency
  257. * @return boolean
  258. */
  259. private function reactToIncome() {
  260. $return = false;
  261. $pendingExpenses = self::getAllOwnedByUserId($this->getUserId(), array('tmr_status' => 'PE', 'tmr_amount <= 0'));
  262. $balanceOk = self::getBalanceFromDB($this->getUserId(), array('tmr_status' => 'OK'));
  263. foreach ($pendingExpenses as $expense) {
  264. $balanceOk += $expense->attemptPEtoOK($balanceOk);
  265. }
  266. # Setting the bill id (for a refund), bill id for expense is done later in react()
  267. if ($this->tmr['tmr_type'] == TM_REFUND_TYPE && $this->tmr['tmr_amount'] > 0) {
  268. $this->tmr['tmr_tmb_id'] = TMBill::newFromScratch()->getId();
  269. $this->updateDB();
  270. }
  271. return $return;
  272. }
  273. /**
  274. * Turn PE to OK is $balanceOk is positive /!\ONLY FOR EUROS
  275. * @TODO Make it work for every currency
  276. * @return int the amount of validated transaction
  277. */
  278. private function attemptPEtoOK($balanceOk) {
  279. $amount = 0;
  280. if ($this->tmr['tmr_currency'] === 'EUR' #Only Euro for the moment
  281. && $this->tmr['tmr_amount'] <= 0 #This is about (pending) expenses only
  282. && $this->tmr['tmr_status'] === 'PE' #This is about pending (expenses) only
  283. && ($this->tmr['tmr_amount'] + $balanceOk) >= 0) { #We can validate if the balance is positive or null
  284. $this->toOK();
  285. //amount to return
  286. $amount = $this->tmr['tmr_amount'];
  287. //Notify the other extensions
  288. $tmr = $this->tmr;
  289. $tmr['tmr_id'] = $this->id;
  290. wfRunHooks('TransactionUpdated', array($tmr));
  291. }
  292. return $amount;
  293. }
  294. private function toOK() {
  295. //new status
  296. $this->tmr['tmr_status'] = 'OK';
  297. //Do the billing
  298. if ( $this->tmr['tmr_amount'] != 0 ) {
  299. $this->tmr['tmr_tmb_id'] = TMBill::newFromScratch()->getId();
  300. }
  301. //Update de DB
  302. $this->updateDB();
  303. }
  304. /**
  305. * Do the necessary after expense logic /!\ONLY FOR EUROS
  306. * ie. Turn PE to OK if account balance is positive
  307. * @TODO Do it on create, this logic writes DB twice in a row...
  308. * @TODO Make it work for every currency
  309. * @return boolean
  310. */
  311. private function reactToExpense() {
  312. $return = false;
  313. if (self::getTrueBalanceFromDB($this->getUserId()) >= 0) {
  314. $this->toOK();
  315. $return = true;
  316. }
  317. return $return;
  318. }
  319. /**
  320. * Check if user is owner OR user is admin AND tmr_status = PE;
  321. * @param User $user
  322. * @return boolean
  323. */
  324. public function canCancel($user) {
  325. return ($this->getUserId() == $user->getId() || $user->isAllowed(TM_ADMIN_RIGHT)) && $this->getStatus() == 'PE';
  326. }
  327. /**
  328. * Turn tmr_status to KO (only if user can & tmr_status = PE)
  329. * @param User $user
  330. * @return boolean True or error string
  331. */
  332. public function cancel($user) {
  333. if (!$this->canCancel($user)) {
  334. return 'Error: Transaction cannot be cancelled';
  335. }
  336. $this->tmr['tmr_status'] = 'KO';
  337. return $this->updateDB();
  338. }
  339. }