/[drupal]/contributions/modules/market/market.module
ViewVC logotype

Contents of /contributions/modules/market/market.module

Parent Directory Parent Directory | Revision Log Revision Log | View Revision Graph Revision Graph


Revision 1.14 - (show annotations) (download) (as text)
Mon Mar 10 00:54:21 2008 UTC (20 months, 2 weeks ago) by greggles
Branch: MAIN
CVS Tags: HEAD
Changes since 1.13: +12 -14 lines
File MIME type: text/x-php
cleanup based on coder - still some issues related to code copy/pasted from cck/views which I'm going to ignore
1 <?php
2 //$Id: market.module,v 1.13 2008/03/10 00:48:24 greggles Exp $
3
4 // Trade states
5 define('MARKET_STATE_TRADE_OPEN', 2);
6 define('MARKET_STATE_TRADE_EXECUTED', 4);
7 define('MARKET_STATE_TRADE_INVALID', 5);
8 define('MARKET_STATE_TRADE_PARTIALLY_EXECUTED', 6);
9 define('MARKET_STATE_TRADE_CANCELLED', 3);
10
11 // Portfolio states
12 define('MARKET_STATE_PORTFOLIO_OPEN', 8);
13 define('MARKET_STATE_PORTFOLIO_CLOSED', 9);
14
15 // Item states
16 define('MARKET_STATE_ITEM_OPEN', 11);
17 define('MARKET_STATE_ITEM_PENDING_DECISION', 14);
18 define('MARKET_STATE_ITEM_CLOSED_SUCCESS', 12);
19 define('MARKET_STATE_ITEM_CLOSED_FAIL', 13);
20
21 // Other random defines
22 define('MARKET_ROBOT_UID', 2);
23
24 /**
25 * Implementation of hook_help().
26 */
27 function market_help($section) {
28 switch ($section) {
29 case 'admin/settings/market':
30 $output = t('Configure userpoints moderation and branding translation');
31 break;
32 case 'admin/help#market':
33 $output = t('Users earn !points as they post nodes, comments, and vote on nodes');
34 }
35 return $output;
36 }
37
38 /**
39 * Implementation of hook_menu().
40 */
41 function market_menu($may_cache) {
42
43 $items = array();
44
45 if ($may_cache) {
46 $items[] = array(
47 'path' => 'admin/settings/market',
48 'callback' => 'drupal_get_form',
49 'callback arguments' => array('market_admin_settings'),
50 'title' => t('Market settings'),
51 'description' => t('Configure market settings'),
52 'access' => user_access('administer markets'),
53 'type' => MENU_NORMAL_ITEM,
54 );
55 }
56
57 return $items;
58 }
59
60 /**
61 * Implementation of hook_perm().
62 */
63 function market_perm() {
64 return array('administer markets');
65 }
66
67 /**
68 * menu callback for settings form.
69 */
70 function market_admin_settings() {
71 $form = array();
72 $group = 'workflow';
73 $form[$group] = array(
74 '#type' => 'fieldset',
75 '#title' => t('Workflow States'),
76 '#collapsible' => TRUE,
77 '#collapsed' => TRUE,
78 '#weight' => -1,
79 '#description' => t('Configure the state IDs for the states of the workflows you have defined. <br> See the INSTALL.txt for more details'),
80 );
81
82 // Trade States
83 $form[$group]['trade'] = array(
84 '#type' => 'fieldset',
85 '#title' => t('Workflow States for Trades'),
86 '#collapsible' => FALSE,
87 '#collapsed' => FALSE,
88 '#weight' => -1,
89 );
90
91 $form[$group]['trade']['market_state_trade_open'] = array(
92 '#type' => 'textfield',
93 '#title' => t('Open State ID'),
94 '#default_value' => variable_get('market_state_trade_open', MARKET_STATE_TRADE_OPEN),
95 '#size' => 20,
96 '#maxlength' => '20',
97 '#required' => TRUE,
98 );
99
100 $form[$group]['trade']['market_state_trade_executed'] = array(
101 '#type' => 'textfield',
102 '#title' => t('Executed State ID'),
103 '#default_value' => variable_get('market_state_trade_executed', MARKET_STATE_TRADE_EXECUTED),
104 '#size' => 20,
105 '#maxlength' => '20',
106 '#required' => TRUE,
107 );
108
109 $form[$group]['trade']['market_state_trade_invalid'] = array(
110 '#type' => 'textfield',
111 '#title' => t('Invalid State ID'),
112 '#default_value' => variable_get('market_state_trade_invalid', MARKET_STATE_TRADE_INVALID),
113 '#size' => 20,
114 '#maxlength' => '20',
115 '#required' => TRUE,
116 );
117
118 $form[$group]['trade']['market_state_trade_partially_executed'] = array(
119 '#type' => 'textfield',
120 '#title' => t('Partially Executed State ID'),
121 '#default_value' => variable_get('market_state_trade_partially_executed', MARKET_STATE_TRADE_PARTIALLY_EXECUTED),
122 '#size' => 20,
123 '#maxlength' => '20',
124 '#required' => TRUE,
125 );
126
127 $form[$group]['trade']['market_state_trade_cancelled'] = array(
128 '#type' => 'textfield',
129 '#title' => t('Cancelled State ID'),
130 '#default_value' => variable_get('market_state_trade_cancelled', MARKET_STATE_TRADE_CANCELLED),
131 '#size' => 20,
132 '#maxlength' => '20',
133 '#required' => TRUE,
134 );
135
136 // Portfolio states
137 $form[$group]['portfolio'] = array(
138 '#type' => 'fieldset',
139 '#title' => t('Workflow States for Portfolios'),
140 '#collapsible' => FALSE,
141 '#collapsed' => FALSE,
142 '#weight' => -1,
143 );
144
145 $form[$group]['portfolio']['market_state_portfolio_open'] = array(
146 '#type' => 'textfield',
147 '#title' => t('Portfolio Open State ID'),
148 '#default_value' => variable_get('market_state_portfolio_open', MARKET_STATE_PORTFOLIO_OPEN),
149 '#size' => 20,
150 '#maxlength' => '20',
151 '#required' => TRUE,
152 );
153 $form[$group]['portfolio']['market_state_portfolio_closed'] = array(
154 '#type' => 'textfield',
155 '#title' => t('Portfolio Open State ID'),
156 '#default_value' => variable_get('market_state_portfolio_closed', MARKET_STATE_PORTFOLIO_CLOSED),
157 '#size' => 20,
158 '#maxlength' => '20',
159 '#required' => TRUE,
160 );
161
162
163 // Item (contract, stock, option, etc.) states
164 $form[$group]['item'] = array(
165 '#type' => 'fieldset',
166 '#title' => t('Workflow States for Items'),
167 '#collapsible' => FALSE,
168 '#collapsed' => FALSE,
169 '#weight' => -1,
170 );
171 $form[$group]['item']['market_state_item_open'] = array(
172 '#type' => 'textfield',
173 '#title' => t('Item Open State ID'),
174 '#default_value' => variable_get('market_state_item_open', MARKET_STATE_ITEM_OPEN),
175 '#size' => 20,
176 '#maxlength' => '20',
177 '#required' => TRUE,
178 );
179 $form[$group]['item']['market_state_item_pending_decision'] = array(
180 '#type' => 'textfield',
181 '#title' => t('Item Pending Decision State ID'),
182 '#default_value' => variable_get('market_state_item_pending_decision', MARKET_STATE_ITEM_PENDING_DECISION),
183 '#size' => 20,
184 '#maxlength' => '20',
185 '#required' => TRUE,
186 );
187 $form[$group]['item']['market_state_item_closed_success'] = array(
188 '#type' => 'textfield',
189 '#title' => t('Item Closed Success State ID'),
190 '#default_value' => variable_get('market_state_item_closed_success', MARKET_STATE_ITEM_CLOSED_SUCCESS),
191 '#size' => 20,
192 '#maxlength' => '20',
193 '#required' => TRUE,
194 );
195 $form[$group]['item']['market_state_item_closed_fail'] = array(
196 '#type' => 'textfield',
197 '#title' => t('Item Closed Fail State ID'),
198 '#default_value' => variable_get('market_state_item_closed_fail', MARKET_STATE_ITEM_CLOSED_FAIL),
199 '#size' => 20,
200 '#maxlength' => '20',
201 '#required' => TRUE,
202 );
203
204 // General Settings
205 $group = 'general';
206 $form[$group] = array(
207 '#type' => 'fieldset',
208 '#title' => t('General Settings'),
209 '#collapsible' => TRUE,
210 '#collapsed' => TRUE,
211 '#weight' => -1,
212 );
213
214 $form[$group]['market_robot_uid'] = array(
215 '#type' => 'textfield',
216 '#title' => t('User ID for the Market State Robot'),
217 '#default_value' => variable_get('market_robot_uid', MARKET_ROBOT_UID),
218 '#weight' => -1,
219 '#size' => 20,
220 '#maxlength' => '20',
221 '#required' => TRUE,
222 '#description' => t('The User ID for the user who will perform state changes. This user should have an advanced role (i.e. administrator) which is allowed to perform the state changes in your trade workflows. See INSTALL.txt for more details'),
223 );
224
225 return system_settings_form($form);
226 }
227
228 /**
229 * Implementation of hook_block().
230 */
231 function market_block($op = 'list', $delta = 0) {
232 if ($op == 'list') {
233 $blocks[0]['info'] = t('Market: Item Price (bid/ask)');
234 $blocks[1]['info'] = t('Market: Items in This Portfolio');
235 return $blocks;
236 }
237 else if ($op == 'view') {
238 switch ($delta) {
239 case 0:
240 if (arg(0) == 'node' && is_numeric(arg(1)) && arg(2) != 'edit') {
241 $node = node_load(arg(1));
242 if ($node->type == 'item') {
243 $block['subject'] = t('Price (may be delayed)');
244 $block['content'] = _market_block_bid_ask(arg(1));
245 }
246 }
247 break;
248 case 1:
249 if (arg(0) == 'node' && is_numeric(arg(1)) && arg(2) != 'edit') {
250 $node = node_load(arg(1));
251 if ($node->type == 'portfolio') {
252 $block['subject'] = t('Items in this portfolio');
253 $block['content'] = _market_block_portfolio_contents(arg(1));
254 }
255 }
256 break;
257 }
258 return $block;
259 }
260 }
261
262
263 function _market_block_bid_ask($item_nid) {
264 // Max bid price (buy orders)
265 $max_bid = db_result(db_query("SELECT max(ctt.field_price_value) FROM {content_type_trade} ctt INNER JOIN {workflow_node} wn ON ctt.nid = wn.nid WHERE field_item_nid = %d AND field_action_value = 'buy' AND wn.sid in (%d, %d)",
266 $item_nid, variable_get('market_state_trade_open', MARKET_STATE_TRADE_OPEN), variable_get('market_state_trade_partially_executed', MARKET_STATE_TRADE_PARTIALLY_EXECUTED)));
267 if ($max_bid > 0) {
268 $items[] = t('Highest buy order: %price.', array('%price' => $max_bid));
269 }
270 else {
271 $items[] = t('There are no open buy orders.');
272 }
273 // Minimum ask (sell orders)
274 $min_ask = db_result(db_query("SELECT min(ctt.field_price_value) FROM {content_type_trade} ctt INNER JOIN {workflow_node} wn ON ctt.nid = wn.nid WHERE field_item_nid = %d AND field_action_value = 'sell' AND wn.sid in (%d, %d)",
275 $item_nid, variable_get('market_state_trade_open', MARKET_STATE_TRADE_OPEN), variable_get('market_state_trade_partially_executed', MARKET_STATE_TRADE_PARTIALLY_EXECUTED)));
276 if ($min_ask > 0) {
277 $items[] = t('Lowest sell order: %price.', array('%price' => $min_ask));
278 }
279 else {
280 $items[] = t('There are no open sell orders.');
281 }
282
283 // Last order
284 $last_nid = db_result(db_query("SELECT max(ctt.nid) FROM {content_type_trade} ctt INNER JOIN {workflow_node} wn ON ctt.nid = wn.nid WHERE field_item_nid = %d AND wn.sid in (%d, %d)",
285 $item_nid, variable_get('market_state_trade_executed', MARKET_STATE_TRADE_EXECUTED), variable_get('market_state_trade_partially_executed', MARKET_STATE_TRADE_PARTIALLY_EXECUTED)));
286 if ($last_nid) {
287 $last_price = db_result(db_query("SELECT ctt.field_price_value FROM {content_type_trade} ctt WHERE ctt.nid = %d",
288 $last_nid));
289 $items[] = t('Last price: %price.', array('%price' => $last_price));
290 }
291 else {
292 $items[] = t('No transactions yet.');
293 }
294
295 return theme('item_list', $items);
296 }
297
298 function _market_block_portfolio_contents($portfolio_nid) {
299 $results = db_query("SELECT n.nid, n.title, cti.field_price_estimate_value FROM node n INNER JOIN content_type_item cti ON n.vid = cti.vid WHERE field_portfolio_nid = %d", $portfolio_nid);
300 while ($result = db_fetch_array($results)) {
301 $output = l(t('@title: last price @price', array('@title' => $result['title'], '@price' => $result['field_price_estimate_value'])), 'node/'. $result['nid']);
302 if (user_access('create trade content')) {
303 $output .= ' '. l('buy', 'node/add/trade', NULL, 'edit[field_item][nids]='. $result['nid'] .'&edit[field_action][key]=buy');
304 $output .= ' '. l('sell', 'node/add/trade', NULL, 'edit[field_item][nids]='. $result['nid'] .'&edit[field_action][key]=sell');
305 }
306 $items[] = $output;
307 }
308 // TODO get the bid/ask for these - maybe slightly intensive so skipped for now.
309 return theme('item_list', $items);
310 }
311
312 /*
313 * Implementation of hook_cron
314 */
315 function market_cron() {
316 // TODO some stuff
317 // TODO close trades on time based items
318 // TODO If it hasn't been done yet, crunch the numbers on yesterday's high/low/average/volume for all items
319
320 }
321
322 /**
323 * Implementation of hook_exit
324 */
325 function market_exit() {
326 // Save the state change for nodes if someone else set a nid for us
327 global $_market_state;
328 if (is_array($_market_state) && count($_market_state)) {
329 foreach ($_market_state as $key => $state) {
330 _market_state_change($state['nid'], $state['state'], $state['comment']);
331 }
332 }
333
334 // Save the remaining change for nodes if someone else set a nid for us
335 // We do this at the end instead of along the way in case a single trade gets updated multiple times
336 global $_market_remaining;
337 if (is_array($_market_remaining) && count($_market_remaining)) {
338 foreach ($_market_remaining as $nid => $remaining) {
339 _market_remaining_change($nid, $remaining);
340 }
341 }
342 }
343
344
345 function _market_state_change($nid, $state, $comment) {
346 // Sneaky robot stuff to make sure we can change the state to executed
347 global $user;
348 $orig_user = $user;
349 $user = user_load(array('uid' => variable_get('market_robot_uid', MARKET_ROBOT_UID)));
350 // Actually change the state - copied from workflow module
351 $node = node_load($nid, NULL, TRUE);
352 $node->workflow = $state;
353 $node->workflow_comment = $comment;
354 $node->revision = 1;
355 node_save($node);
356
357 // Set back the sneaky robot business
358 $user = $orig_user;
359 }
360
361 function _market_remaining_change($nid, $remaining) {
362 $node = node_load($nid, NULL, TRUE);
363 $node->field_remaining[0]['value'] = (string)$remaining;
364 $node->revision = 1;
365 node_save($node);
366 }
367
368 /*
369 * Implementation of hook_workflow
370 */
371 function market_workflow($op, $old_sid, $sid, $node) {
372 if ($op == 'transition post' && $node->type == 'item') {
373 if ($sid == variable_get('market_state_item_closed_success', MARKET_STATE_ITEM_CLOSED_SUCCESS)) {
374 // This is a transition post situation, the node is an "item" and it was closed to success
375 // So, we give the owners of this item 100 points for each contract they own
376 $users = db_query('SELECT n.uid, ctic.field_item_count_value, ctic.field_owned_items_nid FROM node n INNER JOIN content_type_item_count ctic ON n.vid = ctic.vid WHERE field_owned_items_nid = %d', $node->nid);
377 while ($user = db_fetch_array($users)) {
378 $params = array(
379 'uid' => $user['uid'],
380 'points' => $user['field_item_count_value'] * 100, // TODO make the 100 variable per site (or better - per portfolio)
381 'description' => t('Success for contract number @contract_nid. Congrats!', array('@contract_nid' => $user['field_item_count_value'])),
382 'event' => 'trade',
383 'reference' => $trade_nid,
384 );
385 $status = userpoints_userpointsapi($params);
386
387 // MAYBEDO create a workflow where each item_count is moved from a workflow state of OPEN to CREDITED so we can do this in cron runs as well
388 }
389 }
390 }
391
392 }
393
394 /*
395 * Implementation of hook_nodeapi
396 */
397 function market_nodeapi(&$node, $op, $a3 = NULL, $a4 = NULL) {
398 // TODO for items:
399 // TODO Validate the order prior to submit to make sure they can currently enter it
400 // buy orders need to have enough money
401 // sell orders need to have the shares
402 // TODO state the upside/downside of the position prior to submission
403
404 // TODO for portfolios:
405 // TODO some ability to open/close trading on a portfolio
406 // TODO if it's a portfolio buy/sell set the price to 100 and warn the user
407
408 // TODO for items:
409 // TODO On load pull up last price
410
411 switch ($op) {
412 case 'validate':
413 if ($node->type == 'trade' && !isset($node->nid)) {
414 // Set the remaining to the quantity
415
416
417 // Buying - have enough points?
418 if ($node->field_action[0]['value'] == 'buy') {
419 if (($node->field_price[0]['value'] * $node->field_quantity[0]['value']) > userpoints_get_current_points($node->uid)) {
420 form_set_error('field_quantity', t('Insufficient points to purchase this many portfolios. Consider getting more points or reducing the quantity/price.'));
421 }
422 }
423
424 $item = node_load($node->field_item[0]['nid']);
425 if ($item->type == 'portfolio') {
426 if ($node->field_price[0]['value'] != 100) {
427 form_set_error('field_price', t('Bids on whole portfolios must have a price of 100.'));
428 }
429 // Selling - have enough items?
430 if ($node->field_action[0]['value'] == 'sell') {
431 // Get the list of items in this portfolio
432 // Do they own all the items in this portfolio?
433 // If so, do they own enough of them?
434
435 $fewest = market_portfolio_minimum_ownership($node->uid, $node->field_item[0]['nid']);
436 // return array('items' => $fewest_items, 'item_nid' => $fewest_nid);
437
438 if ($fewest === 0) {
439 form_set_error('field_quantity', t('You are trying to sell a portfolio where you do not own any of the items in the portfolio.'));
440 }
441 else if ($fewest['items'] < $node->field_quantity[0]['value']) {
442 form_set_error('field_quantity', t('You are trying to sell more items than you own. You can only sell %fewest or fewer items.', array('%fewest' => $fewest['items'])));
443 }
444 }
445 }
446 if ($item->type == 'item') {
447 // Make sure that the item is open for trading
448 if ($item->_workflow != variable_get('market_state_item_open', MARKET_STATE_ITEM_OPEN)) {
449 form_set_error('field_item_nids', t('You are trying to transact in an item which is not available for sale'));
450 }
451 }
452 }
453 break;
454 case 'insert':
455 if ($node->type == 'trade') {
456 $item = node_load($node->field_item[0]['nid']);
457 // TODO: instead of loading the node, just do one quick query on the node table to get title and type which is all we need
458 if ($item->type == 'portfolio' || $item->type == 'item') {
459 $userpoints_display_message = variable_get('userpoints_display_message', '1');
460 variable_set('userpoints_display_message', '0');
461
462 // This actually does all the work of changing points and recording item counts
463 market_transaction($node->field_action[0]['value'], $node->uid, $node->field_price[0]['value'], $node->field_quantity[0]['value'], $node->nid, $item->nid, $item->title, $item->type);
464
465 variable_set('userpoints_display_message', $userpoints_display_message);
466 }
467 else {
468 // Else: what do we do if it's a trade that's not a portfolio or item
469 watchdog('market', t('Something is pretty wrong. In <a href="!uri">this trade</a> a user traded an item which can not be traded', array('!uri' => url('node/'. $node->nid))));
470 }
471 }
472 break;
473 default:
474 break;
475 }
476 }
477
478 /**
479 * Implementation of hook_form_alter
480 */
481 function market_form_alter($form_id, &$form) {
482 // TODO Remove the remaining quantity from the input form
483 if ($form_id == 'trade_node_form') {
484 $form['field_remaining'][0]['value']['#type'] = 'hidden';
485 }
486 }
487
488 /**
489 * Perform the actual transaction inside of a database transaction
490 * WORKHERE
491 */
492 function market_transaction($action, $uid, $price, $quantity, $trade_nid, $item_nid, $title, $type) {
493 global $_market_state;
494 global $_market_remaining;
495 $_market_remaining[$trade_nid] = $quantity; // Default this guy
496
497 $txn = new pressflow_transaction();
498
499 // Determine if the quantity should be positive or negative - buy vs. sell
500 if ($action == 'buy') {
501 $match_action = 'sell';
502 $match_sort = ' ASC ';
503 $match_compare_order = ' <= ';
504 }
505 else {
506 $match_action = 'buy';
507 $match_sort = ' DESC ';
508 $match_compare_order = ' >= ';
509 }
510
511 // We try to execute the portfolios immediately
512 if ($type == 'portfolio') {
513 if ($action != 'buy') { // TODO we are now doing this buy thing 3 times - stop that!
514 $quantity = -$quantity;
515 }
516
517 // Try to transact the points
518 $status = _market_point_transaction($action, $uid, $price, $quantity, $trade_nid, $title);
519
520 // Only try to transact the trade if the points transferred properly
521 if ($status) {
522
523 $status = _market_portfolio_transaction($item_nid, $quantity, $uid);
524 if ($status) {
525 $_market_state[] = array('nid' => $trade_nid, 'state' => variable_get('market_state_trade_executed', MARKET_STATE_TRADE_EXECUTED), 'comment' => t('Trade executed'));
526 $_market_remaining[$trade_nid] = '0';
527 }
528 else {
529 $_market_state[] = array('nid' => $trade_nid, 'state' => variable_get('market_state_trade_invalid', MARKET_STATE_TRADE_INVALID), 'comment' => t('Trade invalid - accounting problem.'));
530 $_market_remaining[$trade_nid] = abs($quantity);
531 }
532 }
533 else {
534 $_market_state[] = array('nid' => $trade_nid, 'state' => variable_get('market_state_trade_invalid', MARKET_STATE_TRADE_INVALID), 'comment' => t('Trade invalid - points problem.'));
535 $_market_remaining[$trade_nid] = abs($quantity);
536 }
537 }
538 else {
539 // Individual items outside a portfolio are checked against the book.
540 // If they match the book - execute now. If not, store for later
541 // MAYBEDO: some locking mechanism?
542
543 $match = db_fetch_array(db_query_range("SELECT n.nid, n.uid, ctt.field_price_value, COALESCE(ctt.field_remaining_value, ctt.field_quantity_value) quantity, wn.sid state_id FROM {content_type_trade} ctt INNER JOIN {node} n ON ctt.vid = n.vid INNER JOIN {workflow_node} wn ON ctt.nid = wn.nid WHERE field_item_nid = %d AND field_action_value = '%s' AND field_price_value $match_compare_order %d AND wn.sid in (%d, %d) ORDER BY field_price_value %s, nid ASC",
544 $item_nid, $match_action, $price, variable_get('market_state_trade_open', MARKET_STATE_TRADE_OPEN), variable_get('market_state_trade_partially_executed', MARKET_STATE_TRADE_PARTIALLY_EXECUTED), $match_sort, 0, 1));
545 // Coalesce is probably unnecessary, but just in case...
546 drupal_set_message(print_r($match, TRUE));
547 if ($match['nid']) {
548
549 // Set the quantity to the right number for this situation
550 $exact_match = FALSE;
551 $more_in_new = FALSE;
552 $more_in_existing = FALSE;
553
554 if (abs($quantity) == abs($match['quantity'])) {
555 $exact_match = TRUE;
556 $trade_quantity = $quantity;
557 }
558 else if (abs($quantity) > abs($match['quantity'])) {
559 $more_in_new = TRUE;
560 $trade_quantity = $match['quantity'];
561 }
562 else {
563 $more_in_existing = TRUE;
564 $trade_quantity = $quantity;
565 }
566 // Now we flip the bits for buy/sell
567 if ($action == 'buy') {
568 $trade_quantity = abs($trade_quantity);
569 $match_quantity = -abs($trade_quantity);
570 }
571 else {
572 $trade_quantity = -abs($trade_quantity);
573 $match_quantity = abs($trade_quantity);
574 }
575
576 // Great, we got a match, begin the transaction
577 // Points for the match first (this tests that their order is still valid as well)
578 $status = _market_point_transaction($match_action, $match['uid'], $match['field_price_value'], -$trade_quantity, $trade_nid, $title);
579 // And now the initiator of the transaction
580 if ($status) {
581 // Then the initator
582 $status = _market_point_transaction($action, $uid, $match['field_price_value'], $trade_quantity, $trade_nid, $title);
583 }
584 else {
585 // That transaction went bad, mark the trade for workflow changes
586 _market_state_change($match['nid'], variable_get('market_state_trade_invalid', MARKET_STATE_TRADE_INVALID), t('Trade found to be invalid during a transaction due to points problems.'));
587 // We do this twice - if it's already in the state, the changer will bail because the workflow doesn't allow that transition?
588 // If the transaction got rolled back then this exit change will still happen. Kinda weird, may need to rethink this.
589 $_market_state[] = array('nid' => $match['nid'], 'state' => variable_get('market_state_trade_invalid', MARKET_STATE_TRADE_INVALID),
590 'comment' => t('Trade found to be invalid during a transaction due to points problems.'));
591 }
592
593 // And now the items
594 if (!$status) {
595 // That transaction went bad, mark the trade for workflow changes
596 _market_state_change($trade_nid, variable_get('market_state_trade_invalid', MARKET_STATE_TRADE_INVALID), t('Trade found to be invalid during a transaction due to points problems.'));
597 // We do this twice - if it's already in the state, the changer will bail because the workflow doesn't allow that transition?
598 // If the transaction got rolled back then this exit change will still happen. Kinda weird, may need to rethink this.
599 $_market_state[] = array('nid' => $match['nid'], 'state' => variable_get('market_state_trade_invalid', MARKET_STATE_TRADE_INVALID),
600 'comment' => t('Trade found to be invalid during a transaction due to points problems.'));
601 }
602 else {
603 $status = _market_item_transaction($item_nid, $match_quantity, $match['uid']);
604 }
605
606 // If that failed, mark the item as invalid and bail
607 if (!$status) {
608 // asdf
609 }
610 else { // First accounting worked, mark it, and try the second one
611 if ($exact_match || $more_in_new) {
612 // That worked and the amount for the existing was fully filled
613 _market_state_change($match['nid'], variable_get('market_state_trade_executed', MARKET_STATE_TRADE_EXECUTED), t('Trade completed.'));
614 $_market_remaining[$match['nid']] = '0';
615 }
616 else {
617 // Partial fill, set state and remaining shares
618 _market_state_change($match['nid'], variable_get('market_state_trade_partially_executed', MARKET_STATE_TRADE_PARTIALLY_EXECUTED), t('Trade completed.'));
619 $_market_remaining[$match['nid']] = $match['quantity'] - abs($trade_quantity);
620 }
621 // Try the new trade
622 $status = _market_item_transaction($item_nid, $trade_quantity, $uid);
623 if (!$status) {
624 // That transaction went bad, mark the trade for workflow changes
625 // TODO make sure we break out of the trade - if the original is broken then we don't want to do anything else
626 $_market_state[] = array('nid' => $trade_nid, 'state' => variable_get('market_state_trade_invalid', MARKET_STATE_TRADE_INVALID), 'comment' => t('Trade found to be invalid during a transaction.'));
627 // TODO break; or something like that?
628 }
629 else { // Succeded, account, recurse if necessary
630 // Update the price on the item
631 drupal_set_message('updating price to '. $match['field_price_value']);
632 $item_node = node_load($item_nid);
633 $item_node->field_price_estimate[0]['value'] = $match['field_price_value'];
634 node_save($item_node);
635
636 if ($exact_match || $more_in_existing) {
637 $_market_state[] = array('nid' => $trade_nid, 'state' => variable_get('market_state_trade_executed', MARKET_STATE_TRADE_EXECUTED), 'comment' => t('Trade completed.'));
638 $_market_remaining[$trade_nid] = 0;
639 }
640 else {
641 $_market_state[] = array('nid' => $trade_nid, 'state' => variable_get('market_state_trade_partially_executed', MARKET_STATE_TRADE_PARTIALLY_EXECUTED), 'comment' => t('Trade partially completed.'));
642 $_market_remaining[$trade_nid] = abs($trade_quantity) - abs($quantity);
643 // TODO If there are more item on this order unsatisfied then 1)mark this as partial executed 2) recurse on this function again, baby
644 market_transaction($action, $uid, $price, abs($_market_remaining[$trade_nid]), $trade_nid, $item_nid, $title, $type); // Subtle but important difference in quantity - use the updated one
645 }
646 }
647 }
648 // TODO be sure we set the $_market_state for success
649 // TODO be sure we set the $_market_state for ANY failures
650 }
651 }
652
653 // Rollback or end transaction
654 if ($status) {
655 // Commit (pressflow transaction things aren't actually committed, you just don't roll them back)
656 }
657 else {
658 unset($_market_remaining); // Stuff went bad so let's not change the remaining share numbers...
659 $txn->rollback();
660 drupal_set_message(t('Trade was not executed (maybe never will be).'));
661 }
662 return $status;
663 }
664
665 /**
666 * Helper function to make userpoints transactions
667 * We have to do this here instead of hook_nodeapi
668 */
669 function _market_point_transaction($action, $uid, $price, $quantity, $trade_nid, $title) {
670 $status = TRUE;
671
672 // Try to debit/credit the account
673 // Build the userpoints parameters
674 $params = array(
675 'uid' => $uid,
676 'points' => $price * $quantity * -1, // -1 because buying a quantity should decrement the points
677 'description' => t('Trade to @action @quantity items of @title at @price in trade id @nid',
678 array('@action' => $action, '@quantity' => $quantity, '@title' => $title, '@nid' => $trade_nid, '@price' => $price)),
679 'event' => 'trade',
680 'reference' => $trade_nid,
681 );
682 $status = userpoints_userpointsapi($params);
683 if (!$status['status']) {
684 watchdog('market', t('Problem with points: %reason', array('%reason' => $status['reason'])));
685 }
686 return $status['status'];
687 }
688
689 /**
690 * Quite a simple function, really. Maintained for API consistency between portfolios and items and in case we need to do more here later.
691 */
692 function _market_item_transaction($item_nid, $quantity, $uid) {
693 // Do some stuff?
694 $status = TRUE; // Default to TRUE, if we fail along the way it will get flipped
695 // Finds all of the item for a portfolio
696 $status = _market_account_items($item_nid, $quantity, $uid);
697 return $status;
698 }
699
700 /**
701 * Market portfolio transaction -
702 * $portfolio_nid is the nid of the portfolio
703 * $quanitity - positive to buy, negative to sell
704 * $uid is the username of the person making the trade
705 *
706 * returns TRUE if the transaction was processed,
707 * FALSE if something prevented it
708 */
709 function _market_portfolio_transaction($portfolio_nid, $quantity, $uid) {
710 $status = TRUE; // Default to TRUE, if we fail along the way it will get flipped
711
712 // Finds all of the item for a portfolio
713 $results = db_query("SELECT nid FROM {content_type_item} WHERE field_portfolio_nid = %d", $portfolio_nid);
714 while (($result = db_fetch_array($results)) && $status) {
715 $status = _market_account_items($result['nid'], $quantity, $uid);
716 }
717 return $status;
718 }
719
720 function _market_account_items($item_nid, $quantity, $uid) {
721 $status = TRUE;
722 $accounting_nid = FALSE;
723 $accounting_nid = db_result(db_query("SELECT n.nid FROM {content_type_item_count} ct INNER JOIN {node} n ON ct.vid = n.vid WHERE field_owned_items_nid = %d AND n.uid = %d", $item_nid, $uid)); //TODO confirm that this works with revisions and everything - maybe we need more join conditions?
724
725 if ($accounting_nid) {
726 // Update it
727 $accounting_node = node_load($accounting_nid, NULL, TRUE);
728 $accounting_node->field_item_count[0][value] += $quantity;
729 $accounting_node->revision = 1;
730
731 if ($accounting_node->field_item_count[0][value] >= 0) {
732 node_save($accounting_node);
733 }
734 else {
735 $status = FALSE;
736 $message = t('Transaction failed, it would sell more items than are currently owned.');
737 drupal_set_message($message, 'error');
738 watchdog('market', $message);
739 }
740 }
741 else if ($quantity > 0) {
742 // They don't currently have a item_count - as long as they are buying, create a new node for this item
743 $new_node->type = 'item_count';
744 $values = array();
745 $trading_user = user_load(array('uid' => $uid));
746 $values['name'] = $trading_user->name;
747 $values['type'] = 'item_count';
748 $values['title'] = 'auto generated title'; // TODO better titles
749
750 $values['status'] = 1;
751 $values['promote'] = 0;
752 $values['sticky'] = 0;
753 $values['revision'] = 1;
754 $values['comment'] = 2;
755 $values['field_credit'][0]['value'] = strip_tags($row->byline);
756
757 $values['field_owned_items'] = array('nids' => $item_nid); // Caution: if we change the style of the item field to a different widget this might need to be changed as well
758 $values['field_item_count'][0]['value'] = $quantity;
759 $return = drupal_execute('item_count_node_form', $values, $new_node);
760 $errors = form_get_errors();
761 if (count($errors)) {
762 watchdog('market', print_r($errors, TRUE), 'error');
763 $status = FALSE;
764 watchdog('market', 'Something went wrong in creating a new item count for this trade');
765 }
766 }
767 // This should really never happen, but in case it does, at least we block and set the stage for a roll back.
768 else {
769 $status = FALSE;
770 drupal_set_message(t('You tried to sell a something that you do not entirely own: item %item_nid, quantity %quantity, user id %uid, ', array('%item_nid' => $item_nid, '%quantity' => $quantity, '%uid' => $uid)), 'error');
771 }
772 return $status;
773 }
774
775
776 /**
777 * Checks to see which item a user has the fewest of out of a portfolio
778 * Returns an array with the number of items and nid of the item
779 * If no items are owned, returns 0;
780 */
781 function market_portfolio_minimum_ownership($uid, $portfolio_nid) {
782 // MAYBEDO - 2 queries instead of N queries, 1 for items, 1 for owns, then merge
783 $itemss = db_query("SELECT nid FROM {content_type_item} WHERE field_portfolio_nid = %d", $portfolio_nid);
784 $owns = array();
785 while ($item = db_fetch_array($itemss)) {
786 $count = db_result(db_query("SELECT field_item_count_value FROM {content_type_item_count} ctcc INNER JOIN {node} n ON ctcc.vid = n.vid WHERE n.uid = %d AND field_owned_items_nid = %d", $uid, $item['nid']));
787 $owns[$item['nid']] = $count ? $count : 0;
788 if (!isset($fewest_items) || $fewest_items > $owns[$item['nid']]) {
789 $fewest_items = $owns[$item['nid']];
790 $fewest_nid = $item['nid'];
791 }
792 }
793 if (isset($fewest_items)) {
794 return array('items' => $fewest_items, 'item_nid' => $fewest_nid);
795 }
796 else {
797 return 0;
798 }
799
800 }
801
802 /**
803 * Some default views
804 */
805
806 function market_views_default_views() {
807 // Item count (i.e. ownership of contracts)
808 $view = new stdClass();
809 $view->name = 'item_counts';
810 $view->description = 'counts items - arg for uid owning items';
811 $view->access = array ();
812 $view->view_args_php = '';
813 $view->page = TRUE;
814 $view->page_title = 'Item Counts';
815 $view->page_header = '';
816 $view->page_header_format = '1';
817 $view->page_footer = '';
818 $view->page_footer_format = '1';
819 $view->page_empty = '';
820 $view->page_empty_format = '1';
821 $view->page_type = 'table';
822 $view->url = 'item_count';
823 $view->use_pager = TRUE;
824 $view->nodes_per_page = '100';
825 $view->sort = array ();
826 $view->argument = array (
827 array (
828 'type' => 'uid',
829 'argdefault' => '2',
830 'title' => '',
831 'options' => '',
832 'wildcard' => '',
833 'wildcard_substitution' => '',
834 ),
835 );
836 $view->field = array (
837 array (
838 'tablename' => 'node_data_field_owned_items',
839 'field' => 'field_owned_items_nid',
840 'label' => 'Item',
841 'handler' => 'content_views_field_handler_group',
842 'options' => 'default',
843 ),
844 array (
845 'tablename' => 'node_data_field_item_count',
846 'field' => 'field_item_count_value',
847 'label' => 'Count',
848 'handler' => 'content_views_field_handler_group',
849 'options' => 'default',
850 ),
851 array (
852 'tablename' => 'node',
853 'field' => 'view',
854 'label' => 'View',
855 ),
856 array (
857 'tablename' => 'node',
858 'field' => 'edit',
859 'label' => 'Edit',
860 'handler' => 'views_handler_node_edit_destination',
861 ),
862 array (
863 'tablename' => 'users',
864 'field' => 'name',
865 'label' => 'Owner',
866 ),
867 array (
868 'tablename' => 'node',
869 'field' => 'nid',
870 'label' => 'NID of Item Count',
871 ),
872 );
873 $view->filter = array (
874 array (
875 'tablename' => 'node',
876 'field' => 'type',
877 'operator' => 'OR',
878 'options' => '',
879 'value' => array (
880 0 => 'item_count',
881 ),
882 ),
883 );
884 $view->exposed_filter = array (
885 array (
886 'tablename' => 'users',
887 'field' => 'uid',
888 'label' => 'Counts by user:',
889 'optional' => '1',
890 'is_default' => '0',
891 'operator' => '1',
892 'single' => '1',
893 ),
894 );
895 $view->requires = array(node_data_field_owned_items, node_data_field_item_count, node, users);
896 $views[$view->name] = $view;
897
898 // Trades
899 $view = new stdClass();
900 $view->name = 'trades';
901 $view->description = 'trades';
902 $view->access = array (
903 );
904 $view->view_args_php = '';
905 $view->page = TRUE;
906 $view->page_title = 'Trades';
907 $view->page_header = '';
908 $view->page_header_format = '1';
909 $view->page_footer = '';
910 $view->page_footer_format = '1';
911 $view->page_empty = 'No trades match your filter (or none exist)';
912 $view->page_empty_format = '1';
913 $view->page_type = 'table';
914 $view->url = 'trades';
915 $view->use_pager = TRUE;
916 $view->nodes_per_page = '100';
917 $view->sort = array (
918 );
919 $view->argument = array (
920 );
921 $view->field = array (
922 array (
923 'tablename' => 'node',
924 'field' => 'view',
925 'label' => '',
926 ),
927 array (
928 'tablename' => 'node',
929 'field' => 'edit',
930 'label' => 'Edit',
931 'handler' => 'views_handler_node_edit_destination',
932 ),
933 array (
934 'tablename' => 'node',
935 'field' => 'delete',
936 'label' => '',
937 'handler' => 'views_handler_node_delete_destination',
938 ),
939 array (
940 'tablename' => 'node_data_field_item',
941 'field' => 'field_item_nid',
942 'label' => 'Item to trade',
943 'handler' => 'content_views_field_handler_group',
944 'options' => 'default',
945 ),
946 array (
947 'tablename' => 'node_data_field_price',
948 'field' => 'field_price_value',
949 'label' => 'price',
950 'handler' => 'content_views_field_handler_group',
951 'options' => 'default',
952 ),
953 array (
954 'tablename' => 'node_data_field_quantity',
955 'field' => 'field_quantity_value',
956 'label' => 'original quantity',
957 'handler' => 'content_views_field_handler_group',
958 'options' => 'default',
959 ),
960 array (
961 'tablename' => 'node_data_field_remaining',
962 'field' => 'field_remaining_value',
963 'label' => 'Remaining Quantity',
964 'handler' => 'content_views_field_handler_group',
965 'options' => 'default',
966 ),
967 array (
968 'tablename' => 'workflow_states',
969 'field' => 'state',
970 'label' => 'state',
971 ),
972 array (
973 'tablename' => 'users',
974 'field' => 'name',
975 'label' => 'trader',
976 ),
977 array (
978 'tablename' => 'node_data_field_action',
979 'field' => 'field_action_value',
980 'label' => 'action',
981 'handler' => 'content_views_field_handler_group',
982 'options' => 'default',
983 ),
984 );
985 $view->filter = array (
986 array (
987 'tablename' => 'node',
988 'field' => 'type',
989 'operator' => 'OR',
990 'options' => '',
991 'value' => array (
992 0 => 'trade',
993 ),
994 ),
995 array (
996 'tablename' => 'workflow_node',
997 'field' => 'sid',
998 'operator' => 'OR',
999 'options' => '',
1000 'value' => array (
1001 0 => '2',
1002 ),
1003 ),
1004 );
1005 $view->exposed_filter = array (
1006 array (
1007 'tablename' => 'workflow_node',
1008 'field' => 'sid',
1009 'label' => 'State',
1010 'optional' => '1',
1011 'is_default' => '1',
1012 'operator' => '1',
1013 'single' => '0',
1014 ),
1015 );
1016 $view->requires = array(node, node_data_field_item, node_data_field_price, node_data_field_quantity, node_data_field_remaining, workflow_states, users, node_data_field_action, workflow_node);
1017 $views[$view->name] = $view;
1018
1019 // Items that are available to trade
1020 $view = new stdClass();
1021 $view->name = 'available_items';
1022 $view->description = '';
1023 $view->access = array (
1024 );
1025 $view->view_args_php = '';
1026 $view->page = TRUE;
1027 $view->page_title = '';
1028 $view->page_header = '';
1029 $view->page_header_format = '1';
1030 $view->page_footer = '';
1031 $view->page_footer_format = '1';
1032 $view->page_empty = '';
1033 $view->page_empty_format = '1';
1034 $view->page_type = 'table';
1035 $view->url = 'available_items';
1036 $view->use_pager = FALSE;
1037 $view->nodes_per_page = '50';
1038 $view->sort = array (
1039 array (
1040 'tablename' => 'node',
1041 'field' => 'created',
1042 'sortorder' => 'DESC',
1043 'options' => 'normal',
1044 ),
1045 );
1046 $view->argument = array (
1047 );
1048 $view->field = array (
1049 array (
1050 'tablename' => 'node',
1051 'field' => 'title',
1052 'label' => '',
1053 'handler' => 'views_handler_field_nodelink',
1054 'options' => 'link',
1055 ),
1056 );
1057 $view->filter = array (
1058 array (
1059 'tablename' => 'workflow_node',
1060 'field' => 'sid',
1061 'operator' => 'OR',
1062 'options' => '',
1063 'value' => array (
1064 0 => '11',
1065 1 => '8',
1066 ),
1067 ),
1068 array (
1069 'tablename' => 'node',
1070 'field' => 'type',
1071 'operator' => 'OR',
1072 'options' => '',
1073 'value' => array (
1074 0 => 'item',
1075 1 => 'portfolio',
1076 ),
1077 ),
1078 array (
1079 'tablename' => 'node',
1080 'field' => 'status',
1081 'operator' => '=',
1082 'options' => '',
1083 'value' => '1',
1084 ),
1085 );
1086 $view->exposed_filter = array (
1087 );
1088 $view->requires = array(node, workflow_node);
1089 $views[$view->name] = $view;
1090
1091 return $views;
1092 }
1093
1094 // TODO _later_
1095
1096 /*
1097 + Keep track of the original price and the clearing price on the trade nodes (since these can be different)
1098