PluginProbe
WCPOS – Point of Sale (POS) plugin for WooCommerce / 1.9.15
WCPOS – Point of Sale (POS) plugin for WooCommerce v1.9.15
1.10.19 1.10.18 1.10.17 1.10.16 1.10.15 1.10.13 1.10.14 1.10.12 1.10.11 1.10.10 1.10.9 1.10.8 untagged-3d9b7ccddc54df87c672 1.10.7 1.10.6 1.10.5 1.10.3 1.10.4 1.10.2 1.10.1 1.10.0 1.9.17 1.9.15 1.9.16 1.9.14 All 163 releases
woocommerce-pos / includes / Services / Cloud_Print_Submit_Service.php

Cloud_Print_Submit_Service.php in WCPOS – Point of Sale (POS) plugin for WooCommerce 1.9.15, at includes/Services/Cloud_Print_Submit_Service.php

318 lines 9.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Submits queued PrintNode print jobs out-of-band via WP-Cron.
4 *
5 * Checkout only schedules a single cron event; the actual HTTP submission to
6 * PrintNode happens here so the storefront request never blocks on the network.
7 *
8 * @package WCPOS\WooCommercePOS\Services
9 */
10
11 namespace WCPOS\WooCommercePOS\Services;
12
13 use WCPOS\WooCommercePOS\Logger;
14
15 /**
16 * Cloud_Print_Submit_Service class.
17 */
18 class Cloud_Print_Submit_Service {
19 /** Maximum number of submit attempts before a job is terminally failed. */
20 const MAX_ATTEMPTS = 3;
21
22 /**
23 * Job store.
24 *
25 * @var Print_Job_Service
26 */
27 private $jobs;
28
29 /**
30 * Printer registry.
31 *
32 * @var Cloud_Print_Registry
33 */
34 private $registry;
35
36 /**
37 * Constructor — hook the submit cron action.
38 */
39 public function __construct() {
40 $this->jobs = new Print_Job_Service();
41 $this->registry = new Cloud_Print_Registry();
42 add_action( Cloud_Print_Trigger_Service::CRON_SUBMIT, array( $this, 'submit' ), 10, 1 );
43 }
44
45 /**
46 * Submit a queued PrintNode job.
47 *
48 * Idempotent and concurrency-safe: a job that already carries a PrintNode job
49 * id is left alone, and an atomic per-job lock (double-checked under the lock)
50 * guarantees that two concurrent cron workers cannot both submit the same job
51 * and double-print.
52 *
53 * Transient PrintNode submit errors are retried with linear backoff up to
54 * MAX_ATTEMPTS, after which the job is terminally FAILED. Misconfigured-printer
55 * and empty-render failures are terminal immediately (never retried). The API
56 * key is never logged or stored — PrintNode_Client error messages omit it.
57 *
58 * @param int $job_id Print job ID.
59 */
60 public function submit( $job_id ): void {
61 $job_id = (int) $job_id;
62 $job = $this->jobs->get( $job_id );
63 if ( null === $job ) {
64 return;
65 }
66 if ( '' !== $job['external_job_id'] ) {
67 return;
68 }
69 // A job cancelled between scheduling and this run must not be sent.
70 // The cancel path cannot unschedule reliably (retries reschedule with
71 // fresh timestamps), so the worker is the authority on status.
72 if ( ! $this->is_submittable( $job ) ) {
73 return;
74 }
75
76 // Atomic guard: only one worker may submit a given job at a time.
77 if ( ! $this->jobs->acquire_lifecycle_lock( $job_id ) ) {
78 return;
79 }
80
81 try {
82 // Double-check under the lock in case another worker just finished
83 // or the job was cancelled while we waited for it.
84 $job = $this->jobs->get( $job_id );
85 if ( null === $job || '' !== $job['external_job_id'] || ! $this->is_submittable( $job ) ) {
86 return;
87 }
88
89 $printer = $this->registry->get_printer( (string) $job['printer_id'] );
90 if ( null === $printer ) {
91 $this->fail( $job_id, 'Cloud print: printer not found for job.' );
92
93 return;
94 }
95
96 $provider = (string) ( $printer['provider'] ?? '' );
97 if ( 'star-online' === $provider ) {
98 $this->submit_star_online( $job_id, $job, $printer );
99
100 return;
101 }
102
103 if ( 'printnode' !== $provider ) {
104 $this->fail( $job_id, 'Cloud print: unsupported push provider for job.' );
105
106 return;
107 }
108
109 $api_key = (string) ( $printer['printnode_api_key'] ?? '' );
110 $pn_printer_id = (int) ( $printer['printnode_printer_id'] ?? 0 );
111 if ( '' === $api_key || 0 === $pn_printer_id ) {
112 $this->fail( $job_id, 'Cloud print: PrintNode printer is missing an API key or printer id.' );
113
114 return;
115 }
116
117 $payload = $this->jobs->render_payload( $job );
118 if ( '' === $payload ) {
119 $this->fail( $job_id, 'Cloud print: PrintNode job produced no printable content.' );
120
121 return;
122 }
123
124 $content_type = 'escpos' === $job['pn_kind'] ? 'raw_base64' : 'pdf_base64';
125 $title = $this->title_for( $job );
126
127 $result = ( new PrintNode_Client( $api_key ) )->submit_job(
128 $pn_printer_id,
129 $title,
130 $content_type,
131 base64_encode( $payload )
132 );
133
134 if ( is_wp_error( $result ) ) {
135 $this->handle_submit_error( $job_id, $result->get_error_message() );
136
137 return;
138 }
139
140 $this->jobs->record_external_submission( $job_id, 'printnode', (string) $result['id'], 'submitted' );
141
142 if ( 'pdf' === $job['pn_kind'] && ! empty( $job['auto_open_drawer'] ) ) {
143 $drawer_result = $this->submit_printnode_drawer_kick( $api_key, $pn_printer_id, $job );
144 if ( is_wp_error( $drawer_result ) ) {
145 update_post_meta( $job_id, Print_Job_Service::META_DRAWER_ERROR, sanitize_text_field( $drawer_result->get_error_message() ) );
146 Logger::log( sprintf( 'Cloud print: PrintNode drawer kick failed for job %d after receipt submission.', $job_id ) );
147 }
148 }
149
150 $this->jobs->set_status( $job_id, Print_Job_Service::STATUS_PRINTED );
151 } finally {
152 $this->jobs->release_lifecycle_lock( $job_id );
153 }
154 }
155
156 /**
157 * Submit a best-effort raw ESC/POS drawer kick for a PrintNode PDF job.
158 *
159 * @param string $api_key PrintNode API key.
160 * @param int $pn_printer_id PrintNode printer id.
161 * @param array $job Job data.
162 *
163 * @return array|\WP_Error
164 */
165 private function submit_printnode_drawer_kick( string $api_key, int $pn_printer_id, array $job ) {
166 $title = $this->title_for( $job ) . ' Cash Drawer';
167
168 return ( new PrintNode_Client( $api_key ) )->submit_job(
169 $pn_printer_id,
170 $title,
171 'raw_base64',
172 base64_encode( $this->drawer_kick_bytes( (string) ( $job['drawer_connector'] ?? 'pin2' ) ) )
173 );
174 }
175
176 /**
177 * Build ESC/POS drawer kick bytes for PrintNode raw jobs.
178 *
179 * @param string $connector pin2 or pin5.
180 *
181 * @return string
182 */
183 private function drawer_kick_bytes( string $connector ): string {
184 $connector = Print_Job_Service::normalize_drawer_connector( $connector );
185 $pin = 'pin5' === $connector ? "\x01" : "\x00";
186
187 return "\x1B\x70" . $pin . "\x19\xFA";
188 }
189
190 /**
191 * Submit a queued Star Online job: render Star markup and POST it to stario.online.
192 *
193 * @param int $job_id Job id.
194 * @param array $job Job array.
195 * @param array $printer Registered star-online printer.
196 */
197 private function submit_star_online( int $job_id, array $job, array $printer ): void {
198 $api_key = (string) ( $printer['star_api_key'] ?? '' );
199 $url = (string) ( $printer['star_cloudprnt_url'] ?? '' );
200 $device_id = (string) ( $printer['star_device_id'] ?? '' );
201 $api_base = Star_Online_Client::api_base_from_cloudprnt_url( $url );
202 $group = Star_Online_Client::group_from_cloudprnt_url( $url );
203
204 if ( '' === $api_key || null === $api_base || '' === $group || '' === $device_id ) {
205 $this->fail( $job_id, 'Cloud print: Star Online printer is misconfigured.' );
206
207 return;
208 }
209
210 $payload = $this->jobs->render_payload( $job );
211 if ( '' === $payload ) {
212 $this->fail( $job_id, 'Cloud print: Star Online job produced no printable content.' );
213
214 return;
215 }
216
217 $result = ( new Star_Online_Client( $api_base, $api_key ) )->submit_job(
218 $group,
219 $device_id,
220 $this->title_for( $job ),
221 'text/vnd.star.markup',
222 $payload
223 );
224
225 if ( is_wp_error( $result ) ) {
226 $this->handle_submit_error( $job_id, $result->get_error_message() );
227
228 return;
229 }
230
231 $this->jobs->record_external_submission( $job_id, 'star-online', (string) $result['id'], 'submitted' );
232 $this->jobs->set_status( $job_id, Print_Job_Service::STATUS_PRINTED );
233 }
234
235 /**
236 * Whether a job's status still permits submission.
237 *
238 * Pending is the normal case; claimed covers a retry that a worker had
239 * already picked up. Cancelled, printed and failed jobs are terminal and
240 * must never be pushed to a provider.
241 *
242 * @param array $job Job row.
243 *
244 * @return bool True when the job may be submitted.
245 */
246 private function is_submittable( array $job ): bool {
247 return \in_array(
248 $job['status'] ?? '',
249 array( Print_Job_Service::STATUS_PENDING, Print_Job_Service::STATUS_CLAIMED ),
250 true
251 );
252 }
253
254 /**
255 * Handle a transient PrintNode submit error: retry with linear backoff up to
256 * MAX_ATTEMPTS, then terminally fail.
257 *
258 * The PrintNode client never includes the API key in its error messages, so
259 * recording the message verbatim is safe.
260 *
261 * @param int $job_id Job ID.
262 * @param string $error Failure reason from PrintNode_Client.
263 */
264 private function handle_submit_error( int $job_id, string $error ): void {
265 $attempts = (int) get_post_meta( $job_id, Print_Job_Service::META_SUBMIT_ATTEMPTS, true ) + 1;
266 update_post_meta( $job_id, Print_Job_Service::META_SUBMIT_ATTEMPTS, $attempts );
267 update_post_meta( $job_id, Print_Job_Service::META_ERROR, sanitize_text_field( $error ) );
268
269 if ( $attempts < self::MAX_ATTEMPTS ) {
270 $this->jobs->set_status( $job_id, Print_Job_Service::STATUS_PENDING );
271 wp_schedule_single_event(
272 time() + $attempts * 60,
273 Cloud_Print_Trigger_Service::CRON_SUBMIT,
274 array( $job_id )
275 );
276 Logger::log( sprintf( 'Cloud print: external submission failed for job %d, retry %d scheduled.', $job_id, $attempts ) );
277
278 return;
279 }
280
281 $this->jobs->set_status( $job_id, Print_Job_Service::STATUS_FAILED );
282 Logger::log( sprintf( 'Cloud print: external submission failed for job %d after %d attempts.', $job_id, $attempts ) );
283 }
284
285 /**
286 * Build a human-readable PrintNode job title.
287 *
288 * @param array $job Job array.
289 *
290 * @return string
291 */
292 private function title_for( array $job ): string {
293 if ( ! empty( $job['order_id'] ) ) {
294 $order = wc_get_order( (int) $job['order_id'] );
295 if ( $order ) {
296 return 'WCPOS Order #' . $order->get_order_number();
297 }
298 }
299
300 return 'WCPOS Print Job ' . (int) $job['id'];
301 }
302
303 /**
304 * Mark a job failed, record the error, and log a generic failure.
305 *
306 * The PrintNode client never includes the API key in its error messages, so
307 * recording the message verbatim is safe.
308 *
309 * @param int $job_id Job ID.
310 * @param string $error Failure reason.
311 */
312 private function fail( int $job_id, string $error ): void {
313 $this->jobs->set_status( $job_id, Print_Job_Service::STATUS_FAILED );
314 update_post_meta( $job_id, Print_Job_Service::META_ERROR, sanitize_text_field( $error ) );
315 Logger::log( sprintf( 'Cloud print: external submission failed for job %d.', $job_id ) );
316 }
317 }
318