PluginProbe
WCPOS – Point of Sale (POS) plugin for WooCommerce / 1.10.17
WCPOS – Point of Sale (POS) plugin for WooCommerce v1.10.17
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 / Receipt_Snapshot_Store.php

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

271 lines 6.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Immutable receipt snapshot store.
4 *
5 * @package WCPOS\WooCommercePOS\Services
6 */
7
8 namespace WCPOS\WooCommercePOS\Services;
9
10 use RuntimeException;
11 use WCPOS\WooCommercePOS\Logger;
12
13 /**
14 * Receipt_Snapshot_Store class.
15 */
16 class Receipt_Snapshot_Store {
17 /**
18 * Snapshot payload meta key.
19 */
20 const META_KEY_PAYLOAD = '_wcpos_receipt_snapshot';
21
22 /**
23 * Snapshot checksum meta key.
24 */
25 const META_KEY_CHECKSUM = '_wcpos_receipt_snapshot_checksum';
26
27 /**
28 * Snapshot sequence meta key.
29 */
30 const META_KEY_SEQUENCE = '_wcpos_receipt_sequence';
31
32 /**
33 * Snapshot created timestamp meta key.
34 */
35 const META_KEY_CREATED_AT = '_wcpos_receipt_snapshot_created_at_gmt';
36
37 /**
38 * Snapshot option key for sequence counter.
39 */
40 const OPTION_SEQUENCE = 'wcpos_receipt_sequence_counter';
41
42 /**
43 * Singleton instance.
44 *
45 * @var null|self
46 */
47 private static $instance = null;
48
49 /**
50 * Data builder.
51 *
52 * @var Receipt_Data_Builder
53 */
54 private $builder;
55
56 /**
57 * Fiscal service.
58 *
59 * @var Fiscal_Receipt_Service
60 */
61 private $fiscal_service;
62
63 /**
64 * Constructor.
65 */
66 private function __construct() {
67 $this->builder = new Receipt_Data_Builder();
68 $this->fiscal_service = new Fiscal_Receipt_Service();
69 add_action( 'woocommerce_payment_complete', array( $this, 'handle_payment_complete' ) );
70 }
71
72 /**
73 * Get singleton instance.
74 *
75 * @return self
76 */
77 public static function instance(): self {
78 if ( null === self::$instance ) {
79 self::$instance = new self();
80 }
81
82 return self::$instance;
83 }
84
85 /**
86 * Handle payment complete event and persist snapshot if needed.
87 *
88 * @param int $order_id Order ID.
89 */
90 public function handle_payment_complete( int $order_id ): void {
91 $order = wc_get_order( $order_id );
92 if ( ! $order ) {
93 return;
94 }
95
96 if ( $this->has_snapshot( $order_id ) ) {
97 return;
98 }
99
100 $snapshot = $this->builder->build( $order, 'fiscal' );
101 try {
102 $this->persist_snapshot( $order_id, $snapshot );
103 } catch ( RuntimeException $e ) {
104 Logger::log( 'Unable to persist receipt snapshot: ' . $e->getMessage() );
105 }
106 }
107
108 /**
109 * Check if order has a stored snapshot.
110 *
111 * @param int $order_id Order ID.
112 *
113 * @return bool
114 */
115 public function has_snapshot( int $order_id ): bool {
116 $order = wc_get_order( $order_id );
117 if ( ! $order ) {
118 return false;
119 }
120 $payload = $order->get_meta( self::META_KEY_PAYLOAD, true );
121
122 return \is_string( $payload ) && '' !== $payload;
123 }
124
125 /**
126 * Get stored snapshot.
127 *
128 * @param int $order_id Order ID.
129 *
130 * @return null|array
131 */
132 public function get_snapshot( int $order_id ): ?array {
133 $order = wc_get_order( $order_id );
134 if ( ! $order ) {
135 return null;
136 }
137 $payload = $order->get_meta( self::META_KEY_PAYLOAD, true );
138 if ( ! \is_string( $payload ) || '' === $payload ) {
139 return null;
140 }
141
142 $decoded = json_decode( $payload, true );
143 if ( ! \is_array( $decoded ) ) {
144 return null;
145 }
146
147 return $decoded;
148 }
149
150 /**
151 * Persist immutable snapshot fields.
152 *
153 * @param int $order_id Order ID.
154 * @param array $snapshot Snapshot payload.
155 *
156 * @throws RuntimeException When the order snapshot lock cannot be acquired.
157 * @throws RuntimeException When JSON encoding fails.
158 * @throws RuntimeException When the order does not exist.
159 */
160 public function persist_snapshot( int $order_id, array $snapshot ): void {
161 global $wpdb;
162
163 $lock_name = 'wcpos_receipt_snapshot_lock_' . $order_id;
164 $acquired = (int) $wpdb->get_var(
165 $wpdb->prepare( 'SELECT GET_LOCK( %s, %d )', $lock_name, 5 )
166 );
167 if ( 1 !== $acquired ) {
168 throw new RuntimeException( 'Unable to acquire receipt snapshot lock' );
169 }
170
171 try {
172 if ( $this->has_snapshot( $order_id ) ) {
173 return;
174 }
175
176 $sequence = $this->next_sequence();
177 $created = current_time( 'mysql', true );
178
179 $snapshot['fiscal']['sequence'] = $sequence;
180 $snapshot['fiscal']['receipt_number'] = (string) $sequence;
181 $snapshot['fiscal']['immutable_id'] = $order_id . ':' . $sequence;
182
183 $snapshot = $this->fiscal_service->enrich_snapshot( $snapshot, $order_id );
184
185 $json = wp_json_encode( $snapshot );
186 if ( ! \is_string( $json ) ) {
187 Logger::log( 'Failed to encode receipt snapshot to JSON: ' . json_last_error_msg() );
188 throw new RuntimeException( 'Failed to encode receipt snapshot to JSON' );
189 }
190
191 $checksum = hash( 'sha256', $json );
192 $order = wc_get_order( $order_id );
193 if ( ! $order ) {
194 throw new RuntimeException( 'Order not found when persisting snapshot' );
195 }
196 $order->update_meta_data( self::META_KEY_PAYLOAD, $json );
197 $order->update_meta_data( self::META_KEY_CHECKSUM, $checksum );
198 $order->update_meta_data( self::META_KEY_SEQUENCE, (string) $sequence );
199 $order->update_meta_data( self::META_KEY_CREATED_AT, $created );
200 $order->save();
201
202 /* translators: %d: receipt sequence number. */
203 $order->add_order_note( \sprintf( __( 'POS fiscal receipt snapshot created (receipt #%d).', 'woocommerce-pos' ), $sequence ) );
204
205 $this->fiscal_service->set_submission_status( $order_id, 'pending' );
206 } finally {
207 $wpdb->get_var(
208 $wpdb->prepare( 'SELECT RELEASE_LOCK( %s )', $lock_name )
209 );
210 }
211 }
212
213 /**
214 * Get the default mode from settings.
215 *
216 * @return string
217 */
218 public function get_default_mode(): string {
219 $checkout_settings = woocommerce_pos_get_settings( 'checkout' );
220 $mode = \is_array( $checkout_settings ) ? ( $checkout_settings['receipt_default_mode'] ?? 'fiscal' ) : 'fiscal';
221
222 return in_array( $mode, array( 'fiscal', 'live' ), true ) ? $mode : 'fiscal';
223 }
224
225 /**
226 * Resolve effective receipt mode.
227 *
228 * @param null|string $mode Requested mode.
229 *
230 * @return string
231 */
232 public function resolve_mode( ?string $mode = null ): string {
233 if ( in_array( $mode, array( 'fiscal', 'live' ), true ) ) {
234 return $mode;
235 }
236
237 return $this->get_default_mode();
238 }
239
240 /**
241 * Get next sequence number.
242 *
243 * @return int
244 *
245 * @throws RuntimeException When the sequence lock cannot be acquired.
246 */
247 private function next_sequence(): int {
248 global $wpdb;
249
250 $lock_name = 'wcpos_receipt_sequence_lock';
251 $acquired = (int) $wpdb->get_var(
252 $wpdb->prepare( 'SELECT GET_LOCK( %s, %d )', $lock_name, 5 )
253 );
254 if ( 1 !== $acquired ) {
255 throw new RuntimeException( 'Unable to acquire receipt sequence lock' );
256 }
257
258 try {
259 $sequence = (int) get_option( self::OPTION_SEQUENCE, 0 );
260 $sequence++;
261 update_option( self::OPTION_SEQUENCE, $sequence, false );
262 } finally {
263 $wpdb->get_var(
264 $wpdb->prepare( 'SELECT RELEASE_LOCK( %s )', $lock_name )
265 );
266 }
267
268 return $sequence;
269 }
270 }
271