PluginProbe
WCPOS – Point of Sale (POS) plugin for WooCommerce / 1.10.18
WCPOS – Point of Sale (POS) plugin for WooCommerce v1.10.18
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 1.9.13 All 162 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.10.18, at includes/Services/Cloud_Print_Submit_Service.php

213 lines 6.7 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 use WCPOS\WooCommercePOS\Interfaces\Push_Provider_Adapter_Interface;
15
16 /**
17 * Cloud_Print_Submit_Service class.
18 */
19 class Cloud_Print_Submit_Service {
20 /** Maximum number of submit attempts before a job is terminally failed. */
21 const MAX_ATTEMPTS = 3;
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 the submit cron action.
39 */
40 public function __construct() {
41 $this->jobs = new Print_Job_Service();
42 $this->registry = new Cloud_Print_Registry();
43 add_action( Cloud_Print_Trigger_Service::CRON_SUBMIT, array( $this, 'submit' ), 10, 1 );
44 }
45
46 /**
47 * Submit a queued PrintNode job.
48 *
49 * Idempotent and concurrency-safe: a job that already carries a PrintNode job
50 * id is left alone, and an atomic per-job lock (double-checked under the lock)
51 * guarantees that two concurrent cron workers cannot both submit the same job
52 * and double-print.
53 *
54 * Transient PrintNode submit errors are retried with linear backoff up to
55 * MAX_ATTEMPTS, after which the job is terminally FAILED. Misconfigured-printer
56 * and empty-render failures are terminal immediately (never retried). The API
57 * key is never logged or stored — PrintNode_Client error messages omit it.
58 *
59 * @param int $job_id Print job ID.
60 */
61 public function submit( $job_id ): void {
62 $job_id = (int) $job_id;
63 $job = $this->jobs->get( $job_id );
64 if ( null === $job ) {
65 return;
66 }
67 if ( '' !== $job['external_job_id'] ) {
68 return;
69 }
70 // A job cancelled between scheduling and this run must not be sent.
71 // The cancel path cannot unschedule reliably (retries reschedule with
72 // fresh timestamps), so the worker is the authority on status.
73 if ( ! $this->is_submittable( $job ) ) {
74 return;
75 }
76
77 // Atomic guard: only one worker may submit a given job at a time.
78 if ( ! $this->jobs->acquire_lifecycle_lock( $job_id ) ) {
79 return;
80 }
81
82 try {
83 // Double-check under the lock in case another worker just finished
84 // or the job was cancelled while we waited for it.
85 $job = $this->jobs->get( $job_id );
86 if ( null === $job || '' !== $job['external_job_id'] || ! $this->is_submittable( $job ) ) {
87 return;
88 }
89
90 $printer = $this->registry->get_printer( (string) $job['printer_id'] );
91 if ( null === $printer ) {
92 $this->fail( $job_id, 'Cloud print: printer not found for job.' );
93
94 return;
95 }
96
97 $provider = (string) ( $printer['provider'] ?? '' );
98 $adapter = Provider::adapter( $provider );
99 if ( ! $adapter instanceof Push_Provider_Adapter_Interface ) {
100 $this->fail( $job_id, 'Cloud print: unsupported push provider for job.' );
101
102 return;
103 }
104
105 $payload = $this->jobs->render_payload( $job );
106 $result = $adapter->submit( $printer, $job, $payload, $this->title_for( $job ) );
107 if ( ! $result['success'] ) {
108 if ( $result['retryable'] ) {
109 $this->handle_submit_error( $job_id, $result['error'] );
110 } else {
111 $this->fail( $job_id, $result['error'] );
112 }
113
114 return;
115 }
116
117 $this->jobs->record_external_submission( $job_id, $provider, $result['external_job_id'], 'submitted' );
118
119 if ( '' !== $result['drawer_error'] ) {
120 update_post_meta( $job_id, Print_Job_Service::META_DRAWER_ERROR, sanitize_text_field( $result['drawer_error'] ) );
121 Logger::log( sprintf( 'Cloud print: PrintNode drawer kick failed for job %d after receipt submission.', $job_id ) );
122 }
123
124 $this->jobs->set_status( $job_id, Print_Job_Service::STATUS_PRINTED );
125 } finally {
126 $this->jobs->release_lifecycle_lock( $job_id );
127 }
128 }
129
130 /**
131 * Whether a job's status still permits submission.
132 *
133 * Pending is the normal case; claimed covers a retry that a worker had
134 * already picked up. Cancelled, printed and failed jobs are terminal and
135 * must never be pushed to a provider.
136 *
137 * @param array $job Job row.
138 *
139 * @return bool True when the job may be submitted.
140 */
141 private function is_submittable( array $job ): bool {
142 return \in_array(
143 $job['status'] ?? '',
144 array( Print_Job_Service::STATUS_PENDING, Print_Job_Service::STATUS_CLAIMED ),
145 true
146 );
147 }
148
149 /**
150 * Handle a transient PrintNode submit error: retry with linear backoff up to
151 * MAX_ATTEMPTS, then terminally fail.
152 *
153 * The PrintNode client never includes the API key in its error messages, so
154 * recording the message verbatim is safe.
155 *
156 * @param int $job_id Job ID.
157 * @param string $error Failure reason from PrintNode_Client.
158 */
159 private function handle_submit_error( int $job_id, string $error ): void {
160 $attempts = (int) get_post_meta( $job_id, Print_Job_Service::META_SUBMIT_ATTEMPTS, true ) + 1;
161 update_post_meta( $job_id, Print_Job_Service::META_SUBMIT_ATTEMPTS, $attempts );
162 update_post_meta( $job_id, Print_Job_Service::META_ERROR, sanitize_text_field( $error ) );
163
164 if ( $attempts < self::MAX_ATTEMPTS ) {
165 $this->jobs->set_status( $job_id, Print_Job_Service::STATUS_PENDING );
166 wp_schedule_single_event(
167 time() + $attempts * 60,
168 Cloud_Print_Trigger_Service::CRON_SUBMIT,
169 array( $job_id )
170 );
171 Logger::log( sprintf( 'Cloud print: external submission failed for job %d, retry %d scheduled.', $job_id, $attempts ) );
172
173 return;
174 }
175
176 $this->jobs->set_status( $job_id, Print_Job_Service::STATUS_FAILED );
177 Logger::log( sprintf( 'Cloud print: external submission failed for job %d after %d attempts.', $job_id, $attempts ) );
178 }
179
180 /**
181 * Build a human-readable PrintNode job title.
182 *
183 * @param array $job Job array.
184 *
185 * @return string
186 */
187 private function title_for( array $job ): string {
188 if ( ! empty( $job['order_id'] ) ) {
189 $order = wc_get_order( (int) $job['order_id'] );
190 if ( $order ) {
191 return 'WCPOS Order #' . $order->get_order_number();
192 }
193 }
194
195 return 'WCPOS Print Job ' . (int) $job['id'];
196 }
197
198 /**
199 * Mark a job failed, record the error, and log a generic failure.
200 *
201 * The PrintNode client never includes the API key in its error messages, so
202 * recording the message verbatim is safe.
203 *
204 * @param int $job_id Job ID.
205 * @param string $error Failure reason.
206 */
207 private function fail( int $job_id, string $error ): void {
208 $this->jobs->set_status( $job_id, Print_Job_Service::STATUS_FAILED );
209 update_post_meta( $job_id, Print_Job_Service::META_ERROR, sanitize_text_field( $error ) );
210 Logger::log( sprintf( 'Cloud print: external submission failed for job %d.', $job_id ) );
211 }
212 }
213