Completed
Pull Request — master (#2165)
by Justin
06:55
created

WPSC_Purchase_Log::get_log_by_meta()   C

Complexity

Conditions 8
Paths 7

Size

Total Lines 24
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 8
eloc 15
c 0
b 0
f 0
nc 7
nop 2
dl 0
loc 24
rs 5.7377
1
<?php
2
// by default, expire stats cache after 48 hours
3
// this doesn't have any effect if you're not using APC or memcached
4
5
if ( ! defined( 'WPSC_PURCHASE_LOG_STATS_CACHE_EXPIRE' ) ) {
6
	define( 'WPSC_PURCHASE_LOG_STATS_CACHE_EXPIRE', DAY_IN_SECONDS * 2 );
7
}
8
9
class WPSC_Purchase_Log {
10
	const INCOMPLETE_SALE  		= 1;
11
	const ORDER_RECEIVED  	 	= 2;
12
	const ACCEPTED_PAYMENT		= 3;
13
	const JOB_DISPATCHED   		= 4;
14
	const CLOSED_ORDER     		= 5;
15
	const PAYMENT_DECLINED 		= 6;
16
	const REFUNDED         		= 7;
17
	const REFUND_PENDING   		= 8;
18
	const PARTIALLY_REFUNDED 	= 9;
19
20
	/**
21
	 * Names of column that requires escaping values as strings before being inserted
22
	 * into the database
23
	 *
24
	 * @access private
25
	 * @static
26
	 * @since 3.8.9
27
	 *
28
	 * @var array
29
	 */
30
	private static $string_cols = array(
31
		'sessionid',
32
		'transactid',
33
		'authcode',
34
		'date',
35
		'gateway',
36
		'billing_country',
37
		'shipping_country',
38
		'email_sent',
39
		'stock_adjusted',
40
		'discount_data',
41
		'track_id',
42
		'billing_region',
43
		'shipping_region',
44
		'find_us',
45
		'engrave_text',
46
		'shipping_method',
47
		'shipping_option',
48
		'affiliate_id',
49
		'plugin_version',
50
		'notes',
51
	);
52
53
	/**
54
	 * Names of column that requires escaping values as integers before being inserted
55
	 * into the database
56
	 *
57
	 * @static
58
	 * @since 3.8.9
59
	 * @var array
60
	 */
61
	private static $int_cols = array(
62
		'id',
63
		'statusno',
64
		'processed',
65
		'user_ID',
66
	);
67
68
	/**
69
	 * Names of column that requires escaping values as float before being inserted
70
	 * into the database
71
	 *
72
	 * @static
73
	 * @since 4.0
74
	 * @var array
75
	 */
76
	private static $float_cols = array(
77
		'totalprice',
78
		'base_shipping',
79
		'discount_value',
80
		'wpec_taxes_total',
81
		'wpec_taxes_rate',
82
	);
83
84
	/**
85
	 * Array of metadata
86
	 *
87
	 * @static
88
	 * @since 4.0
89
	 * @var array
90
	 */
91
	private static $metadata = array(
0 ignored issues
show
Unused Code introduced by
The property $metadata is not used and could be removed.

This check marks private properties in classes that are never used. Those properties can be removed.

Loading history...
92
		'totalprice',
93
		'base_shipping',
94
		'discount_value',
95
		'wpec_taxes_total',
96
		'wpec_taxes_rate',
97
	);
98
99
	/**
100
	 * Get the SQL query format for a column
101
	 *
102
	 * @since 3.8.9
103
	 * @param  string $col Name of the column
104
	 * @return string      Placeholder
105
	 */
106
	private static function get_column_format( $col ) {
107
		if ( in_array( $col, self::$string_cols ) ) {
108
			return '%s';
109
		}
110
111
		if ( in_array( $col, self::$int_cols ) ) {
112
			return '%d';
113
		}
114
115
		return '%f';
116
	}
117
118
	/**
119
	 * Query the purchase log table to get sales and earning stats
120
	 *
121
	 * Accepts an array of arguments:
122
	 * 	- 'ids': IDs of products for which you want to get stats
123
	 * 	- 'products': array of WPSC_Product objects for which you want to get stats
124
	 * 	- 'start' and 'end': the timestamp range (integers) during which you want
125
	 * 	                     to collect the stats.
126
	 * 	                     You can use none, either, or both of them.
127
	 * 	                     Note that the [start, end) interval is a left-closed,
128
	 * 	                     right-open.
129
	 * 	                     E.g.: to get stats from Jan 1st, 2013 to
130
	 * 	                     Dec 31st, 2013 (23:59:59),
131
	 * 	                     set "start" to the timestamp for Jan 1st, 2013, set
132
	 * 	                     "end" to the timestamp for Jan 1st, 2014
133
	 *  - 'order': what to sort the results by, defaults to 'id'.
134
	 *            Can be 'ids', 'sales', 'earnings' or empty string to disable sorting
135
	 *  - 'orderby': how to sort the results, defaults to 'ASC'.
136
	 *              Can be 'ASC', 'DESC' (lowercase is fine too)
137
	 *  - 'per_page': How many items to fetch, defaults to 0, which fetches all results
138
	 *  - 'page': Which page of the results to fetch, defaults to 1.
139
	 *            Has no effect if per_page is set to 0.
140
	 *
141
	 * @since 3.8.14
142
	 * @param  array|string $args Arguments
143
	 * @return array       Array containing 'sales' and 'earnings' stats
144
	 */
145
	public static function fetch_stats( $args ) {
146
		global $wpdb;
147
148
		$defaults = array(
149
			'ids'      => array(), // IDs of the products to be queried
150
			'products' => array(), // Array of WPSC_Products objects
151
			'start'    => 0,       // Int. timestamp, has to be UTC
152
			'end'      => 0,       // Int. timestamp, has to be UTC
153
			'order'    => 'ASC',
154
			'orderby'  => 'id',
155
			'per_page' => 0,
156
			'page'     => 1,
157
		);
158
159
		$args = wp_parse_args( $args, $defaults );
160
161
		// convert more advanced date / time args into "start" and "end"
162
		$args = self::convert_date_args( $args );
163
164
		// build an array of WPSC_Product objects based on 'ids' and 'products' args
165
		$products = array_merge(
166
			$args['products'],
167
			array_map( array( 'WPSC_Product', 'get_instance' ), $args['ids'] )
168
		);
169
170
		// eliminate duplicates (Note: usage of this requires PHP 5.2.9)
171
		$products = array_unique( $products, SORT_REGULAR );
172
173
		if ( empty( $products ) ) {
174
			return null;
175
		}
176
177
		$needs_fetching = array();
178
179
		$stats = array(
180
			'sales'    => 0,
181
			'earnings' => 0,
182
		);
183
184
		// if we have date restriction, that means we won't be able to rely upon
185
		// individual stats cache inside WPSC_Product objects
186
		$has_date_restriction = ! ( empty( $args['start'] ) && empty( $args['end'] ) );
187
188
		// if we do NOT have date restriction, find out which WPSC_Product object
189
		// has stats cache, and which don't
190
		if ( ! $has_date_restriction ) {
191
			foreach ( $products as $product ) {
192
				// store the ID if this product doesn't have a stats cache yet
193
				if ( $product->post->_wpsc_stats === '' ) {
194
					$needs_fetching[] = $product->post->ID;
195
				} else {
196
197
					// tally up the sales and earnings if this one has cache already
198
					$prod_meta = get_post_meta( $product->post->ID, '_wpsc_stats', true );
199
200
					if ( isset( $prod_meta['sales'] ) && isset( $prod_meta['earnings'] ) ) {
201
						$stats['sales']    += $prod_meta['sales'];
202
						$stats['earnings'] += $prod_meta['earnings'];
203
					}
204
					$needs_fetching[]   = $product->post->ID;
205
				}
206
			}
207
		}
208
209
		// never hurts to make sure
210
		$needs_fetching = array_map( 'absint', $needs_fetching );
211
212
		// pagination arguments
213
		$limit = '';
214
215
		if ( ! empty( $args['per_page'] ) ) {
216
			$offset = ( $args['page'] - 1 ) * $args['per_page'];
217
			$limit = "LIMIT " . absint( $args['per_page'] ) . " OFFSET " . absint( $offset );
218
		}
219
220
		// sorting
221
		$order = '';
222
223
		if ( ! empty( $args['orderby'] ) )
224
			$order = "ORDER BY " . esc_sql( $args['orderby'] ) . " " . esc_sql( $args['order'] );
225
226
		// date
227
		$where = "WHERE p.processed IN (3, 4, 5)";
228
229
		if ( $has_date_restriction ) {
230
			// start date equal or larger than $args['start']
231
			if ( ! empty( $args['start'] ) )
232
				$where .= " AND CAST(p.date AS UNSIGNED) >= " . absint( $args['start'] );
233
234
			// end date less than $args['end'].
235
			// the "<" sign is not a typo, such upper limit makes it easier for
236
			// people to specify range.
237
			// E.g.: [1/1/2013 - 1/1/2014) rather than:
238
			//       [1/1/2013 - 31/12/2013 23:59:59]
239
			if ( ! empty( $args['end'] ) )
240
				$where .= " AND CAST(p.date AS UNSIGNED) < " . absint( $args['end'] );
241
		}
242
243
		// assemble the SQL query
244
		$sql = "
245
			SELECT cc.prodid AS id, SUM(cc.quantity) AS sales, SUM(cc.quantity * cc.price) AS earnings
246
			FROM $wpdb->wpsc_purchase_logs AS p
247
			INNER JOIN
248
				$wpdb->wpsc_cart_contents AS cc
249
				ON p.id = cc.purchaseid AND cc.prodid IN (" . implode( ', ', $needs_fetching ) . ")
250
			{$where}
251
			GROUP BY cc.prodid
252
			{$order}
253
			{$limit}
254
		";
255
256
		// if the result is cached, don't bother querying the database
257
		$cache_key = md5( $sql );
258
		$results   = wp_cache_get( $cache_key, 'wpsc_purchase_logs_stats' );
259
260
		if ( false === $results ) {
261
			$results = $wpdb->get_results( $sql );
262
			wp_cache_set( $cache_key, $results, 'wpsc_purchase_logs_stats', WPSC_PURCHASE_LOG_STATS_CACHE_EXPIRE );
263
		}
264
265
		// tally up the sales and earnings from the query results
266
		foreach ( $results as $row ) {
267
			if ( ! $has_date_restriction ) {
268
				$product           = WPSC_Product::get_instance( $row->id );
269
				$product->sales    = $row->sales;
270
				$product->earnings = $row->earnings;
271
			}
272
273
			$stats['sales']    += $row->sales;
274
			$stats['earnings'] += $row->earnings;
275
		}
276
277
		return $stats;
278
	}
279
280
	/**
281
	 * Convert advanced date/time arguments like year, month, day, 'ago' etc.
282
	 * into basic arguments which are "start" and "end".
283
	 *
284
	 * @since  3.8.14
285
	 * @param  array $args Arguments
286
	 * @return array       Arguments after converting
287
	 */
288
	private static function convert_date_args( $args ) {
289
		// TODO: Implement this
290
		return $args;
291
	}
292
293
	/**
294
	 * Get overall sales and earning stats for just one product
295
	 *
296
	 * @since 3.8.14
297
	 * @param  int $id ID of the product
298
	 * @return array   Array containing 'sales' and 'earnings' stats
299
	 */
300
	public static function get_stats_for_product( $id, $args = '' ) {
301
302
		$product = WPSC_Product::get_instance( $id );
303
304
		// if this product has variations
305
		if ( $product->has_variations ) {
306
			// get total stats of variations
307
			$args['products'] = $product->variations;
308
		} else {
309
			// otherwise, get stats of only this product
310
			$args['products'] = array( $product );
311
		}
312
313
		return self::fetch_stats( $args );
314
	}
315
316
	/**
317
	 * Check whether the status code indicates a completed status
318
	 *
319
	 * @since 3.8.13
320
	 * @param int  $status Status code
321
	 * @return boolean
322
	 */
323
	public static function is_order_status_completed( $status ) {
324
		$completed_status = apply_filters( 'wpsc_order_status_completed', array(
325
			self::ACCEPTED_PAYMENT,
326
			self::JOB_DISPATCHED,
327
			self::CLOSED_ORDER,
328
		) );
329
330
		return in_array( $status, $completed_status );
331
	}
332
333
	/**
334
	 * Contains the values fetched from the DB
335
	 *
336
	 * @access private
337
	 * @since 3.8.9
338
	 *
339
	 * @var array
340
	 */
341
	private $data = array();
342
343
	/**
344
	 * Data that is not directly stored inside the DB but is inferred
345
	 *
346
	 * @since 3.9
347
	 * @var array
348
	 */
349
	private $meta_data = array();
350
351
	private $gateway_data = array();
352
353
	/**
354
	 * True if the DB row is fetched into $this->data.
355
	 *
356
	 * @access private
357
	 * @since 3.8.9
358
	 *
359
	 * @var string
360
	 */
361
	private $fetched           = false;
362
	private $is_status_changed = false;
363
	private $previous_status   = false;
364
365
	private $cart_contents = array();
366
	private $cart_ids = array();
367
	private $can_edit = null;
368
369
	/**
370
	 * Contains the constructor arguments. This array is necessary because we will
371
	 * lazy load the DB row into $this->data whenever necessary. Lazy loading is,
372
	 * in turn, necessary because sometimes right after saving a new record, we need
373
	 * to fetch a property with the same object.
374
	 *
375
	 * @access private
376
	 * @since 3.8.9
377
	 *
378
	 * @var array
379
	 */
380
	private $args = array(
381
		'col'   => '',
382
		'value' => '',
383
	);
384
385
	/**
386
	 * True if the row exists in DB
387
	 *
388
	 * @access private
389
	 * @since 3.8.9
390
	 *
391
	 * @var string
392
	 */
393
	private $exists = false;
394
395
	/**
396
	 * Update cache of the passed log object
397
	 *
398
	 * @access public
399
	 * @static
400
	 * @since 3.8.9
401
	 *
402
	 * @param WPSC_Purchase_Log $log The log object that you want to store into cache
403
	 * @return void
404
	 */
405
	public static function update_cache( &$log ) {
406
		// wpsc_purchase_logs stores the data array, while wpsc_purchase_logs_sessionid stores the
407
		// log id that's associated with the sessionid
408
409
		$id = $log->get( 'id' );
410
		wp_cache_set( $id, $log->data, 'wpsc_purchase_logs' );
411
		if ( $sessionid = $log->get( 'sessionid' ) )
412
			wp_cache_set( $sessionid, $id, 'wpsc_purchase_logs_sessionid' );
413
		wp_cache_set( $id, $log->cart_contents, 'wpsc_purchase_log_cart_contents' );
414
		do_action( 'wpsc_purchase_log_update_cache', $log );
415
	}
416
417
	/**
418
	 * Deletes cache of a log (either by using the log ID or sessionid)
419
	 *
420
	 * @access public
421
	 * @static
422
	 * @since 3.8.9
423
	 *
424
	 * @param string $value The value to query
425
	 * @param string $col Optional. Defaults to 'id'. Whether to delete cache by using
426
	 *                    a purchase log ID or sessionid
427
	 * @return void
428
	 */
429
	public static function delete_cache( $value, $col = 'id' ) {
430
		// this will pull from the old cache, so no worries there
431
		$log = new WPSC_Purchase_Log( $value, $col );
432
433
		wp_cache_delete( $log->get( 'id' ), 'wpsc_purchase_logs' );
434
		wp_cache_delete( $log->get( 'sessionid' ), 'wpsc_purchase_logs_sessionid' );
435
		wp_cache_delete( $log->get( 'id' ), 'wpsc_purchase_log_cart_contents' );
436
		wp_cache_delete( $log->get( 'id' ), 'wpsc_purchase_log_cart_contents' );
437
		wp_cache_delete( $log->get( 'id' ), 'wpsc_purchase_meta' );
438
439
		do_action( 'wpsc_purchase_log_delete_cache', $log, $value, $col );
440
	}
441
442
	/**
443
	 * Deletes a log from the database.
444
	 *
445
	 * @access  public
446
	 * @since   3.8.9
447
	 *
448
	 * @uses  $wpdb                              Global database instance.
449
	 * @uses  wpsc_is_store_admin()              Check user has admin capabilities.
450
	 * @uses  WPSC_Purchase_Log::delete_cache()  Delete purchaselog cache.
451
	 * @uses  WPSC_Claimed_Stock                 Claimed Stock class.
452
	 *
453
	 * @param   string   $log_id   ID of the log.
0 ignored issues
show
Documentation introduced by
Should the type for parameter $log_id not be false|string?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
454
	 * @return  boolean            Deleted successfully.
455
	 */
456
	public function delete( $log_id = false ) {
457
458
		global $wpdb;
459
460
		if ( ! ( isset( $this ) && get_class( $this ) == __CLASS__ ) ) {
461
			_wpsc_doing_it_wrong( 'WPSC_Purchase_Log::delete', __( 'WPSC_Purchase_Log::delete() is no longer a static method and should not be called statically.', 'wp-e-commerce' ), '3.9.0' );
462
		}
463
464
		if ( false !== $log_id ) {
465
			_wpsc_deprecated_argument( __FUNCTION__, '3.9.0', 'The $log_id param is not used. You must first create an instance of WPSC_Purchase_Log before calling this method.' );
466
		}
467
468
		if ( ! wpsc_is_store_admin() ) {
469
			return false;
470
		}
471
472
		$log_id = $this->get( 'id' );
473
474
		if ( $log_id > 0 ) {
475
476
			do_action( 'wpsc_purchase_log_before_delete', $log_id );
477
478
			self::delete_cache( $log_id );
479
480
			// Delete claimed stock
481
			$purchlog_status = $wpdb->get_var( $wpdb->prepare( "SELECT `processed` FROM `" . WPSC_TABLE_PURCHASE_LOGS . "` WHERE `id`= %d", $log_id ) );
482
			if ( $purchlog_status == WPSC_Purchase_Log::CLOSED_ORDER || $purchlog_status == WPSC_Purchase_Log::INCOMPLETE_SALE ) {
483
				$claimed_query = new WPSC_Claimed_Stock( array(
484
					'cart_id'        => $log_id,
485
					'cart_submitted' => 1
486
				) );
487
				$claimed_query->clear_claimed_stock( 0 );
488
			}
489
490
			// Delete cart content, submitted data, then purchase log
491
			$wpdb->query( $wpdb->prepare( "DELETE FROM `" . WPSC_TABLE_CART_CONTENTS . "` WHERE `purchaseid` = %d", $log_id ) );
492
			$wpdb->query( $wpdb->prepare( "DELETE FROM `" . WPSC_TABLE_SUBMITTED_FORM_DATA . "` WHERE `log_id` IN (%d)", $log_id ) );
493
			$wpdb->query( $wpdb->prepare( "DELETE FROM `" . WPSC_TABLE_PURCHASE_LOGS . "` WHERE `id` = %d LIMIT 1", $log_id ) );
494
			$wpdb->query( $wpdb->prepare( "DELETE FROM `" . WPSC_TABLE_PURCHASE_META . "` WHERE `wpsc_purchase_id` = %d", $log_id ) );
495
			$wpdb->query( $wpdb->prepare( "DELETE FROM `" . WPSC_TABLE_DOWNLOAD_STATUS . "` WHERE `purchid` = %d ", $log_id ) );
496
497
			do_action( 'wpsc_purchase_log_delete', $log_id );
498
499
			return true;
500
501
		}
502
503
		return false;
504
505
	}
506
507
	/**
508
	 * Constructor of the purchase log object. If no argument is passed, this simply
509
	 * create a new empty object. Otherwise, this will get the purchase log from the
510
	 * DB either by using purchase log id or sessionid (specified by the 2nd argument).
511
	 *
512
	 * Eg:
513
	 *
514
	 * // get purchase log with ID number 23
515
	 * $log = new WPSC_Purchase_Log( 23 );
516
	 *
517
	 * // get purchase log with sessionid "asdf"
518
	 * $log = new WPSC_Purchase_Log( 'asdf', 'sessionid' )
519
	 *
520
	 * @access public
521
	 * @since 3.8.9
522
	 *
523
	 * @param string $value Optional. Defaults to false.
0 ignored issues
show
Documentation introduced by
Should the type for parameter $value not be false|string?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
524
	 * @param string $col Optional. Defaults to 'id'.
525
	 */
526
	public function __construct( $value = false, $col = 'id' ) {
527
		if ( false === $value ) {
528
			return;
529
		}
530
531
		if ( is_array( $value ) ) {
532
			$this->set( $value );
533
			return;
534
		}
535
536
		global $wpdb;
537
538
		if ( ! in_array( $col, array( 'id', 'sessionid' ) ) )
539
			return;
540
541
		// store the constructor args into an array so that later we can lazy load the data
542
		$this->args = array(
543
			'col'   => $col,
544
			'value' => $value,
545
		);
546
547
		// if the sessionid is in cache, pull out the id
548
		if ( $col == 'sessionid'  && $id = wp_cache_get( $value, 'wpsc_purchase_logs_sessionid' ) ) {
549
				$col = 'id';
550
				$value = $id;
551
		}
552
553
		// if the id is specified, try to get from cache
554
		if ( $col == 'id' ) {
555
			$this->data = wp_cache_get( $value, 'wpsc_purchase_logs' );
556
			$this->cart_contents = wp_cache_get( $value, 'wpsc_purchase_log_cart_contents' );
557
		}
558
559
		// cache exists
560
		if ( $this->data ) {
561
			$this->fetched = true;
0 ignored issues
show
Documentation Bug introduced by
The property $fetched was declared of type string, but true is of type boolean. Maybe add a type cast?

This check looks for assignments to scalar types that may be of the wrong type.

To ensure the code behaves as expected, it may be a good idea to add an explicit type cast.

$answer = 42;

$correct = false;

$correct = (bool) $answer;
Loading history...
562
			$this->exists  = true;
0 ignored issues
show
Documentation Bug introduced by
The property $exists was declared of type string, but true is of type boolean. Maybe add a type cast?

This check looks for assignments to scalar types that may be of the wrong type.

To ensure the code behaves as expected, it may be a good idea to add an explicit type cast.

$answer = 42;

$correct = false;

$correct = (bool) $answer;
Loading history...
563
			return;
564
		}
565
	}
566
567
	private function set_total_shipping() {
568
569
		$base_shipping  = $this->get( 'base_shipping' );
570
		$item_shipping  = wp_list_pluck( $this->get_cart_contents(), 'pnp' );
571
572
		$this->meta_data['total_shipping'] = $base_shipping + array_sum( $item_shipping );
573
574
		return $this->meta_data['total_shipping'];
575
	}
576
577
	private function set_gateway_name() {
578
		global $wpsc_gateways;
579
		$gateway = $this->get( 'gateway' );
580
		$gateway_name = $gateway;
581
582
		if( 'wpsc_merchant_testmode' == $gateway )
583
			$gateway_name = __( 'Manual Payment', 'wp-e-commerce' );
584
		elseif ( isset( $wpsc_gateways[$gateway] ) )
585
			$gateway_name = $wpsc_gateways[$gateway]['name'];
586
587
		$this->meta_data['gateway_name'] = $gateway_name;
588
		return $this->meta_data['gateway_name'];
589
	}
590
591
	private function set_shipping_method_names() {
592
		global $wpsc_shipping_modules;
593
594
		$shipping_method = $this->get( 'shipping_method' );
595
		$shipping_option = $this->get( 'shipping_option' );
596
		$shipping_method_name = $shipping_method;
597
		$shipping_option_name = $shipping_option;
598
599
		if ( ! empty ( $wpsc_shipping_modules[$shipping_method] ) ) {
0 ignored issues
show
Coding Style introduced by
Space before opening parenthesis of function call prohibited
Loading history...
600
			$shipping_class = $wpsc_shipping_modules[$shipping_method];
601
			$shipping_method_name = $shipping_class->name;
602
		}
603
604
		$this->meta_data['shipping_method_name'] = $shipping_method_name;
605
		$this->meta_data['shipping_option_name'] = $shipping_option_name;
606
	}
607
608
	private function set_meta_props() {
609
610
		foreach ( wpsc_get_purchase_custom( $this->get( 'id' ) ) as $key => $value  ) {
611
			$this->meta_data[ $key ] = wpsc_get_purchase_meta( $this->get( 'id' ), $key, true );
612
		}
613
614
		$this->set_total_shipping();
615
		$this->set_gateway_name();
616
		$this->set_shipping_method_names();
617
	}
618
619
	public function get_meta() {
620
621
		if ( empty( $this->data ) && empty( $this->meta_data ) ) {
622
			$this->fetch();
623
		}
624
625
		return (array) apply_filters( 'wpsc_purchase_log_meta_data', $this->meta_data );
626
	}
627
628
	/**
629
	 * Fetches the actual record from the database
630
	 *
631
	 * @access private
632
	 * @since 3.8.9
633
	 *
634
	 * @return void
635
	 */
636
	private function fetch() {
637
		global $wpdb;
638
639
		if ( $this->fetched ) {
640
			return;
641
		}
642
643
		// If $this->args is not set yet, it means the object contains a new unsaved
644
		// row so we don't need to fetch from DB
645
		if ( ! $this->args['col'] || ! $this->args['value'] ) {
646
			return;
647
		}
648
649
		extract( $this->args );
0 ignored issues
show
introduced by
extract() usage is highly discouraged, due to the complexity and unintended issues it might cause.
Loading history...
650
651
		$format = self::get_column_format( $col );
652
		$sql    = $wpdb->prepare( "SELECT * FROM " . WPSC_TABLE_PURCHASE_LOGS . " WHERE {$col} = {$format}", $value );
653
654
		$this->exists = false;
0 ignored issues
show
Documentation Bug introduced by
The property $exists was declared of type string, but false is of type false. Maybe add a type cast?

This check looks for assignments to scalar types that may be of the wrong type.

To ensure the code behaves as expected, it may be a good idea to add an explicit type cast.

$answer = 42;

$correct = false;

$correct = (bool) $answer;
Loading history...
655
656
		if ( $data = $wpdb->get_row( $sql, ARRAY_A ) ) {
657
			$this->exists        = true;
0 ignored issues
show
Documentation Bug introduced by
The property $exists was declared of type string, but true is of type boolean. Maybe add a type cast?

This check looks for assignments to scalar types that may be of the wrong type.

To ensure the code behaves as expected, it may be a good idea to add an explicit type cast.

$answer = 42;

$correct = false;

$correct = (bool) $answer;
Loading history...
658
			$this->data          = apply_filters( 'wpsc_purchase_log_data', $data );
659
			$this->cart_contents = $this->get_cart_contents();
660
661
			$this->set_meta_props();
662
			self::update_cache( $this );
663
		}
664
665
		do_action( 'wpsc_purchase_log_fetched', $this );
666
667
		$this->fetched = true;
0 ignored issues
show
Documentation Bug introduced by
The property $fetched was declared of type string, but true is of type boolean. Maybe add a type cast?

This check looks for assignments to scalar types that may be of the wrong type.

To ensure the code behaves as expected, it may be a good idea to add an explicit type cast.

$answer = 42;

$correct = false;

$correct = (bool) $answer;
Loading history...
668
	}
669
670
	/**
671
	 * Whether the DB row for this purchase log exists
672
	 *
673
	 * @access public
674
	 * @since 3.8.9
675
	 *
676
	 * @return bool True if it exists. Otherwise false.
677
	 */
678
	public function exists() {
679
		$this->fetch();
680
		return $this->exists;
681
	}
682
683
	/**
684
	 * Returns the value of the specified property of the purchase log
685
	 *
686
	 * @access public
687
	 * @since 3.8.9
688
	 *
689
	 * @param string $key Name of the property (column)
690
	 * @return mixed
691
	 */
692
	public function get( $key ) {
693
		// lazy load the purchase log row if it's not fetched from the database yet
694
		if ( empty( $this->data ) || ! array_key_exists( $key, $this->data ) ) {
695
			$this->fetch();
696
		}
697
698
		if ( isset( $this->data[ $key ] ) ) {
699
			$value = $this->data[ $key ];
700
		} else if ( isset( $this->meta_data[ $key ] ) ) {
701
			$value = $this->meta_data[ $key ];
702
		} else {
703
			$value = null;
704
		}
705
706
		return apply_filters( 'wpsc_purchase_log_get_property', $value, $key, $this );
707
	}
708
709
	public function get_cart_contents() {
710
		global $wpdb;
711
712
		if ( ! empty( $this->cart_contents ) && $this->fetched ) {
713
			return $this->cart_contents;
714
		}
715
716
		$id = $this->get( 'id' );
717
718
		// Bail if we don't have a log object yet (no id).
719
		if ( empty( $id ) ) {
720
			return $this->cart_contents;
721
		}
722
723
		$sql = $wpdb->prepare( "SELECT * FROM " . WPSC_TABLE_CART_CONTENTS . " WHERE purchaseid = %d", $id );
724
		$this->cart_contents = $wpdb->get_results( $sql );
725
726
		if ( is_array( $this->cart_contents ) ) {
727
			foreach ( $this->cart_contents as $index => $item ) {
728
				$this->cart_ids[ absint( $item->id ) ] = $index;
729
			}
730
		}
731
732
		return $this->cart_contents;
733
	}
734
735
	public function get_cart_item( $item_id ) {
736
		$item_id = absint( $item_id );
737
		$cart    = $this->get_cart_contents();
738
739
		if ( isset( $this->cart_ids[ $item_id ] ) ) {
740
			return $cart[ $this->cart_ids[ $item_id ] ];
741
		}
742
743
		return false;
744
	}
745
746
	public function update_cart_item( $item_id, $data ) {
747
		global $wpdb;
748
749
		$item_id = absint( $item_id );
750
		$item = $this->get_cart_item( $item_id );
751
752
		if ( $item ) {
753
			do_action( 'wpsc_purchase_log_before_update_cart_item', $item_id );
754
755
			$data = wp_unslash( $data );
756
			$result = $wpdb->update( WPSC_TABLE_CART_CONTENTS, $data, array( 'id' => $item_id  ) );
757
758
			if ( $result ) {
759
760
				$this->cart_contents = array();
761
				$this->get_cart_item( $item_id );
762
763
				do_action( 'wpsc_purchase_log_update_cart_item', $item_id );
764
			}
765
766
			return $result;
767
		}
768
769
		return false;
770
	}
771
772
	public function remove_cart_item( $item_id ) {
773
		global $wpdb;
774
775
		$item_id = absint( $item_id );
776
		$item = $this->get_cart_item( $item_id );
777
778
		if ( $item ) {
779
			do_action( 'wpsc_purchase_log_before_remove_cart_item', $item_id );
780
781
			$result = $wpdb->delete( WPSC_TABLE_CART_CONTENTS, array( 'id' => $item_id ) );
782
783
			if ( $result ) {
784
785
				unset( $this->cart_contents[ $this->cart_ids[ $item_id ] ] );
786
				unset( $this->cart_ids[ $item_id ] );
787
788
				do_action( 'wpsc_purchase_log_remove_cart_item', $item_id );
789
			}
790
791
			return $result;
792
		}
793
794
		return false;
795
	}
796
797
	/**
798
	 * Init the purchase log items for this purchase log.
799
	 *
800
	 * @since  4.0
801
	 *
802
	 * @return wpsc_purchaselogs_items|false The purhchase log item object or false.
803
	 */
804
	public function init_items() {
805
		global $purchlogitem;
806
		if ( ! $this->exists() ) {
807
			return false;
808
		}
809
810
		// TODO: seriously get rid of all these badly coded purchaselogs.functions.php functions and wpsc_purchaselogs/wpsc_purchaselogs_items classes in 4.0
811
		$purchlogitem = new wpsc_purchaselogs_items( $this->get( 'id' ) );
812
813
		return $purchlogitem;
814
	}
815
816
	/**
817
	 * Returns the whole database row in the form of an associative array
818
	 *
819
	 * @access public
820
	 * @since 3.8.9
821
	 *
822
	 * @return array
823
	 */
824
	public function get_data() {
825
		if ( empty( $this->data ) ) {
826
			$this->fetch();
827
		}
828
829
		return apply_filters( 'wpsc_purchase_log_get_data', $this->data, $this );
830
	}
831
832
	public function get_gateway_data( $from_currency = false, $to_currency = false ) {
833
		if ( ! $this->exists() ) {
834
			return array();
835
		}
836
837
		$subtotal = 0;
838
		$shipping = wpsc_convert_currency( (float) $this->get( 'base_shipping' ), $from_currency, $to_currency );
839
		$items    = array();
840
841
		$this->gateway_data = array(
842
			'amount'  => wpsc_convert_currency( $this->get( 'totalprice' ), $from_currency, $to_currency ),
843
			'invoice' => $this->get( 'sessionid' ),
844
			'tax'     => wpsc_convert_currency( $this->get( 'wpec_taxes_total' ), $from_currency, $to_currency ),
845
		);
846
847
		foreach ( $this->cart_contents as $item ) {
848
			$item_price = wpsc_convert_currency( $item->price, $from_currency, $to_currency );
849
			$items[] = array(
850
				'name'     => $item->name,
851
				'amount'   => $item_price,
852
				'tax'      => wpsc_convert_currency( $item->tax_charged, $from_currency, $to_currency ),
853
				'quantity' => $item->quantity,
854
			);
855
			$subtotal += $item_price * $item->quantity;
856
			$shipping += wpsc_convert_currency( $item->pnp, $from_currency, $to_currency );
857
		}
858
859
		$this->gateway_data['discount'] = wpsc_convert_currency( (float) $this->get( 'discount_value' ), $from_currency, $to_currency );
860
861
		$this->gateway_data['items'] = $items;
862
		$this->gateway_data['shipping'] = $shipping;
863
		$this->gateway_data['subtotal'] = $subtotal;
864
865
		if ( $from_currency ) {
866
			// adjust total amount in case there's slight decimal error
867
			$total = $subtotal + $shipping + $this->gateway_data['tax'] - $this->gateway_data['discount'];
868
			if ( $this->gateway_data['amount'] != $total ) {
869
				$this->gateway_data['amount'] = $total;
870
			}
871
		}
872
873
		$this->gateway_data = apply_filters( 'wpsc_purchase_log_gateway_data', $this->gateway_data, $this->get_data() );
874
		return $this->gateway_data;
875
	}
876
877
	/**
878
	 * Sets a property to a certain value. This function accepts a key and a value
879
	 * as arguments, or an associative array containing key value pairs.
880
	 *
881
	 * Loops through data, comparing against database, and saves as meta if not found in purchase log table.
882
	 *
883
	 * @access public
884
	 * @since 3.8.9
885
	 *
886
	 * @param mixed $key Name of the property (column), or an array containing key
887
	 *                   value pairs
888
	 * @param string|int $value Optional. Defaults to false. In case $key is a string,
0 ignored issues
show
Documentation introduced by
Should the type for parameter $value not be string|integer|null?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
889
	 *                          this should be specified.
890
	 * @return WPSC_Purchase_Log The current object (for method chaining)
891
	 */
892
	public function set( $key, $value = null ) {
893
		if ( is_array( $key ) ) {
894
			$properties = $key;
895
		} else {
896
			if ( is_null( $value ) ) {
897
				return $this;
898
			}
899
900
			$properties = array( $key => $value );
901
		}
902
903
		$properties = apply_filters( 'wpsc_purchase_log_set_properties', $properties, $this );
904
905
		if ( array_key_exists( 'processed', $properties ) ) {
906
			$this->previous_status = $this->get( 'processed' );
907
908
			if ( $properties['processed'] != $this->previous_status ) {
909
				$this->is_status_changed = true;
910
			}
911
		}
912
913
		if ( ! is_array( $this->data ) ) {
914
			$this->data = array();
915
		}
916
917
		foreach ( $properties as $key => $value ) {
918
			if ( ! in_array( $key, array_merge( self::$string_cols, self::$int_cols, self::$float_cols ) ) ) {
919
				$this->meta_data[ $key ] = $value;
920
				unset( $properties[ $key ] );
921
			}
0 ignored issues
show
introduced by
Blank line found after control structure
Loading history...
922
923
		}
924
925
		$this->data = array_merge( $this->data, $properties );
926
		return $this;
927
	}
928
929
	/**
930
	 * Returns an array containing the parameter format so that this can be used in
931
	 * $wpdb methods (update, insert etc.)
932
	 *
933
	 * @access private
934
	 * @since 3.8.9
935
	 *
936
	 * @param array $data
937
	 * @return array
938
	 */
939
	private function get_data_format( $data ) {
940
		$format = array();
941
942
		foreach ( $data as $key => $value ) {
943
			$format[] = self::get_column_format( $key );
944
		}
945
946
		return $format;
947
	}
948
949
	/**
950
	 * Saves the purchase log back to the database
951
	 *
952
	 * @access public
953
	 * @since 3.8.9
954
	 *
955
	 * @return void
956
	 */
957
	public function save() {
958
		global $wpdb;
959
960
		do_action( 'wpsc_purchase_log_pre_save', $this );
961
962
		// $this->args['col'] is empty when the record is new and needs to
963
		// be inserted. Otherwise, it means we're performing an update
964
		$where_col = $this->args['col'];
965
966
		$result = false;
967
968
		if ( $where_col ) {
969
			$where_val = $this->args['value'];
970
			$where_format = self::get_column_format( $where_col );
971
			do_action( 'wpsc_purchase_log_pre_update', $this );
972
			self::delete_cache( $where_val, $where_col );
973
			$data = apply_filters( 'wpsc_purchase_log_update_data', $this->data );
974
			$format = $this->get_data_format( $data );
975
			$result = $wpdb->update( WPSC_TABLE_PURCHASE_LOGS, $data, array( $where_col => $where_val ), $format, array( $where_format ) );
976
			do_action( 'wpsc_purchase_log_update', $this );
977
		} else {
978
			do_action( 'wpsc_purchase_log_pre_insert', $this );
979
			$data = apply_filters( 'wpsc_purchase_log_insert_data', $this->data );
980
			$format = $this->get_data_format( $data );
981
			$result = $wpdb->insert( WPSC_TABLE_PURCHASE_LOGS, $data, $format );
982
983
			if ( $result ) {
984
				$this->set( 'id', $wpdb->insert_id );
985
986
				// set $this->args so that properties can be lazy loaded right after
987
				// the row is inserted into the db
988
				$this->args = array(
989
					'col'   => 'id',
990
					'value' => $this->get( 'id' ),
991
				);
992
			}
993
994
			do_action( 'wpsc_purchase_log_insert', $this );
995
		}
996
997
		if ( $this->is_status_changed ) {
998
999
			if ( $this->is_transaction_completed() ) {
1000
				$this->update_downloadable_status();
1001
			}
1002
1003
			$current_status          = $this->get( 'processed' );
1004
			$previous_status         = $this->previous_status;
1005
			$this->previous_status   = $current_status;
1006
			$this->is_status_changed = false;
1007
1008
			do_action( 'wpsc_update_purchase_log_status', $this->get( 'id' ), $current_status, $previous_status, $this );
1009
		}
1010
1011
		if ( ! empty( $this->meta_data ) ) {
1012
			$this->save_meta_data();
1013
		}
1014
1015
		do_action( 'wpsc_purchase_log_save', $this );
1016
1017
		return $result;
1018
	}
1019
1020
	/**
1021
	 * Save meta data for purchase log, if any was set via set().
1022
	 *
1023
	 * @since  4.0
1024
	 * @return void
1025
	 */
1026
	private function save_meta_data() {
1027
		do_action( 'wpsc_purchase_log_pre_save_meta', $this );
1028
1029
		$meta = $this->get_meta();
1030
1031
		foreach ( $meta as $key => $value ) {
1032
			wpsc_update_purchase_meta( $this->get( 'id' ), $key, $value );
1033
		}
1034
1035
		do_action( 'wpsc_purchase_log_save_meta', $this );
1036
	}
1037
1038
	private function update_downloadable_status() {
1039
		global $wpdb;
1040
1041
		foreach ( $this->get_cart_contents() as $item ) {
1042
			$wpdb->update(
1043
				WPSC_TABLE_DOWNLOAD_STATUS,
1044
				array(
1045
					'active' => '1'
1046
				),
1047
				array(
1048
					'cartid'  => $item->id,
1049
					'purchid' => $this->get( 'id' ),
1050
				)
1051
			);
1052
		}
1053
	}
1054
1055
	/**
1056
	 * Adds ability to retrieve a purchase log by a meta key or value.
1057
	 *
1058
	 * @since  4.0
1059
	 *
1060
	 * @param  string $key   Meta key. Optional.
1061
	 * @param  string $value Meta value. Optional.
1062
	 *
1063
	 * @return false|WPSC_Purchase_Log  False if no log is found or meta key and value are both not provided. WPSC_Purchase_Log object if found.
1064
	 */
1065
	public static function get_log_by_meta( $key = '', $value = '' ) {
1066
1067
		if ( empty( $key ) && empty( $value ) ) {
1068
			return false;
1069
		}
1070
1071
		global $wpdb;
1072
1073
		if ( ! empty( $key ) && empty( $value ) ) {
1074
			$sql = $wpdb->prepare( 'SELECT wpsc_purchase_id FROM ' . WPSC_TABLE_PURCHASE_META . ' WHERE meta_key = %s', $key );
1075
		} else if ( empty( $key ) && ! empty( $value ) ) {
1076
			$sql = $wpdb->prepare( 'SELECT wpsc_purchase_id FROM ' . WPSC_TABLE_PURCHASE_META . ' WHERE meta_value = %s', $value );
1077
		} else {
1078
			$sql = $wpdb->prepare( 'SELECT wpsc_purchase_id FROM ' . WPSC_TABLE_PURCHASE_META . ' WHERE meta_key = %s AND meta_value = %s', $key, $value );
1079
		}
1080
1081
		$id = $wpdb->get_var( $sql );
1082
1083
		if ( $id ) {
1084
			return new WPSC_Purchase_Log( $id );
1085
		} else {
1086
			return false;
1087
		}
1088
	}
1089
1090
	public function get_next_log_id() {
1091
		if ( ! $this->exists() ) {
1092
			return false;
1093
		}
1094
1095
		global $wpdb;
1096
1097
		$sql = $wpdb->prepare(
1098
			"SELECT MIN(id) FROM " . WPSC_TABLE_PURCHASE_LOGS . " WHERE id > %d",
1099
			$this->get( 'id' )
1100
		);
1101
1102
		return $wpdb->get_var( $sql );
1103
	}
1104
1105
	public function get_previous_log_id() {
1106
		if ( ! $this->exists() ) {
1107
			return false;
1108
		}
1109
1110
		global $wpdb;
1111
1112
		$sql = $wpdb->prepare(
1113
			"SELECT MAX(id) FROM " . WPSC_TABLE_PURCHASE_LOGS . " WHERE id < %d",
1114
			$this->get( 'id' )
1115
		);
1116
1117
		return $wpdb->get_var( $sql );
1118
	}
1119
1120
	public function is_transaction_completed() {
1121
		return WPSC_Purchase_Log::is_order_status_completed( $this->get( 'processed' ) );
1122
	}
1123
1124
	public function can_edit() {
1125
		if ( null === $this->can_edit ) {
1126
			$can_edit = current_user_can( 'edit_others_posts' ) && ! $this->is_transaction_completed();
1127
			$this->can_edit = apply_filters( 'wpsc_can_edit_order', $can_edit, $this );
1128
		}
1129
1130
		return $this->can_edit;
1131
	}
1132
1133
	public function is_order_received() {
1134
		return $this->get( 'processed' ) == self::ORDER_RECEIVED;
1135
	}
1136
1137
	public function is_incomplete_sale() {
1138
		return $this->get( 'processed' ) == self::INCOMPLETE_SALE;
1139
	}
1140
1141
	public function is_accepted_payment() {
1142
		return $this->get( 'processed' ) == self::ACCEPTED_PAYMENT;
1143
	}
1144
1145
	public function is_job_dispatched() {
1146
		return $this->get( 'processed' ) == self::JOB_DISPATCHED;
1147
	}
1148
1149
	public function is_closed_order() {
1150
		return $this->get( 'processed' ) == self::CLOSED_ORDER;
1151
	}
1152
1153
	public function is_payment_declined() {
1154
		return $this->get( 'processed' ) == self::PAYMENT_DECLINED;
1155
	}
1156
1157
	public function is_refunded() {
1158
		return $this->get( 'processed' ) == self::REFUNDED;
1159
	}
1160
1161
	public function is_refund_pending() {
1162
		return $this->get( 'processed' ) == self::REFUND_PENDING;
1163
	}
1164
}
1165