PluginProbe ʕ •ᴥ•ʔ
Event Tickets with Ticket Scanner / 2.9.4
Event Tickets with Ticket Scanner v2.9.4
3.1.2 3.1.1 3.1.0 3.0.9 3.0.8 3.0.7 3.0.6 3.0.5 3.0.4 trunk 2.6.0 2.7.0 2.7.1 2.7.10 2.7.2 2.7.3 2.7.4 2.7.5 2.7.6 2.7.7 2.7.8 2.7.9 2.8.0 2.8.1 2.8.10 2.8.2 2.8.3 2.8.4 2.8.5 2.8.6 2.8.7 2.8.8 2.8.9 2.9.0 2.9.2 2.9.3 2.9.4 2.9.5 2.9.6 2.9.7 2.9.8 2.9.9 3.0.0 3.0.1 3.0.2 3.0.3
event-tickets-with-ticket-scanner / tests / PartialRefundAndCodeCleanupTest.php
event-tickets-with-ticket-scanner / tests Last commit date
bin 3 months ago fixtures 3 months ago AdminCodeCleanupAndPDFTest.php 3 months ago AdminCodesByProductTest.php 3 months ago AdminCustomerNameTest.php 3 months ago AdminErrorLogTest.php 3 months ago AdminFormatWarningTest.php 3 months ago AdminListAndCodeOpsTest.php 3 months ago AdminListQueryTest.php 3 months ago AdminMetaAndErrorTest.php 3 months ago AdminOptionsAndMigrationTest.php 3 months ago AdminOrderInfoTest.php 3 months ago AdminRedeemAmountTest.php 3 months ago AdminRestrictionAndRstrTest.php 3 months ago AdminSettingsAndDatepickerTest.php 3 months ago AdminSettingsCodeTest.php 3 months ago AdminTicketInfoTest.php 3 months ago AdminTimezoneAndMiscTest.php 3 months ago AdminWCCodeOpsTest.php 3 months ago AuthtokenAccessTest.php 3 months ago AuthtokenCRUDTest.php 3 months ago AuthtokenEditAndRemoveTest.php 3 months ago AuthtokenTest.php 3 months ago BaseLimitsTest.php 3 months ago CartCodeRestrictionAndAssignTest.php 3 months ago CartMetaAndBeforeCartTest.php 3 months ago CartValidationTest.php 3 months ago ContextWizardTest.php 3 months ago CoreAlignAndSearchTest.php 3 months ago CoreCodeOpsAndDBLogTest.php 3 months ago CoreDecodeAndReplaceTest.php 3 months ago CoreEncodeMetaTest.php 3 months ago CoreExpiredAndRegisteredTest.php 3 months ago CoreMetaAndUrlTest.php 3 months ago CoreMetaObjectStructureTest.php 3 months ago CoreMetaTest.php 3 months ago CoreMethodsTest.php 3 months ago CoreOrderAndIPTest.php 3 months ago CoreParserAndQueryTest.php 3 months ago CoreParserURLAndMergePDFTest.php 3 months ago CoreReplaceAndExpireTest.php 3 months ago CoreTicketIdAndExpirationTest.php 3 months ago CoreTicketIdAndUrlTest.php 3 months ago CoreTicketURLComponentsTest.php 3 months ago CountRedeemsTodayTest.php 3 months ago CronjobTest.php 3 months ago DBOperationsTest.php 3 months ago DBSanitizeAndQueryTest.php 3 months ago DBSanitizeTest.php 3 months ago DBTablesAndUpdateTest.php 3 months ago DatabaseTest.php 3 months ago DateCalcAndCorrectProductTest.php 3 months ago DaychooserTest.php 3 months ago EmailHandlerTest.php 3 months ago EmailOrderMetaAndAttachmentsTest.php 3 months ago ErrorLogAndSanitizeTest.php 3 months ago EventTimeWindowTest.php 3 months ago FrontendCheckCodeTest.php 3 months ago FrontendCountConfirmedTest.php 3 months ago FrontendExecuteJSONAndCheckCodeTest.php 3 months ago FrontendIsUsedAndOptionsTest.php 3 months ago FrontendMarkUsedTest.php 3 months ago FrontendValidationTest.php 3 months ago FullTicketFlowTest.php 3 months ago HelpersTest.php 3 months ago ListManagementTest.php 3 months ago MaxRedeemPerDayTest.php 3 months ago MessengerAndRESTEndpointsTest.php 3 months ago MessengerTypeCheckTest.php 3 months ago OldPremiumGuardTest.php 3 months ago OneTimeUseTest.php 3 months ago OptionsChangeAndLoadTest.php 3 months ago OptionsDeleteAndResetTest.php 3 months ago OptionsExportImportTest.php 3 months ago OptionsFormatsTest.php 3 months ago OptionsHistoryTest.php 3 months ago OptionsMigrationTest.php 3 months ago OptionsResetAndFormatTest.php 3 months ago OptionsTest.php 3 months ago OrderDeleteAndRefundDeleteTest.php 3 months ago OrderMetaKeyValueDisplayTest.php 3 months ago OrderStatusHandlerTest.php 3 months ago OrderTicketGenerationTest.php 3 months ago OrderTicketQueryTest.php 3 months ago PDFAndQRConfigTest.php 3 months ago PDFMergeAndPWAServiceWorkerTest.php 3 months ago PDFSettersTest.php 3 months ago PDFSettingsTest.php 3 months ago PDFUtilsTest.php 3 months ago PWAManifestAndSessionTest.php 3 months ago PartialRefundAndCodeCleanupTest.php 3 months ago PluginInitAndShortcodeTest.php 3 months ago ProductSettingsTest.php 3 months ago QRCodeAndDesignerTest.php 3 months ago QRPDFAndBadgePDFTest.php 3 months ago RESTRoutesAndPingTest.php 3 months ago RefundTest.php 3 months ago RestApiTest.php 3 months ago RestApiTicketTest.php 3 months ago ScannerBoilerplateAndFormatWarningsTest.php 3 months ago SeatingBlockTest.php 3 months ago SeatingCoordinatorTest.php 3 months ago SeatingExportTest.php 3 months ago SeatingMetaTest.php 3 months ago SeatingPlanCRUDTest.php 3 months ago SeatingPlanTest.php 3 months ago SeatingSeatCRUDTest.php 3 months ago SetupWizardTest.php 3 months ago ShortcodeAndCartCheckTest.php 3 months ago ShortcodeAndCodesDisplayTest.php 3 months ago ShortcodesAndCodeListTest.php 3 months ago StaticDateAndTimeTest.php 3 months ago StaticHelpersAndMessengerTest.php 3 months ago StaticUtilMethodsTest.php 3 months ago TicketBadgeTemplateTest.php 3 months ago TicketCalcDateTest.php 3 months ago TicketDesignerAndBadgeTest.php 3 months ago TicketDesignerTemplateTest.php 3 months ago TicketDesignerTest.php 3 months ago TicketDisplayTest.php 3 months ago TicketExpirationAndICSTest.php 3 months ago TicketLabelAndDisplayTest.php 3 months ago TicketLabelsAndStaticsTest.php 3 months ago TicketQRSettersTest.php 3 months ago TicketRedeemTest.php 3 months ago TicketRoutingTest.php 3 months ago TicketScannerMethodsTest.php 3 months ago TicketSettersAndOrderTest.php 3 months ago TicketUtilAndICSTest.php 3 months ago TicketUtilMethodsTest.php 3 months ago TicketWPMLAndSubscriptionTest.php 3 months ago TimezoneRedeemBlockingTest.php 3 months ago UrlParsingTest.php 3 months ago VariationTicketTest.php 3 months ago WCBaseMetaConstantsTest.php 3 months ago WCCheckoutCartFlowTest.php 3 months ago WCEmailAttachmentTest.php 3 months ago WCFrontendAjaxAndDatepickerTest.php 3 months ago WCFrontendCartHandlerTest.php 3 months ago WCFrontendCartHandlersTest.php 3 months ago WCFrontendCartTest.php 3 months ago WCFrontendCartValidationTest.php 3 months ago WCFrontendHandlersTest.php 3 months ago WCFrontendLabelsTest.php 3 months ago WCFrontendMethodsTest.php 3 months ago WCOrderDeleteTest.php 3 months ago WCOrderDisplayMetaTest.php 3 months ago WCOrderHasTicketsTest.php 3 months ago WCOrderHooksTest.php 3 months ago WCOrderItemMetaDisplayTest.php 3 months ago WCOrderItemOpsTest.php 3 months ago WCOrderLifecycleTest.php 3 months ago WCOrderMethodsTest.php 3 months ago WCOrderTicketDetectionTest.php 3 months ago WCOrderTicketNumberTest.php 3 months ago WCProductAndVariationTest.php 3 months ago WCProductColumnsTest.php 3 months ago WCProductConfigAndColumnsTest.php 3 months ago WCProductIsTicketAndListsTest.php 3 months ago WCProductMetaSaveTest.php 3 months ago WCProductMethodsTest.php 3 months ago WCProductTabsAndSideBoxTest.php 3 months ago WCProductTicketTest.php 3 months ago WebhooksAndMigrationTest.php 3 months ago bootstrap.php 3 months ago composer.json 3 months ago phpunit.xml 3 months ago test-premium-compat.sh 3 months ago
PartialRefundAndCodeCleanupTest.php
610 lines
1 <?php
2 /**
3 * Tests for partial refund and code cleanup operations:
4 * - woocommerce_order_partially_refunded: Removes excess codes on partial refund
5 * - woocommerce_delete_order_item: Frees codes/restrictions when item deleted
6 * - removeAllTicketsFromOrder: Bulk remove all tickets
7 * - removeUsedInformationFromCode: Clear redemption data from code
8 * - removeWoocommerceOrderInfoFromCode: Detach code from order
9 * - removeRedeemWoocommerceTicketForCode: Un-redeem without detaching
10 * - deleteCodesEntryOnOrderItem: Delete ticket metadata from order item
11 */
12
13 class PartialRefundAndCodeCleanupTest extends WP_UnitTestCase {
14
15 private $main;
16
17 public function set_up(): void {
18 parent::set_up();
19 $this->main = sasoEventtickets::Instance();
20
21 if (!class_exists('WC_Product_Simple')) {
22 $this->markTestSkipped('WooCommerce not available');
23 }
24 }
25
26 // ── Helpers ─────────────────────────────────────────────────
27
28 private function createTicketProduct(array $extraMeta = []): array {
29 $listId = $this->main->getDB()->insert('lists', [
30 'name' => 'Refund List ' . uniqid(),
31 'aktiv' => 1,
32 'meta' => '{}',
33 ]);
34
35 $product = new WC_Product_Simple();
36 $product->set_name('Refund Ticket ' . uniqid());
37 $product->set_regular_price('25.00');
38 $product->set_status('publish');
39 $product->save();
40 $pid = $product->get_id();
41
42 update_post_meta($pid, 'saso_eventtickets_is_ticket', 'yes');
43 update_post_meta($pid, 'saso_eventtickets_list', $listId);
44
45 foreach ($extraMeta as $key => $value) {
46 update_post_meta($pid, $key, $value);
47 }
48
49 return ['product' => $product, 'product_id' => $pid, 'list_id' => $listId];
50 }
51
52 private function createCompletedOrderWithCodes(WC_Product $product, int $quantity = 3): WC_Order {
53 $order = wc_create_order();
54 $order->add_product($product, $quantity);
55 $order->set_billing_first_name('Refund');
56 $order->set_billing_last_name('Test');
57 $order->set_billing_email('refund@test.com');
58 $order->calculate_totals();
59 $order->set_status('completed');
60 $order->save();
61 return wc_get_order($order->get_id());
62 }
63
64 private function getOrderItemAndId(WC_Order $order): array {
65 $items = $order->get_items();
66 $item = reset($items);
67 $item_id = key($items);
68 return ['item' => $item, 'item_id' => $item_id];
69 }
70
71 private function getCodeCount(int $order_id): int {
72 return count($this->main->getCore()->getCodesByOrderId($order_id));
73 }
74
75 private function enableOption(string $key): void {
76 update_option('sasoEventtickets' . $key, '1');
77 $this->resetOptionsCache();
78 }
79
80 private function disableOption(string $key): void {
81 update_option('sasoEventtickets' . $key, '0');
82 $this->resetOptionsCache();
83 }
84
85 private function resetOptionsCache(): void {
86 // Reset _isLoaded flags so options are re-read from DB
87 $ref = new ReflectionProperty($this->main->getOptions(), '_options');
88 $ref->setAccessible(true);
89 $options = $ref->getValue($this->main->getOptions());
90 foreach ($options as $idx => $option) {
91 $options[$idx]['_isLoaded'] = false;
92 }
93 $ref->setValue($this->main->getOptions(), $options);
94 }
95
96 private function createRefundForOrder(WC_Order $order, int $item_id, int $qty): WC_Order_Refund {
97 $refund = wc_create_refund([
98 'order_id' => $order->get_id(),
99 'amount' => 0,
100 'reason' => 'Test refund',
101 'line_items' => [
102 $item_id => [
103 'qty' => $qty,
104 'refund_total' => 0,
105 ],
106 ],
107 ]);
108 return $refund;
109 }
110
111 // ── removeUsedInformationFromCode ───────────────────────────
112
113 public function test_removeUsedInformationFromCode_clears_registration_data(): void {
114 $tp = $this->createTicketProduct();
115 $order = $this->createCompletedOrderWithCodes($tp['product'], 1);
116
117 $codes = $this->main->getCore()->getCodesByOrderId($order->get_id());
118 $this->assertNotEmpty($codes);
119 $code = $codes[0]['code'];
120
121 // Simulate used info
122 $codeObj = $this->main->getCore()->retrieveCodeByCode($code);
123 $metaObj = $this->main->getCore()->encodeMetaValuesAndFillObject($codeObj['meta'], $codeObj);
124 $metaObj['used']['reg_ip'] = '192.168.1.1';
125 $metaObj['used']['reg_request'] = '2026-01-01 12:00:00';
126 $metaObj['confirmedCount'] = 5;
127 $codeObj['meta'] = $this->main->getCore()->json_encode_with_error_handling($metaObj);
128 $this->main->getDB()->update('codes', ['meta' => $codeObj['meta']], ['id' => $codeObj['id']]);
129
130 // Remove used info
131 $result = $this->main->getAdmin()->removeUsedInformationFromCode(['code' => $code]);
132
133 $metaAfter = $this->main->getCore()->encodeMetaValuesAndFillObject($result['meta'], $result);
134 $this->assertEmpty($metaAfter['used']['reg_ip']);
135 $this->assertEmpty($metaAfter['used']['reg_request']);
136 $this->assertEquals(0, $metaAfter['confirmedCount']);
137 }
138
139 public function test_removeUsedInformationFromCode_throws_without_code_param(): void {
140 $this->expectException(Exception::class);
141 $this->expectExceptionMessage('#9231');
142 $this->main->getAdmin()->removeUsedInformationFromCode([]);
143 }
144
145 // ── removeWoocommerceOrderInfoFromCode ───────────────────────
146
147 public function test_removeWoocommerceOrderInfoFromCode_detaches_code_from_order(): void {
148 $tp = $this->createTicketProduct();
149 $order = $this->createCompletedOrderWithCodes($tp['product'], 1);
150
151 $codes = $this->main->getCore()->getCodesByOrderId($order->get_id());
152 $this->assertNotEmpty($codes);
153 $code = $codes[0]['code'];
154
155 // Verify code is linked to order
156 $codeObj = $this->main->getCore()->retrieveCodeByCode($code);
157 $this->assertEquals($order->get_id(), intval($codeObj['order_id']));
158
159 // Remove order info
160 $this->main->getAdmin()->removeWoocommerceOrderInfoFromCode(['code' => $code]);
161
162 // Verify code is detached
163 $codeAfter = $this->main->getCore()->retrieveCodeByCode($code);
164 $this->assertEquals(0, intval($codeAfter['order_id']));
165
166 $metaAfter = $this->main->getCore()->encodeMetaValuesAndFillObject($codeAfter['meta'], $codeAfter);
167 $this->assertEquals(0, intval($metaAfter['woocommerce']['order_id']));
168 }
169
170 public function test_removeWoocommerceOrderInfoFromCode_throws_without_code_param(): void {
171 $this->expectException(Exception::class);
172 $this->expectExceptionMessage('#9611');
173 $this->main->getAdmin()->removeWoocommerceOrderInfoFromCode([]);
174 }
175
176 public function test_removeWoocommerceOrderInfoFromCode_updates_order_item_meta(): void {
177 $tp = $this->createTicketProduct();
178 $order = $this->createCompletedOrderWithCodes($tp['product'], 2);
179
180 $data = $this->getOrderItemAndId($order);
181 $codesStr = wc_get_order_item_meta($data['item_id'], '_saso_eventtickets_product_code', true);
182 $this->assertNotEmpty($codesStr);
183
184 $codeParts = explode(',', $codesStr);
185 $this->assertCount(2, $codeParts);
186
187 // Remove first code's order info
188 $this->main->getAdmin()->removeWoocommerceOrderInfoFromCode(['code' => trim($codeParts[0])]);
189
190 // After removal, check order item — depending on remaining codes, it may be reduced or cleared
191 $codesStrAfter = wc_get_order_item_meta($data['item_id'], '_saso_eventtickets_product_code', true);
192 // The remaining code(s) should still be in the meta, or meta should be cleared if none remain
193 // With 2 codes, removing 1 leaves 1
194 if (!empty($codesStrAfter)) {
195 $remaining = explode(',', $codesStrAfter);
196 // Count should be less than or equal to original
197 $this->assertLessThanOrEqual(2, count($remaining));
198 }
199 }
200
201 // ── removeRedeemWoocommerceTicketForCode ─────────────────────
202
203 public function test_removeRedeemWoocommerceTicketForCode_clears_redeem_data(): void {
204 $tp = $this->createTicketProduct();
205 $order = $this->createCompletedOrderWithCodes($tp['product'], 1);
206
207 $codes = $this->main->getCore()->getCodesByOrderId($order->get_id());
208 $code = $codes[0]['code'];
209
210 // Simulate redemption
211 $codeObj = $this->main->getCore()->retrieveCodeByCode($code);
212 $metaObj = $this->main->getCore()->encodeMetaValuesAndFillObject($codeObj['meta'], $codeObj);
213 $metaObj['wc_ticket']['redeemed_date'] = '2026-01-15 14:00:00';
214 $metaObj['wc_ticket']['redeemed_date_tz'] = 'Europe/Berlin';
215 $metaObj['wc_ticket']['ip'] = '10.0.0.1';
216 $metaObj['used']['reg_ip'] = '10.0.0.1';
217 $codeObj['meta'] = $this->main->getCore()->json_encode_with_error_handling($metaObj);
218 $this->main->getDB()->update('codes', ['meta' => $codeObj['meta'], 'redeemed' => 1], ['id' => $codeObj['id']]);
219
220 // Un-redeem
221 $result = $this->main->getAdmin()->removeRedeemWoocommerceTicketForCode(['code' => $code]);
222
223 $metaAfter = $this->main->getCore()->encodeMetaValuesAndFillObject($result['meta'], $result);
224 $this->assertEmpty($metaAfter['wc_ticket']['redeemed_date']);
225 $this->assertEmpty($metaAfter['wc_ticket']['ip']);
226 $this->assertEmpty($metaAfter['used']['reg_ip']);
227
228 // Check redeemed flag in DB
229 $codeAfter = $this->main->getCore()->retrieveCodeByCode($code);
230 $this->assertEquals(0, intval($codeAfter['redeemed']));
231 }
232
233 public function test_removeRedeemWoocommerceTicketForCode_throws_without_code(): void {
234 $this->expectException(Exception::class);
235 $this->expectExceptionMessage('#9621');
236 $this->main->getAdmin()->removeRedeemWoocommerceTicketForCode([]);
237 }
238
239 public function test_removeRedeemWoocommerceTicketForCode_keeps_order_link(): void {
240 $tp = $this->createTicketProduct();
241 $order = $this->createCompletedOrderWithCodes($tp['product'], 1);
242
243 $codes = $this->main->getCore()->getCodesByOrderId($order->get_id());
244 $code = $codes[0]['code'];
245
246 // Un-redeem
247 $this->main->getAdmin()->removeRedeemWoocommerceTicketForCode(['code' => $code]);
248
249 // Code should still be linked to order
250 $codeAfter = $this->main->getCore()->retrieveCodeByCode($code);
251 $this->assertEquals($order->get_id(), intval($codeAfter['order_id']));
252 }
253
254 // ── deleteCodesEntryOnOrderItem ─────────────────────────────
255
256 public function test_deleteCodesEntryOnOrderItem_clears_all_ticket_meta(): void {
257 $tp = $this->createTicketProduct();
258 $order = $this->createCompletedOrderWithCodes($tp['product'], 1);
259
260 $data = $this->getOrderItemAndId($order);
261
262 // Verify meta exists
263 $isTicket = wc_get_order_item_meta($data['item_id'], '_saso_eventtickets_is_ticket', true);
264 $this->assertNotEmpty($isTicket);
265
266 $codesStr = wc_get_order_item_meta($data['item_id'], '_saso_eventtickets_product_code', true);
267 $this->assertNotEmpty($codesStr);
268
269 // Delete
270 $this->main->getWC()->getOrderManager()->deleteCodesEntryOnOrderItem($data['item_id']);
271
272 // All meta should be gone
273 $this->assertEmpty(wc_get_order_item_meta($data['item_id'], '_saso_eventtickets_is_ticket', true));
274 $this->assertEmpty(wc_get_order_item_meta($data['item_id'], '_saso_eventtickets_product_code', true));
275 $this->assertEmpty(wc_get_order_item_meta($data['item_id'], '_saso_eventtickets_public_ticket_ids', true));
276 }
277
278 // ── woocommerce_order_partially_refunded ────────────────────
279
280 public function test_partial_refund_does_nothing_when_option_disabled(): void {
281 $this->disableOption('wcassignmentOrderItemRefund');
282
283 $tp = $this->createTicketProduct();
284 $order = $this->createCompletedOrderWithCodes($tp['product'], 3);
285 $data = $this->getOrderItemAndId($order);
286
287 $codesBefore = $this->getCodeCount($order->get_id());
288 $this->assertEquals(3, $codesBefore);
289
290 // Create a refund
291 $refund = $this->createRefundForOrder($order, $data['item_id'], 1);
292
293 // Call handler
294 $this->main->getWC()->getOrderManager()->woocommerce_order_partially_refunded(
295 $order->get_id(), $refund->get_id()
296 );
297
298 // Codes in order item meta should be unchanged (option is off)
299 $codesStr = wc_get_order_item_meta($data['item_id'], '_saso_eventtickets_product_code', true);
300 $codeParts = explode(',', $codesStr);
301 $this->assertCount(3, $codeParts);
302 }
303
304 public function test_partial_refund_removes_excess_codes_when_option_enabled(): void {
305 $this->enableOption('wcassignmentOrderItemRefund');
306
307 $tp = $this->createTicketProduct();
308 $order = $this->createCompletedOrderWithCodes($tp['product'], 3);
309 $data = $this->getOrderItemAndId($order);
310
311 $codesStr = wc_get_order_item_meta($data['item_id'], '_saso_eventtickets_product_code', true);
312 $this->assertNotEmpty($codesStr);
313 $codeParts = explode(',', $codesStr);
314 $this->assertCount(3, $codeParts);
315
316 // Refund 1 of 3 items
317 $refund = $this->createRefundForOrder($order, $data['item_id'], 1);
318
319 // Reload order to pick up refund data
320 $order = wc_get_order($order->get_id());
321
322 $this->main->getWC()->getOrderManager()->woocommerce_order_partially_refunded(
323 $order->get_id(), $refund->get_id()
324 );
325
326 // Should now have 2 codes in order item meta
327 $codesStrAfter = wc_get_order_item_meta($data['item_id'], '_saso_eventtickets_product_code', true);
328 $codePartsAfter = explode(',', $codesStrAfter);
329 $this->assertCount(2, $codePartsAfter);
330
331 $this->disableOption('wcassignmentOrderItemRefund');
332 }
333
334 public function test_partial_refund_removes_all_codes_when_fully_refunded(): void {
335 $this->enableOption('wcassignmentOrderItemRefund');
336
337 $tp = $this->createTicketProduct();
338 $order = $this->createCompletedOrderWithCodes($tp['product'], 2);
339 $data = $this->getOrderItemAndId($order);
340
341 // Refund all 2 items
342 $refund = $this->createRefundForOrder($order, $data['item_id'], 2);
343 $order = wc_get_order($order->get_id());
344
345 $this->main->getWC()->getOrderManager()->woocommerce_order_partially_refunded(
346 $order->get_id(), $refund->get_id()
347 );
348
349 // Should have 0 codes
350 $codesStrAfter = wc_get_order_item_meta($data['item_id'], '_saso_eventtickets_product_code', true);
351 $codePartsAfter = !empty($codesStrAfter) ? explode(',', $codesStrAfter) : [];
352 $this->assertCount(0, $codePartsAfter);
353
354 $this->disableOption('wcassignmentOrderItemRefund');
355 }
356
357 public function test_partial_refund_respects_tickets_per_item(): void {
358 $this->enableOption('wcassignmentOrderItemRefund');
359
360 $tp = $this->createTicketProduct([
361 'saso_eventtickets_ticket_amount_per_item' => 2,
362 ]);
363 $order = $this->createCompletedOrderWithCodes($tp['product'], 2);
364 $data = $this->getOrderItemAndId($order);
365
366 // With tickets_per_item=2 and quantity=2, we should have 4 codes
367 $codesStr = wc_get_order_item_meta($data['item_id'], '_saso_eventtickets_product_code', true);
368 $codeParts = explode(',', $codesStr);
369 $this->assertCount(4, $codeParts);
370
371 // Refund 1 item → remaining = 1 item × 2 tickets = 2 codes
372 $refund = $this->createRefundForOrder($order, $data['item_id'], 1);
373 $order = wc_get_order($order->get_id());
374
375 $this->main->getWC()->getOrderManager()->woocommerce_order_partially_refunded(
376 $order->get_id(), $refund->get_id()
377 );
378
379 $codesStrAfter = wc_get_order_item_meta($data['item_id'], '_saso_eventtickets_product_code', true);
380 $codePartsAfter = explode(',', $codesStrAfter);
381 $this->assertCount(2, $codePartsAfter);
382
383 $this->disableOption('wcassignmentOrderItemRefund');
384 }
385
386 public function test_partial_refund_skips_non_ticket_products(): void {
387 $this->enableOption('wcassignmentOrderItemRefund');
388
389 // Create a non-ticket product
390 $product = new WC_Product_Simple();
391 $product->set_name('Regular Product');
392 $product->set_regular_price('15.00');
393 $product->set_status('publish');
394 $product->save();
395 // Don't set is_ticket meta
396
397 $order = wc_create_order();
398 $order->add_product($product, 2);
399 $order->calculate_totals();
400 $order->set_status('completed');
401 $order->save();
402 $order = wc_get_order($order->get_id());
403
404 $data = $this->getOrderItemAndId($order);
405
406 $refund = $this->createRefundForOrder($order, $data['item_id'], 1);
407 $order = wc_get_order($order->get_id());
408
409 // Should not crash when processing non-ticket items
410 $this->main->getWC()->getOrderManager()->woocommerce_order_partially_refunded(
411 $order->get_id(), $refund->get_id()
412 );
413
414 $this->assertTrue(true); // No exception = pass
415
416 $this->disableOption('wcassignmentOrderItemRefund');
417 }
418
419 public function test_partial_refund_adds_order_note(): void {
420 $this->enableOption('wcassignmentOrderItemRefund');
421
422 $tp = $this->createTicketProduct();
423 $order = $this->createCompletedOrderWithCodes($tp['product'], 2);
424 $data = $this->getOrderItemAndId($order);
425
426 $refund = $this->createRefundForOrder($order, $data['item_id'], 1);
427 $order = wc_get_order($order->get_id());
428
429 // Count notes before
430 $notesBefore = wc_get_order_notes(['order_id' => $order->get_id()]);
431 $countBefore = count($notesBefore);
432
433 $this->main->getWC()->getOrderManager()->woocommerce_order_partially_refunded(
434 $order->get_id(), $refund->get_id()
435 );
436
437 // Should have at least one new note about the refunded ticket
438 $notesAfter = wc_get_order_notes(['order_id' => $order->get_id()]);
439 $this->assertGreaterThan($countBefore, count($notesAfter));
440
441 $this->disableOption('wcassignmentOrderItemRefund');
442 }
443
444 // ── woocommerce_delete_order_item ───────────────────────────
445
446 public function test_delete_order_item_frees_codes_when_option_enabled(): void {
447 $this->enableOption('wcRestrictFreeCodeByOrderRefund');
448
449 $tp = $this->createTicketProduct();
450 $order = $this->createCompletedOrderWithCodes($tp['product'], 1);
451 $data = $this->getOrderItemAndId($order);
452
453 $codesStr = wc_get_order_item_meta($data['item_id'], '_saso_eventtickets_product_code', true);
454 $this->assertNotEmpty($codesStr);
455
456 $code = trim($codesStr);
457
458 // Verify code is linked
459 $codeObj = $this->main->getCore()->retrieveCodeByCode($code);
460 $this->assertEquals($order->get_id(), intval($codeObj['order_id']));
461
462 // Delete order item
463 $this->main->getWC()->getOrderManager()->woocommerce_delete_order_item($data['item_id']);
464
465 // Code should be freed
466 $codeAfter = $this->main->getCore()->retrieveCodeByCode($code);
467 $this->assertEquals(0, intval($codeAfter['order_id']));
468
469 $this->disableOption('wcRestrictFreeCodeByOrderRefund');
470 }
471
472 public function test_delete_order_item_does_not_free_codes_when_option_disabled(): void {
473 $this->disableOption('wcRestrictFreeCodeByOrderRefund');
474
475 $tp = $this->createTicketProduct();
476 $order = $this->createCompletedOrderWithCodes($tp['product'], 1);
477 $data = $this->getOrderItemAndId($order);
478
479 $codesStr = wc_get_order_item_meta($data['item_id'], '_saso_eventtickets_product_code', true);
480 $this->assertNotEmpty($codesStr);
481 $code = trim($codesStr);
482
483 // Delete order item without the option
484 $this->main->getWC()->getOrderManager()->woocommerce_delete_order_item($data['item_id']);
485
486 // Code should still be linked (option was off)
487 $codeAfter = $this->main->getCore()->retrieveCodeByCode($code);
488 $this->assertEquals($order->get_id(), intval($codeAfter['order_id']));
489 }
490
491 // ── removeAllTicketsFromOrder ────────────────────────────────
492
493 public function test_removeAllTicketsFromOrder_clears_all_items(): void {
494 $this->enableOption('wcRestrictFreeCodeByOrderRefund');
495
496 $tp = $this->createTicketProduct();
497 $order = $this->createCompletedOrderWithCodes($tp['product'], 3);
498 $data = $this->getOrderItemAndId($order);
499
500 // Verify codes exist
501 $codesStr = wc_get_order_item_meta($data['item_id'], '_saso_eventtickets_product_code', true);
502 $this->assertNotEmpty($codesStr);
503
504 // Remove all tickets
505 $result = $this->main->getWC()->getOrderManager()->removeAllTicketsFromOrder([
506 'order_id' => $order->get_id(),
507 ]);
508
509 $this->assertTrue($result);
510
511 // Order item meta should be cleared
512 $codesStrAfter = wc_get_order_item_meta($data['item_id'], '_saso_eventtickets_product_code', true);
513 $this->assertEmpty($codesStrAfter);
514
515 $isTicket = wc_get_order_item_meta($data['item_id'], '_saso_eventtickets_is_ticket', true);
516 $this->assertEmpty($isTicket);
517
518 $this->disableOption('wcRestrictFreeCodeByOrderRefund');
519 }
520
521 public function test_removeAllTicketsFromOrder_with_invalid_order_id(): void {
522 $result = $this->main->getWC()->getOrderManager()->removeAllTicketsFromOrder([
523 'order_id' => 0,
524 ]);
525 $this->assertTrue($result); // Returns true even with invalid ID
526 }
527
528 public function test_removeAllTicketsFromOrder_multiple_items(): void {
529 $this->enableOption('wcRestrictFreeCodeByOrderRefund');
530
531 $tp1 = $this->createTicketProduct();
532 $tp2 = $this->createTicketProduct();
533
534 $order = wc_create_order();
535 $order->add_product($tp1['product'], 2);
536 $order->add_product($tp2['product'], 1);
537 $order->calculate_totals();
538 $order->set_status('completed');
539 $order->save();
540 $order = wc_get_order($order->get_id());
541
542 // Should have 3 codes total
543 $codes = $this->main->getCore()->getCodesByOrderId($order->get_id());
544 $this->assertCount(3, $codes);
545
546 // Remove all
547 $this->main->getWC()->getOrderManager()->removeAllTicketsFromOrder([
548 'order_id' => $order->get_id(),
549 ]);
550
551 // All items should be cleared
552 $items = $order->get_items();
553 foreach ($items as $item_id => $item) {
554 $codesStr = wc_get_order_item_meta($item_id, '_saso_eventtickets_product_code', true);
555 $this->assertEmpty($codesStr, "Codes should be cleared for item $item_id");
556 }
557
558 $this->disableOption('wcRestrictFreeCodeByOrderRefund');
559 }
560
561 // ── Integration: refund + verify code is free ───────────────
562
563 public function test_partial_refund_frees_removed_codes_in_db(): void {
564 $this->enableOption('wcassignmentOrderItemRefund');
565
566 $tp = $this->createTicketProduct();
567 $order = $this->createCompletedOrderWithCodes($tp['product'], 3);
568 $data = $this->getOrderItemAndId($order);
569
570 // Get original codes
571 $codesStr = wc_get_order_item_meta($data['item_id'], '_saso_eventtickets_product_code', true);
572 $originalCodes = explode(',', $codesStr);
573 $this->assertCount(3, $originalCodes);
574
575 // Refund 2 items
576 $refund = $this->createRefundForOrder($order, $data['item_id'], 2);
577 $order = wc_get_order($order->get_id());
578
579 $this->main->getWC()->getOrderManager()->woocommerce_order_partially_refunded(
580 $order->get_id(), $refund->get_id()
581 );
582
583 // Remaining should be 1
584 $codesStrAfter = wc_get_order_item_meta($data['item_id'], '_saso_eventtickets_product_code', true);
585 $remainingCodes = explode(',', $codesStrAfter);
586 $this->assertCount(1, $remainingCodes);
587
588 // The removed codes (last 2) should be detached from the order
589 $removedCodes = array_slice($originalCodes, 1);
590 foreach ($removedCodes as $removedCode) {
591 $removedCode = trim($removedCode);
592 if (!empty($removedCode)) {
593 $codeObj = $this->main->getCore()->retrieveCodeByCode($removedCode);
594 $this->assertEquals(0, intval($codeObj['order_id']),
595 "Removed code $removedCode should have order_id=0"
596 );
597 }
598 }
599
600 // The remaining code should still be linked
601 $remainingCode = trim($remainingCodes[0]);
602 $codeObj = $this->main->getCore()->retrieveCodeByCode($remainingCode);
603 $this->assertEquals($order->get_id(), intval($codeObj['order_id']),
604 "Remaining code should still be linked to order"
605 );
606
607 $this->disableOption('wcassignmentOrderItemRefund');
608 }
609 }
610