File Coverage

blib/lib/Lib/Pepper/Simple.pm
Criterion Covered Total %
statement 64 300 21.3
branch 9 128 7.0
condition 1 95 1.0
subroutine 17 26 65.3
pod 6 6 100.0
total 97 555 17.4


line stmt bran cond sub pod time code
1             package Lib::Pepper::Simple;
2             #---AUTOPRAGMASTART---
3 5     5   532621 use v5.42;
  5         23  
4 5     5   53 use strict;
  5         24  
  5         277  
5 5     5   2703 use diagnostics;
  5         2580152  
  5         46  
6 5     5   4085 use mro 'c3';
  5         14  
  5         46  
7 5     5   3335 use English;
  5         13707  
  5         34  
8 5     5   2748 use Carp qw[carp croak confess cluck longmess shortmess];
  5         10  
  5         531  
9             our $VERSION = 0.5;
10 5     5   2057 use autodie qw( close );
  5         77786  
  5         34  
11 5     5   3998 use Array::Contains;
  5         14375  
  5         419  
12 5     5   2129 use utf8;
  5         1239  
  5         39  
13 5     5   2886 use Data::Dumper;
  5         44892  
  5         398  
14 5     5   2656 use Data::Printer;
  5         172684  
  5         43  
15             #---AUTOPRAGMAEND---
16              
17              
18 5     5   4799 use Lib::Pepper;
  5         17  
  5         340  
19 5     5   2828 use Lib::Pepper::Instance;
  5         97  
  5         314  
20 5     5   37 use Lib::Pepper::OptionList;
  5         76  
  5         262  
21 5     5   130 use Lib::Pepper::Constants qw(:all);
  5         71  
  5         30285  
