class-db-manager.php 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279
  1. <?php
  2. /**
  3. * Database operations and CSV-protocol processing for the
  4. * Studiou WC Order Print Statuses plugin.
  5. */
  6. defined('ABSPATH') || exit;
  7. class Studiou_DB_Manager {
  8. /**
  9. * Order statuses that block automated print-pipeline transitions.
  10. * Cancelled / refunded orders should not be silently reactivated by a bulk
  11. * action; completed orders are already finished.
  12. */
  13. const TERMINAL_STATUSES = array('cancelled', 'refunded', 'failed', 'completed');
  14. /**
  15. * Return one row per (order × line-item) for the print-shop export.
  16. * Multiple product categories are concatenated into prod_cat instead of
  17. * multiplying the row.
  18. *
  19. * @param int[] $order_ids
  20. * @return array
  21. */
  22. public static function get_orders_for_printing($order_ids) {
  23. global $wpdb;
  24. $order_ids = array_filter(array_map('intval', (array) $order_ids));
  25. if (empty($order_ids)) {
  26. return array();
  27. }
  28. $placeholders = implode(',', array_fill(0, count($order_ids), '%d'));
  29. $sql = "
  30. SELECT
  31. `orders`.`id` AS `order_no`,
  32. GROUP_CONCAT(DISTINCT `t`.`name` ORDER BY `t`.`name` SEPARATOR ', ') AS `prod_cat`,
  33. `products`.`post_title` AS `prod_name`,
  34. `product_variations`.`post_title` AS `prod_var`,
  35. `product_variations_name`.`name` AS `prod_var_type`,
  36. `images`.`guid` AS `prod_img_url`,
  37. `order_products`.`product_qty` AS `qty`,
  38. `orders`.`billing_email` AS `email`
  39. FROM `{$wpdb->prefix}wc_orders` `orders`
  40. JOIN `{$wpdb->prefix}wc_order_product_lookup` `order_products`
  41. ON `orders`.`id` = `order_products`.`order_id`
  42. JOIN `{$wpdb->posts}` `products`
  43. ON `products`.`ID` = `order_products`.`product_id`
  44. JOIN `{$wpdb->posts}` `product_variations`
  45. ON `product_variations`.`ID` = `order_products`.`variation_id`
  46. LEFT JOIN `{$wpdb->postmeta}` `product_variations_attr`
  47. ON `product_variations_attr`.`post_id` = `product_variations`.`ID`
  48. AND `product_variations_attr`.`meta_key` = 'attribute_pa_format'
  49. LEFT JOIN `{$wpdb->terms}` `product_variations_name`
  50. ON `product_variations_name`.`name` = `product_variations_attr`.`meta_value`
  51. LEFT JOIN `{$wpdb->postmeta}` `product_meta`
  52. ON `product_meta`.`post_id` = `products`.`ID`
  53. AND `product_meta`.`meta_key` = '_thumbnail_id'
  54. LEFT JOIN `{$wpdb->posts}` `images`
  55. ON `images`.`ID` = `product_meta`.`meta_value`
  56. AND `images`.`post_type` = 'attachment'
  57. LEFT JOIN `{$wpdb->term_relationships}` `tr`
  58. ON `tr`.`object_id` = `products`.`ID`
  59. LEFT JOIN `{$wpdb->term_taxonomy}` `tt`
  60. ON `tt`.`term_taxonomy_id` = `tr`.`term_taxonomy_id`
  61. AND `tt`.`taxonomy` = 'product_cat'
  62. LEFT JOIN `{$wpdb->terms}` `t`
  63. ON `t`.`term_id` = `tt`.`term_id`
  64. WHERE `orders`.`id` IN ($placeholders)
  65. GROUP BY
  66. `orders`.`id`,
  67. `order_products`.`product_id`,
  68. `order_products`.`variation_id`
  69. ORDER BY `orders`.`id`
  70. ";
  71. $prepared = $wpdb->prepare($sql, $order_ids);
  72. $results = $wpdb->get_results($prepared);
  73. if ($results === false) {
  74. UtilsLog::log('SQL error in get_orders_for_printing: ' . $wpdb->last_error);
  75. return array();
  76. }
  77. return $results;
  78. }
  79. /**
  80. * Bulk-set orders to "to-print", skipping orders in terminal states.
  81. *
  82. * @param int[] $order_ids
  83. * @return int Number of orders actually updated.
  84. */
  85. public static function set_orders_to_prepare_printing($order_ids) {
  86. return self::bulk_set_print_status($order_ids, 'to-print', 'to_print_date');
  87. }
  88. /**
  89. * Bulk-set orders to "in-print", skipping orders in terminal states.
  90. *
  91. * @param int[] $order_ids
  92. * @return int Number of orders actually updated.
  93. */
  94. public static function set_orders_to_in_printing($order_ids) {
  95. return self::bulk_set_print_status($order_ids, 'in-print', 'in_print_date');
  96. }
  97. private static function bulk_set_print_status($order_ids, $status_slug, $meta_key) {
  98. $updated_count = 0;
  99. $skipped = 0;
  100. foreach ((array) $order_ids as $order_id) {
  101. $order_id = (int) $order_id;
  102. $order = wc_get_order($order_id);
  103. if (!$order) {
  104. UtilsLog::log('Order #' . $order_id . ' not found');
  105. continue;
  106. }
  107. if ($order->has_status(self::TERMINAL_STATUSES)) {
  108. UtilsLog::log('Order #' . $order_id . ' skipped (terminal status: ' . $order->get_status() . ')');
  109. $skipped++;
  110. continue;
  111. }
  112. $order->update_status($status_slug);
  113. $order->update_meta_data($meta_key, current_time('mysql'));
  114. $order->save();
  115. $updated_count++;
  116. UtilsLog::log('Order #' . $order_id . ' marked as "' . $status_slug . '"');
  117. }
  118. if ($skipped > 0) {
  119. UtilsLog::message(
  120. sprintf(
  121. _n(
  122. '%d order skipped because its status is final (cancelled, refunded, failed, or completed).',
  123. '%d orders skipped because their status is final (cancelled, refunded, failed, or completed).',
  124. $skipped,
  125. 'studiou-wc-ord-print-statuses'
  126. ),
  127. $skipped
  128. ),
  129. 'warning'
  130. );
  131. }
  132. return $updated_count;
  133. }
  134. /**
  135. * Import an InPrint protocol CSV.
  136. *
  137. * @param array $csv_data Rows produced by parse_csv().
  138. * @return array{updated_count:int,errors:string[]}
  139. */
  140. public static function import_inprint_protocol($csv_data) {
  141. $updated_count = 0;
  142. $errors = array();
  143. foreach (self::dedupe_by_order_no($csv_data) as $row) {
  144. if (!isset($row['order_no']) || !isset($row['externalorder']) || !isset($row['externalorderdate'])) {
  145. $errors[] = __('Invalid row format in CSV.', 'studiou-wc-ord-print-statuses');
  146. continue;
  147. }
  148. $order = wc_get_order($row['order_no']);
  149. if (!$order) {
  150. $errors[] = sprintf(__('Order %s not found.', 'studiou-wc-ord-print-statuses'), $row['order_no']);
  151. continue;
  152. }
  153. if ($order->has_status(self::TERMINAL_STATUSES)) {
  154. $errors[] = sprintf(
  155. __('Order %s is in a final state and was not updated.', 'studiou-wc-ord-print-statuses'),
  156. $row['order_no']
  157. );
  158. continue;
  159. }
  160. $order->update_status('in-print');
  161. $order->update_meta_data('in_print_date', current_time('mysql'));
  162. $order->update_meta_data('external_ref_ord_no', sanitize_text_field($row['externalorder']));
  163. $order->update_meta_data('external_ref_ord_date', self::normalise_csv_date($row['externalorderdate']));
  164. $order->save();
  165. $updated_count++;
  166. }
  167. return array('updated_count' => $updated_count, 'errors' => $errors);
  168. }
  169. /**
  170. * Import a Delivered protocol CSV.
  171. *
  172. * @param array $csv_data Rows produced by parse_csv().
  173. * @return array{updated_count:int,errors:string[]}
  174. */
  175. public static function import_delivered_protocol($csv_data) {
  176. $updated_count = 0;
  177. $errors = array();
  178. foreach (self::dedupe_by_order_no($csv_data) as $row) {
  179. if (!isset($row['order_no'])) {
  180. $errors[] = __('Invalid row format in CSV.', 'studiou-wc-ord-print-statuses');
  181. continue;
  182. }
  183. $order = wc_get_order($row['order_no']);
  184. if (!$order) {
  185. $errors[] = sprintf(__('Order %s not found.', 'studiou-wc-ord-print-statuses'), $row['order_no']);
  186. continue;
  187. }
  188. $order->update_status('completed');
  189. $order->update_meta_data('external_ref_ord_delivered', current_time('mysql'));
  190. $order->save();
  191. $updated_count++;
  192. }
  193. return array('updated_count' => $updated_count, 'errors' => $errors);
  194. }
  195. /**
  196. * Parse a CSV file into an array of associative rows.
  197. * Headers are normalised (lowercased, whitespace stripped) so files prepared
  198. * to either the legacy spec casing ("Order No") or the documented lowercase
  199. * form ("order_no") both work.
  200. *
  201. * @param string $file_path
  202. * @return array Empty array on failure.
  203. */
  204. public static function parse_csv($file_path) {
  205. $csv_data = array();
  206. $handle = @fopen($file_path, 'r');
  207. if ($handle === false) {
  208. UtilsLog::log('parse_csv: failed to open ' . $file_path);
  209. return $csv_data;
  210. }
  211. try {
  212. $headers = fgetcsv($handle, 0, ',');
  213. if (!is_array($headers) || empty(array_filter($headers))) {
  214. UtilsLog::log('parse_csv: header row missing or empty');
  215. return $csv_data;
  216. }
  217. $headers = array_map(array(__CLASS__, 'normalise_header'), $headers);
  218. while (($data = fgetcsv($handle, 0, ',')) !== false) {
  219. if (count($data) !== count($headers)) {
  220. continue;
  221. }
  222. $csv_data[] = array_combine($headers, $data);
  223. }
  224. } finally {
  225. fclose($handle);
  226. }
  227. return $csv_data;
  228. }
  229. private static function normalise_header($header) {
  230. $header = strtolower(trim((string) $header));
  231. // Map "order no" → "order_no" by collapsing internal whitespace to underscore.
  232. $header = preg_replace('/\s+/', '_', $header);
  233. return $header;
  234. }
  235. private static function dedupe_by_order_no($rows) {
  236. $seen = array();
  237. $unique = array();
  238. foreach ($rows as $row) {
  239. $key = isset($row['order_no']) ? trim((string) $row['order_no']) : '';
  240. if ($key === '' || isset($seen[$key])) {
  241. continue;
  242. }
  243. $seen[$key] = true;
  244. $unique[] = $row;
  245. }
  246. return $unique;
  247. }
  248. private static function normalise_csv_date($raw) {
  249. $raw = sanitize_text_field((string) $raw);
  250. if ($raw === '') {
  251. return '';
  252. }
  253. $timestamp = strtotime($raw);
  254. return $timestamp ? date('Y-m-d H:i:s', $timestamp) : $raw;
  255. }
  256. }