| 1 |
<?php |
| 2 |
/** |
| 3 |
* Creates cloud print jobs from WooCommerce order events. |
| 4 |
* |
| 5 |
* @package WCPOS\WooCommercePOS\Services |
| 6 |
*/ |
| 7 |
|
| 8 |
namespace WCPOS\WooCommercePOS\Services; |
| 9 |
|
| 10 |
use WCPOS\WooCommercePOS\Logger; |
| 11 |
|
| 12 |
/** |
| 13 |
* Cloud_Print_Trigger_Service class. |
| 14 |
*/ |
| 15 |
class Cloud_Print_Trigger_Service { |
| 16 |
const OPTION = 'woocommerce_pos_settings_cloud_print'; |
| 17 |
|
| 18 |
/** |
| 19 |
* Cron hook used to submit a PrintNode job out-of-band (never on checkout). |
| 20 |
*/ |
| 21 |
const CRON_SUBMIT = 'wcpos_cloud_print_submit'; |
| 22 |
|
| 23 |
/** |
| 24 |
* Job store. |
| 25 |
* |
| 26 |
* @var Print_Job_Service |
| 27 |
*/ |
| 28 |
private $jobs; |
| 29 |
|
| 30 |
/** |
| 31 |
* Printer registry. |
| 32 |
* |
| 33 |
* @var Cloud_Print_Registry |
| 34 |
*/ |
| 35 |
private $registry; |
| 36 |
|
| 37 |
/** |
| 38 |
* Constructor — hook order events. |
| 39 |
*/ |
| 40 |
public function __construct() { |
| 41 |
$this->jobs = new Print_Job_Service(); |
| 42 |
$this->registry = new Cloud_Print_Registry(); |
| 43 |
add_action( 'woocommerce_new_order', array( $this, 'handle_order' ), 20, 1 ); |
| 44 |
add_action( 'woocommerce_order_status_changed', array( $this, 'handle_order' ), 20, 1 ); |
| 45 |
} |
| 46 |
|
| 47 |
/** |
| 48 |
* Create jobs for an order according to the configured assignments. |
| 49 |
* |
| 50 |
* @param int $order_id Order ID. |
| 51 |
*/ |
| 52 |
public function handle_order( $order_id ): void { |
| 53 |
$order = wc_get_order( (int) $order_id ); |
| 54 |
if ( ! $order ) { |
| 55 |
return; |
| 56 |
} |
| 57 |
|
| 58 |
$settings = get_option( self::OPTION, array() ); |
| 59 |
$assignments = isset( $settings['assignments'] ) && \is_array( $settings['assignments'] ) ? $settings['assignments'] : array(); |
| 60 |
|
| 61 |
/** |
| 62 |
* Filter the cloud-print assignments for an order. Pro uses this to |
| 63 |
* substitute per-outlet assignments based on the order's store. |
| 64 |
* |
| 65 |
* @param array $assignments Global assignments. |
| 66 |
* @param \WC_Order $order The order being processed. |
| 67 |
*/ |
| 68 |
$assignments = apply_filters( 'woocommerce_pos_cloud_print_assignments', $assignments, $order ); |
| 69 |
if ( ! \is_array( $assignments ) ) { |
| 70 |
$assignments = array(); |
| 71 |
} |
| 72 |
|
| 73 |
if ( empty( $assignments ) ) { |
| 74 |
return; |
| 75 |
} |
| 76 |
|
| 77 |
$is_pos = 'woocommerce-pos' === $order->get_created_via(); |
| 78 |
|
| 79 |
foreach ( $assignments as $assignment ) { |
| 80 |
if ( empty( $assignment['printer_id'] ) || empty( $assignment['template_id'] ) ) { |
| 81 |
continue; |
| 82 |
} |
| 83 |
$scope = isset( $assignment['scope'] ) ? (string) $assignment['scope'] : 'every'; |
| 84 |
if ( ! $this->scope_matches( $scope, $is_pos ) ) { |
| 85 |
continue; |
| 86 |
} |
| 87 |
if ( $this->already_queued( $order->get_id(), (string) $assignment['printer_id'], (string) $assignment['template_id'] ) ) { |
| 88 |
continue; |
| 89 |
} |
| 90 |
|
| 91 |
$printer = $this->registry->get_printer( (string) $assignment['printer_id'] ); |
| 92 |
if ( empty( $printer ) ) { |
| 93 |
continue; |
| 94 |
} |
| 95 |
$provider = (string) ( $printer['provider'] ?? '' ); |
| 96 |
|
| 97 |
$template_id = (string) $assignment['template_id']; |
| 98 |
$template = Print_Job_Service::load_template( $template_id ); |
| 99 |
if ( null === $template ) { |
| 100 |
continue; |
| 101 |
} |
| 102 |
|
| 103 |
$job_id = self::enqueue_order_job( |
| 104 |
$this->jobs, |
| 105 |
(string) $assignment['printer_id'], |
| 106 |
$printer, |
| 107 |
$order->get_id(), |
| 108 |
$template_id, |
| 109 |
$template |
| 110 |
); |
| 111 |
if ( 0 === $job_id ) { |
| 112 |
Logger::log( |
| 113 |
sprintf( |
| 114 |
'Cloud print: skipping assignment for printer "%s" — template "%s" is not printable on provider "%s".', |
| 115 |
(string) $assignment['printer_id'], |
| 116 |
$template_id, |
| 117 |
$provider |
| 118 |
) |
| 119 |
); |
| 120 |
} |
| 121 |
} |
| 122 |
} |
| 123 |
|
| 124 |
/** |
| 125 |
* Enqueue a print job for an order + template, deriving the wire format from |
| 126 |
* the printer's provider. Shared by the order-event trigger and the manual |
| 127 |
* print-jobs endpoint so the two cannot drift. |
| 128 |
* |
| 129 |
* For PrintNode the job's submit event is scheduled out-of-band (PrintNode |
| 130 |
* does not poll). For polling providers (Star/Epson) the printer fetches the |
| 131 |
* job on its next poll, so no submit is scheduled. |
| 132 |
* |
| 133 |
* @param Print_Job_Service $jobs Job store. |
| 134 |
* @param string $printer_id Registered printer id. |
| 135 |
* @param array $printer Registered printer config. |
| 136 |
* @param int $order_id Order id to render. |
| 137 |
* @param string $template_id Template id (numeric) or virtual slug. |
| 138 |
* @param array $template Loaded template array. |
| 139 |
* @param array $drawer_options Drawer options. |
| 140 |
* |
| 141 |
* @return int Created job id, or 0 when the template is not printable on the provider. |
| 142 |
*/ |
| 143 |
public static function enqueue_order_job( Print_Job_Service $jobs, string $printer_id, array $printer, int $order_id, string $template_id, array $template, array $drawer_options = array() ): int { |
| 144 |
$provider = (string) ( $printer['provider'] ?? '' ); |
| 145 |
$drawer_options = self::drawer_options_for_provider( $provider, $drawer_options ); |
| 146 |
|
| 147 |
if ( 'printnode' === $provider ) { |
| 148 |
$fmt = ( new Print_Format_Resolver() )->resolve( $printer, $template ); |
| 149 |
if ( '' === $fmt['kind'] ) { |
| 150 |
return 0; |
| 151 |
} |
| 152 |
|
| 153 |
$job_id = $jobs->create( |
| 154 |
array( |
| 155 |
'printer_id' => $printer_id, |
| 156 |
'order_id' => $order_id, |
| 157 |
'template_id' => $template_id, |
| 158 |
'content_type' => $fmt['content_type'], |
| 159 |
'pn_kind' => $fmt['kind'], |
| 160 |
'auto_open_drawer' => ! empty( $drawer_options['auto_open_drawer'] ), |
| 161 |
'drawer_connector' => $drawer_options['drawer_connector'], |
| 162 |
) |
| 163 |
); |
| 164 |
if ( $job_id > 0 ) { |
| 165 |
wp_schedule_single_event( time(), self::CRON_SUBMIT, array( $job_id ) ); |
| 166 |
} |
| 167 |
|
| 168 |
return $job_id; |
| 169 |
} |
| 170 |
|
| 171 |
$engine = (string) ( $template['engine'] ?? '' ); |
| 172 |
$wire = Provider::wire_format( $provider, $engine ); |
| 173 |
if ( null === $wire ) { |
| 174 |
return 0; |
| 175 |
} |
| 176 |
|
| 177 |
$job_id = $jobs->create( |
| 178 |
array( |
| 179 |
'printer_id' => $printer_id, |
| 180 |
'content_type' => Provider::content_type( $provider ), |
| 181 |
'order_id' => $order_id, |
| 182 |
'template_id' => $template_id, |
| 183 |
'auto_open_drawer' => ! empty( $drawer_options['auto_open_drawer'] ), |
| 184 |
'drawer_connector' => $drawer_options['drawer_connector'], |
| 185 |
) |
| 186 |
); |
| 187 |
|
| 188 |
// Push providers (e.g. Star Online) don't poll us; submit out-of-band. |
| 189 |
if ( $job_id > 0 && Provider::requires_submit( $provider ) ) { |
| 190 |
wp_schedule_single_event( time(), self::CRON_SUBMIT, array( $job_id ) ); |
| 191 |
} |
| 192 |
|
| 193 |
return $job_id; |
| 194 |
} |
| 195 |
|
| 196 |
/** |
| 197 |
* Keep drawer metadata scoped to providers implemented by this server change. |
| 198 |
* |
| 199 |
* Star providers use Star-specific drawer commands and are intentionally not |
| 200 |
* changed by the Epson/PrintNode implementation. |
| 201 |
* |
| 202 |
* @param string $provider Provider key. |
| 203 |
* @param array $drawer_options Drawer options. |
| 204 |
* |
| 205 |
* @return array{auto_open_drawer:bool, drawer_connector:string} |
| 206 |
*/ |
| 207 |
private static function drawer_options_for_provider( string $provider, array $drawer_options ): array { |
| 208 |
if ( ! in_array( $provider, array( 'epson-sdp', 'printnode' ), true ) ) { |
| 209 |
return array( |
| 210 |
'auto_open_drawer' => false, |
| 211 |
'drawer_connector' => 'pin2', |
| 212 |
); |
| 213 |
} |
| 214 |
|
| 215 |
return array( |
| 216 |
'auto_open_drawer' => ! empty( $drawer_options['auto_open_drawer'] ), |
| 217 |
'drawer_connector' => Print_Job_Service::normalize_drawer_connector( (string) ( $drawer_options['drawer_connector'] ?? 'pin2' ) ), |
| 218 |
); |
| 219 |
} |
| 220 |
|
| 221 |
/** |
| 222 |
* Whether an assignment scope applies to this order origin. |
| 223 |
* |
| 224 |
* @param string $scope every|pos|online. |
| 225 |
* @param bool $is_pos Whether the order was created via the POS. |
| 226 |
*/ |
| 227 |
private function scope_matches( string $scope, bool $is_pos ): bool { |
| 228 |
if ( 'every' === $scope ) { |
| 229 |
return true; |
| 230 |
} |
| 231 |
if ( 'pos' === $scope ) { |
| 232 |
return $is_pos; |
| 233 |
} |
| 234 |
if ( 'online' === $scope ) { |
| 235 |
return ! $is_pos; |
| 236 |
} |
| 237 |
|
| 238 |
return false; |
| 239 |
} |
| 240 |
|
| 241 |
/** |
| 242 |
* Guard against duplicate jobs for the same order+printer+template. |
| 243 |
* |
| 244 |
* @param int $order_id Order ID. |
| 245 |
* @param string $printer_id Printer ID. |
| 246 |
* @param string $template_id Template ID. |
| 247 |
*/ |
| 248 |
private function already_queued( int $order_id, string $printer_id, string $template_id ): bool { |
| 249 |
$existing = $this->jobs->query( |
| 250 |
array( |
| 251 |
'printer_id' => $printer_id, |
| 252 |
'order_id' => $order_id, |
| 253 |
'template_id' => $template_id, |
| 254 |
'limit' => 1, |
| 255 |
) |
| 256 |
); |
| 257 |
|
| 258 |
return ! empty( $existing ); |
| 259 |
} |
| 260 |
} |
| 261 |
|