22              
23             # Package-level state for multi-terminal support
24             # These variables track the library initialization state across all instances
25             our $LIBRARY_INITIALIZED = 0; # Boolean: Library initialized in this process?
26             our $INSTANCE_COUNT = 0; # Integer: Number of active instances
27             our $INIT_LIBRARY_PATH = ''; # String: Library path used for initialization
28             our $INIT_CONFIG_XML = undef; # String: Config XML used for initialization
29             our $INIT_LICENSE_XML = undef; # String: License XML used for initialization
30             our %INSTANCE_ID_COUNTERS = (); # Hash: terminal_type => next_available_id
31              
32             # High-level wrapper for Lib::Pepper providing simple payment terminal operations
33              
34 5     5 1 511927 sub new($proto, %params) {
  5         10  
  5         19  
  5         6  
35 5   33     27 my $class = ref($proto) || $proto;
36              
37             # Validate required parameters
38 5 100       15 if(!defined $params{terminal_type}) {
39 1         143 croak("new: terminal_type parameter is required");
40             }
41 4 100       12 if(!defined $params{terminal_address}) {
42 1         92 croak("new: terminal_address parameter is required");
43             }
44              
45             # Handle config: accept either config_file OR config_xml
46 3         15 my $configXml;
47 3 100       11 if(defined $params{config_xml}) {
    50          
48 2         5 $configXml = $params{config_xml};
49             } elsif(defined $params{config_file}) {
50             # Load config from file
51 0 0       0 if(!-f $params{config_file}) {
52 0         0 croak("new: config_file '$params{config_file}' does not exist");
53             }
54 0 0       0 open(my $fh, '<', $params{config_file}) or croak("new: cannot open config_file '$params{config_file}': $ERRNO");
55 0         0 $configXml = do { local $INPUT_RECORD_SEPARATOR = undef; <$fh> };
  0         0  
  0         0  
56 0         0 close($fh);
57             } else {
58 1         98 croak("new: either config_xml or config_file parameter is required");
59             }
60              
61             # Handle license: accept either license_file OR license_xml
62 2         4 my $licenseXml;
63 2 50       11 if(defined $params{license_xml}) {
    50          
64 0         0 $licenseXml = $params{license_xml};
65             } elsif(defined $params{license_file}) {
66             # Load license from file
67 0 0       0 if(!-f $params{license_file}) {
68 0         0 croak("new: license_file '$params{license_file}' does not exist");
69             }
70 0 0       0 open(my $fh, '<', $params{license_file}) or croak("new: cannot open license_file '$params{license_file}': $ERRNO");
71 0         0 $licenseXml = do { local $INPUT_RECORD_SEPARATOR = undef; <$fh> };
  0         0  
  0         0  
72 0         0 close($fh);
73             } else {
74 2         408 croak("new: either license_xml or license_file parameter is required");
75             }
76              
77             # Create object with default values
78             my $self = bless {
79             # Library state
80             initialized => 0,
81             library_path => $params{library_path} // '',
82             config_xml => $configXml,
83             license_xml => $licenseXml,
84              
85             # Instance state
86             instance => undef,
87             terminal_type => $params{terminal_type},
88             terminal_address => $params{terminal_address},
89             configured => 0,
90              
91             # Configuration
92             pos_number => $params{pos_number} // '0001',
93             merchant_password => $params{merchant_password} // '000000',
94             language => $params{language} // PEP_LANGUAGE_ENGLISH,
95             ticket_width => $params{ticket_width} // 40,
96             ticket_printing_mode => $params{ticket_printing_mode} // 0, # Default: POS prints (disables terminal printer)
97             tip_enabled => $params{tip_enabled} // 0, # Default: no tip dialog
98              
99             # Callback handling
100             callback => $params{callback},
101             userdata => $params{userdata} // {},
102              
103             # Logging
104             reph => $params{reph}, # Optional reporting handler with debuglog() method
105              
106             # State tracking
107 0   0     0 connection_open => 0,
      0        
      0        
      0        
      0        
      0        
      0        
      0        
108             last_error => undef,
109             last_transaction => undef,
110              
111             # Multi-instance support: Track if this instance was counted
112             _instance_counted => 0, # Set to 1 only after successful initialization
113             }, $class;
114              
115             # Set config_byte based on ticket_printing_mode if not explicitly provided
116             # Mode 1 (EFT prints) = PEP_CONFIG_BYTE_NORMAL (normal operation)
117             # Mode 0 (POS prints) = PEP_CONFIG_BYTE_DISABLE_PRINTER (disable terminal printer)
118             # Other modes = PEP_CONFIG_BYTE_NORMAL (normal operation)
119 0 0       0 if(exists $params{config_byte}) {
120             # User explicitly provided config_byte, use it
121 0         0 $self->{config_byte} = $params{config_byte};
122             } else {
123             # Auto-calculate based on ticket_printing_mode
124 0 0       0 $self->{config_byte} = ($self->{ticket_printing_mode} == 0)
125             ? PEP_CONFIG_BYTE_DISABLE_PRINTER
126             : PEP_CONFIG_BYTE_NORMAL;
127             }
128              
129             # Handle CARDTYPES_AUTODETECT placeholder if present
130 0 0       0 if($self->{config_xml} =~ /CARDTYPES_AUTODETECT/) {
131 0         0 my $cardtypesPath = Lib::Pepper->cardtypesFile();
132 0 0       0 if(!defined $cardtypesPath) {
133 0         0 croak("Could not find installed pepper_cardtypes.xml file");
134             }
135 0         0 $self->{config_xml} =~ s/CARDTYPES_AUTODETECT/$cardtypesPath/g;
136             }
137              
138             # Attempt full initialization
139 0         0 my $success = 0;
140 0         0 my $needsInitialization = !$LIBRARY_INITIALIZED; # Declared before eval for error handling
141              
142 0         0 eval {
143              
144             # ========================================
145             # MULTI-INSTANCE SUPPORT: Library initialization
146             # ========================================
147             # Initialize Pepper library only if not already initialized
148             # This allows multiple terminals to share the same library instance
149              
150 0 0       0 if($needsInitialization) {
151             # First instance - initialize library
152             Lib::Pepper->initialize(
153             library_path => $self->{library_path},
154             config_xml => $self->{config_xml},
155             license_xml => $self->{license_xml},
156 0         0 );
157              
158             # Store initialization parameters for validation of subsequent instances
159 0         0 $INIT_LIBRARY_PATH = $self->{library_path};
160 0         0 $INIT_CONFIG_XML = $self->{config_xml};
161 0         0 $INIT_LICENSE_XML = $self->{license_xml};
162 0         0 $LIBRARY_INITIALIZED = 1;
163              
164 0         0 $self->{initialized} = 1;
165             } else {
166             # Library already initialized - validate config compatibility
167 0 0       0 if(!_validate_config($self->{library_path}, $self->{config_xml}, $self->{license_xml})) {
168 0         0 croak("Configuration mismatch: Lib::Pepper library already initialized with different config. " .
169             "All Lib::Pepper::Simple instances in the same process must use identical " .
170             "library_path, config_xml, and license_xml parameters.");
171             }
172 0         0 $self->{initialized} = 1; # Library is already initialized
173             }
174              
175             # ========================================
176             # MULTI-INSTANCE SUPPORT: Instance ID allocation
177             # ========================================
178             # Allocate instance_id automatically if not provided by user
179             # Instance IDs are managed per terminal_type
180 0         0 my $instanceId = $params{instance_id};
181 0 0       0 if(!defined $instanceId) {
182 0         0 $instanceId = _allocate_instance_id($self->{terminal_type});
183             }
184             # Store instance_id in object for status reporting
185 0         0 $self->{instance_id} = $instanceId;
186              
187             # Create instance
188             $self->{instance} = Lib::Pepper::Instance->new(
189             terminal_type => $self->{terminal_type},
190             instance_id => $instanceId,
191             reph => $self->{reph}, # Pass reporting handler down
192 0         0 );
193              
194             # Configure instance
195             my $configResult = $self->{instance}->configure(
196             callback => $self->{callback} // \&_defaultCallback,
197             options => {
198             sHostName => $self->{terminal_address},
199             iLanguageValue => $self->{language},
200             sPosIdentificationString => $self->{pos_number},
201             iTicketWidthValue => $self->{ticket_width},
202             iConfigByteValue => $self->{config_byte},
203             sMerchantPasswordString => $self->{merchant_password},
204             iTicketPrintingModeValue => $self->{ticket_printing_mode},
205             },
206             userdata => $self->{userdata},
207 0   0     0 );
208 0         0 $self->{configured} = 1;
209              
210             # Check and handle recovery flag automatically
211 0 0       0 if(defined $configResult) {
212 0         0 my $configData = $configResult->toHashref();
213 0   0     0 my $recoveryFlag = $configData->{iRecoveryFlag} // 0;
214              
215 0 0       0 if($recoveryFlag) {
216             # Perform recovery operation automatically
217 0         0 my $recoveryOptions = Lib::Pepper::OptionList->new();
218              
219 0         0 my ($op1, $out1) = $self->{instance}->prepareOperation(PEP_OPERATION_RECOVERY, $recoveryOptions);
220 0         0 my ($op2, $out2) = $self->{instance}->startOperation(PEP_OPERATION_RECOVERY, $recoveryOptions);
221 0         0 my ($op3, $out3) = $self->{instance}->executeOperation(PEP_OPERATION_RECOVERY, $recoveryOptions);
222 0         0 my ($op4, $out4) = $self->{instance}->finalizeOperation(PEP_OPERATION_RECOVERY, $recoveryOptions);
223              
224 0         0 my $recoveryStatus = $self->{instance}->operationStatus($op4, 1);
225 0 0       0 if(!$recoveryStatus) {
226 0         0 croak("Recovery operation did not complete successfully");
227             }
228             }
229             }
230              
231             # Open connection (required for ZVT terminals)
232             my $openResult = $self->{instance}->openConnection(
233             options => {
234 0   0     0 sOperatorIdentificationString => $params{operator_id} // 'OPERATOR01',
235             }
236             );
237              
238             # Wait for OPEN to complete if pending
239 0 0       0 if(!$openResult->{status}) {
240 0         0 my $opHandle = $openResult->{operation_handle};
241 0         0 my $waitStatus = $self->{instance}->operationStatus($opHandle, 1);
242 0 0       0 if(!$waitStatus) {
243 0         0 croak("OPEN operation did not complete successfully");
244             }
245             }
246 0         0 $self->{connection_open} = 1;
247              
248 0         0 $success = 1;
249             };
250              
251 0 0       0 if(!$success) {
252 0         0 my $error = $EVAL_ERROR;
253              
254             # ========================================
255             # MULTI-INSTANCE SUPPORT: Cleanup on failure
256             # ========================================
257             # Note: We do NOT finalize the library on failure.
258             # The library stays initialized, allowing retry attempts.
259             # Instance counter was NOT incremented yet, so nothing to decrement.
260             # This is consistent with our never-finalize design.
261              
262 0         0 croak("Failed to initialize Lib::Pepper::Simple: $error");
263             }
264              
265             # ========================================
266             # MULTI-INSTANCE SUPPORT: Increment instance counter
267             # ========================================
268             # Only increment after successful initialization
269             # This ensures the counter stays accurate even if initialization fails
270 0         0 $INSTANCE_COUNT++;
271 0         0 $self->{_instance_counted} = 1; # Mark that this instance was counted
272              
273 0         0 return $self;
274             }
275              
276 0     0 1 0 sub checkStatus($self) {
  0         0  
  0         0  
277             my $status = {
278             # Instance-level status
279             library_initialized => $self->{initialized},
280             instance_configured => $self->{configured},
281             connection_open => $self->{connection_open},
282             terminal_type => $self->{terminal_type},
283             terminal_address => $self->{terminal_address},
284             instance_id => $self->{instance_id},
285             ready_for_transactions => 0,
286             last_error => $self->{last_error},
287              
288             # Process-level status (multi-terminal support)
289 0         0 process_instance_count => $INSTANCE_COUNT,
290             process_library_initialized => $LIBRARY_INITIALIZED,
291             };
292              
293             # Ready if all critical components are initialized
294             $status->{ready_for_transactions} = (
295             $status->{library_initialized} &&
296             $status->{instance_configured} &&
297             $status->{connection_open}
298 0   0     0 );
299              
300 0         0 return $status;
301             }
302              
303 0     0 1 0 sub doPayment($self, $amount, %options) {
  0         0  
  0         0  
  0         0  
  0         0  
304             # Validate state
305 0 0       0 if(!$self->{connection_open}) {
306 0         0 croak("doPayment: connection not open");
307             }
308 0 0       0 if(!$self->{configured}) {
309 0         0 croak("doPayment: instance not configured");
310             }
311              
312             # Validate amount
313 0 0 0     0 if(!defined $amount || $amount !~ /^\d+$/ || $amount <= 0) {
      0        
314 0         0 croak("doPayment: amount must be a positive integer (cents)");
315             }
316              
317             # Build transaction parameters
318 0   0     0 my $transactionType = $options{transaction_type} // PEP_TRANSACTION_TYPE_GOODS_PAYMENT;
319              
320             # Convert string transaction types to constants
321 0 0 0     0 if(defined $transactionType && $transactionType !~ /^\d+$/) {
322 0 0       0 if($transactionType eq 'goods') {
323 0         0 $transactionType = PEP_TRANSACTION_TYPE_GOODS_PAYMENT;
324             } else {
325 0         0 croak("doPayment: invalid transaction_type '$transactionType' (use 'goods' or numeric constant)");
326             }
327             }
328              
329 0         0 my $currency = $options{currency};
330              
331             # Build additional options for the transaction
332 0   0     0 my $txnOptions = $options{options} // {};
333              
334             # Tip support via Pepper API:
335             # - Transaction type 13 (GoodsPaymentWithTip): NOT supported by GlobalPayments ZVT (returns -1402)
336             # - iServiceByteValue bit 3 (tippable): Transaction completes but tip dialog depends on terminal config
337             # - The tip prompt/dialog must be enabled at the TERMINAL level by your payment processor
338             # - If you need tips, either:
339             # 1. Contact GlobalPayments to enable tip prompting on your terminal
340             # 2. Collect tip amount in your POS and pass it via doPayment(..., options => {iTipAmount => $tip})
341 0 0       0 if($self->{tip_enabled}) {
342 0 0       0 if(!exists $txnOptions->{iServiceByteValue}) {
343 0         0 $txnOptions->{iServiceByteValue} = 8; # Bit 3 = tippable
344             } else {
345 0         0 $txnOptions->{iServiceByteValue} |= 8;
346             }
347             }
348              
349             # Execute transaction
350 0         0 my $result;
351 0         0 my $success = 0;
352 0         0 eval {
353             $result = $self->{instance}->transaction(
354             amount => $amount,
355             transaction_type => $transactionType,
356             (defined $currency ? (currency => $currency) : ()),
357 0 0       0 (keys %{$txnOptions} ? (options => $txnOptions) : ()),
  0 0       0  
358             );
359 0         0 $success = 1;
360             };
361              
362 0 0       0 if(!$success) {
363 0         0 $self->{last_error} = $EVAL_ERROR;
364             return {
365 0         0 success => 0,
366             authorized => 0,
367             error => $EVAL_ERROR,
368             };
369             }
370              
371             # Parse result output
372 0         0 my $outputData = {};
373 0 0       0 if(defined $result->{output}) {
374 0         0 $outputData = $result->{output}->toHashref();
375             }
376              
377             # Extract transaction result - CRITICAL: check iTransactionResultValue, not iFunctionResultValue!
378 0   0     0 my $transactionResult = $outputData->{iTransactionResultValue} // -999;
379 0   0     0 my $transactionText = $outputData->{sTransactionText} || '';
380              
381             # Build response
382             my $response = {
383             success => $result->{status} ? 1 : 0,
384             authorized => ($transactionResult == 0) ? 1 : 0,
385             amount_charged => ($transactionResult == 0) ? $amount : 0,
386             transaction_result => $transactionResult,
387             transaction_text => $transactionText,
388             trace_number => $outputData->{sTraceNumberString} || undef,
389             authorization_code => $outputData->{sAuthorizationNumberString} || undef,
390             reference_number => $outputData->{sTransactionReferenceNumberString} || undef,
391             terminal_id => $outputData->{sTerminalIdentificationString} || undef,
392             card_type => $outputData->{sCardNameString} || undef,
393             card_number => $outputData->{sCardNumberString} || undef,
394             transaction_date => $outputData->{sTransactionDate} || undef,
395             transaction_time => $outputData->{sTransactionTime} || undef,
396 0 0 0     0 raw_output => $outputData,
    0 0        
    0 0        
      0        
      0        
      0        
      0        
      0        
397             };
398              
399             # Store last transaction details
400 0         0 $self->{last_transaction} = $response;
401 0         0 $self->{last_error} = undef;
402              
403 0         0 return $response;
404             }
405              
406 0     0 1 0 sub cancelPayment($self, $traceNumber, $amount, %options) {
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
407             # Validate state
408 0 0       0 if(!$self->{connection_open}) {
409 0         0 croak("cancelPayment: connection not open");
410             }
411 0 0       0 if(!$self->{configured}) {
412 0         0 croak("cancelPayment: instance not configured");
413             }
414              
415             # Validate parameters
416 0 0       0 if(!defined $traceNumber) {
417 0         0 croak("cancelPayment: trace_number parameter is required");
418             }
419 0 0 0     0 if(!defined $amount || $amount !~ /^\d+$/ || $amount <= 0) {
      0        
420 0         0 croak("cancelPayment: amount must be a positive integer (cents)");
421             }
422 0 0 0     0 if(!exists $options{reference_number} || !defined $options{reference_number}) {
423 0         0 croak("cancelPayment: reference_number parameter is required for card-not-present refunds");
424             }
425              
426 0         0 my $response = {
427             success => 0,
428             trace_number => undef,
429             amount_refunded => 0,
430             transaction_text => '',
431             raw_output => {},
432             };
433              
434             # Perform VOID (Transaction Type 12 - VoidGoodsPayment)
435             # Using sTransactionReferenceNumberString for referenced void
436             #
437             # IMPORTANT: Despite being called "VOID", this works AFTER settlement when using
438             # the reference number. This is the correct ZVT method for card-not-present refunds.
439             # Transaction Type 41 (Credit) would require card swipe even with reference number.
440             #
441             # This discovery is not documented in Pepper docs but confirmed working on GP PAY
442             # terminals with Generic ZVT protocol.
443 0         0 my $refundResult;
444 0         0 my $refundSuccess = 0;
445              
446 0         0 eval {
447             # Build options for referenced void (no card required)
448             my $refundOptions = {
449             sTransactionReferenceNumberString => $options{reference_number},
450 0         0 };
451              
452 0 0       0 if($ENV{DEBUG_PEPPER}) {
453 0         0 $self->_log("=== VOID/REFUND Parameters ===");
454 0         0 $self->_log("Transaction Type: VoidGoodsPayment (12)");
455 0         0 $self->_log("Amount: $amount");
456 0         0 for my $key (sort keys %{$refundOptions}) {
  0         0  
457 0         0 $self->_log("$key: $refundOptions->{$key}");
458             }
459             }
460              
461             $refundResult = $self->{instance}->transaction(
462 0         0 amount => $amount,
463             transaction_type => PEP_TRANSACTION_TYPE_VOID_GOODS_PAYMENT,
464             options => $refundOptions,
465             );
466 0         0 $refundSuccess = 1;
467             };
468              
469 0 0       0 if(!$refundSuccess) {
470 0         0 $self->{last_error} = "REFUND failed: $EVAL_ERROR";
471 0         0 return $response;
472             }
473              
474 0 0       0 if($ENV{DEBUG_PEPPER}) {
475 0         0 $self->_log("=== REFUND Result Check ===");
476 0         0 $self->_log("refundSuccess = $refundSuccess");
477 0 0       0 $self->_log("refundResult defined = " . (defined $refundResult ? 'YES' : 'NO'));
478 0 0       0 if(defined $refundResult) {
479 0 0       0 $self->_log("refundResult->{output} defined = " . (defined $refundResult->{output} ? 'YES' : 'NO'));
480             }
481             }
482              
483 0 0       0 if(defined $refundResult->{output}) {
484 0         0 my $refundData = $refundResult->{output}->toHashref();
485 0   0     0 my $refundTransResult = $refundData->{iTransactionResultValue} // -999;
486 0   0     0 my $refundTransText = $refundData->{sTransactionText} || '';
487              
488 0 0       0 if($ENV{DEBUG_PEPPER}) {
489 0         0 $self->_log("iTransactionResultValue: $refundTransResult");
490 0         0 $self->_log("sTransactionText: '$refundTransText'");
491             }
492              
493 0         0 $response->{transaction_text} = $refundTransText;
494 0         0 $response->{raw_output} = $refundData;
495              
496 0 0       0 if($refundTransResult == 0) {
497             # REFUND succeeded!
498 0         0 $response->{success} = 1;
499 0   0     0 $response->{trace_number} = $refundData->{sTraceNumberString} || undef;
500 0         0 $response->{amount_refunded} = $amount;
501              
502 0         0 $self->{last_error} = undef;
503 0         0 return $response;
504             } else {
505             # REFUND failed
506 0         0 $self->{last_error} = "REFUND failed: $refundTransText";
507 0         0 return $response;
508             }
509             }
510              
511 0         0 $self->{last_error} = "REFUND failed: no output data";
512 0         0 return $response;
513             }
514              
515 0     0 1 0 sub endOfDay($self, %options) {
  0         0  
  0         0  
  0         0  
516             # Validate state
517 0 0       0 if(!$self->{connection_open}) {
518 0         0 croak("endOfDay: connection not open");
519             }
520 0 0       0 if(!$self->{configured}) {
521 0         0 croak("endOfDay: instance not configured");
522             }
523              
524             # Execute settlement operation
525 0         0 my $result;
526 0         0 my $success = 0;
527 0         0 eval {
528             $result = $self->{instance}->settlement(
529             options => $options{options} // {},
530 0   0     0 );
531 0         0 $success = 1;
532             };
533              
534 0 0       0 if(!$success) {
535 0         0 $self->{last_error} = $EVAL_ERROR;
536             return {
537 0         0 success => 0,
538             error => $EVAL_ERROR,
539             };
540             }
541              
542             # Parse settlement output
543 0         0 my $outputData = {};
544 0 0       0 if(defined $result->{output}) {
545 0         0 $outputData = $result->{output}->toHashref();
546             }
547              
548             # Check settlement result (use iFunctionResultValue for settlement)
549 0   0     0 my $functionResult = $outputData->{iFunctionResultValue} // -999;
550 0   0     0 my $functionText = $outputData->{sFunctionText} || '';
551              
552             # Build response
553             my $response = {
554             success => ($functionResult == 0) ? 1 : 0,
555             function_result => $functionResult,
556             function_text => $functionText,
557             transaction_count => $outputData->{iNumberOfTransactions} || 0,
558             total_amount => $outputData->{iTotalAmount} || 0,
559             settlement_date => $outputData->{sSettlementDate} || undef,
560             settlement_time => $outputData->{sSettlementTime} || undef,
561 0 0 0     0 raw_output => $outputData,
      0        
      0        
      0        
562             };
563              
564 0 0       0 $self->{last_error} = ($functionResult == 0) ? undef : $functionText;
565              
566 0         0 return $response;
567             }
568              
569             # Allocate a unique instance_id for the given terminal_type
570             # Instance IDs are managed per terminal type (e.g., Generic ZVT terminals get IDs 1, 2, 3, ...)
571 0     0   0 sub _allocate_instance_id($terminal_type) {
  0         0  
  0         0  
572             # Initialize counter for this terminal type if not exists
573 0   0     0 $INSTANCE_ID_COUNTERS{$terminal_type} //= 1;
574              
575             # Get current ID and increment for next time
576 0         0 my $id = $INSTANCE_ID_COUNTERS{$terminal_type};
577 0         0 $INSTANCE_ID_COUNTERS{$terminal_type}++;
578              
579 0         0 return $id;
580             }
581              
582             # Validate that library initialization parameters match the first instance
583             # Returns 1 if valid, 0 if mismatch detected
584 0     0   0 sub _validate_config($library_path, $config_xml, $license_xml) {
  0         0  
  0         0  
  0         0  
  0         0  
585             # If library not initialized yet, any config is valid (this is first instance)
586 0 0       0 return 1 if !$LIBRARY_INITIALIZED;
587              
588             # Validate library_path matches
589 0 0       0 if($library_path ne $INIT_LIBRARY_PATH) {
590 0         0 return 0;
591             }
592              
593             # Validate config_xml matches (strict - must be identical)
594 0 0       0 if($config_xml ne $INIT_CONFIG_XML) {
595 0         0 return 0;
596             }
597              
598             # Validate license_xml matches
599 0 0       0 if($license_xml ne $INIT_LICENSE_XML) {
600 0         0 return 0;
601             }
602              
603 0         0 return 1;
604             }
605              
606             # Class method to check process-wide library status
607             # Returns hashref with library state information
608 1     1 1 221635 sub library_status($class) {
  1         2  
  1         1  
609             return {
610 1         8 initialized => $LIBRARY_INITIALIZED,
611             instance_count => $INSTANCE_COUNT,
612             library_path => $INIT_LIBRARY_PATH,
613             instance_ids => { %INSTANCE_ID_COUNTERS },
614             };
615             }
616              
617 0     0     sub DESTROY($self) {
  0            
  0            
618             # Cleanup instance resources
619 0 0         if($self->{instance}) {
620 0           eval {
621             # Connection cleanup handled by Instance destructor
622 0           undef $self->{instance};
623             };
624             }
625              
626             # ========================================
627             # MULTI-INSTANCE SUPPORT: Reference counting
628             # ========================================
629             # CRITICAL: Only decrement if this instance was actually counted!
630             # If constructor failed before incrementing, don't decrement
631 0 0         return unless $self->{_instance_counted};
632              
633             # Decrement the instance counter
634 0           $INSTANCE_COUNT--;
635              
636             # ========================================
637             # DESIGN DECISION: Never finalize the library
638             # ========================================
639             # The Pepper C library has a critical limitation: once pepFinalize() is called,
640             # pepInitialize() cannot be called again in the same process (returns error -103).
641             #
642             # SOLUTION: We never call pepFinalize(), keeping the library loaded in memory.
643             # This allows creating new instances even after all previous instances are destroyed.
644             #
645             # Trade-offs:
646             # PRO: Can create instances → destroy all → create new instances ✓
647             # PRO: No -103 errors, can run unlimited test iterations ✓
648             # PRO: Simpler lifecycle management ✓
649             # CON: Library stays in memory until process exit (~few MB)
650             # CON: Cannot reset library state without restarting process
651             #
652             # Note: Instance-level resources (connections, handles) are still properly cleaned up.
653             # Only the library initialization state persists.
654              
655 0           return;
656             }
657              
658             # Internal logging helper - uses reph if available, falls back to STDERR
659 0     0     sub _log($self, @parts) {
  0            
  0            
  0            
660 0 0 0       if($self->{reph} && $self->{reph}->can('debuglog')) {
661 0           $self->{reph}->debuglog(@parts);
662             } else {
663             # Fallback to STDERR if no reph provided
664 0           print STDERR join('', @parts), "\n";
665             }
666 0           return;
667             }
668              
669             # Default callback if user doesn't provide one
670             sub _defaultCallback {
671             # Silent default callback - user can provide their own for logging/debugging
672 0     0     return;
673             }
674              
675             1;
676             __END__