NAME
Lib::Pepper::Simple - High-level payment terminal interface
SYNOPSIS
use Lib::Pepper::Simple;
use Lib::Pepper::Constants qw(:all);
# Initialize connection to terminal
my $pepper = Lib::Pepper::Simple->new(
terminal_type => PEP_TERMINAL_TYPE_GENERIC_ZVT,
terminal_address => '192.168.1.163:20008',
license_file => '/path/to/license.xml',
config_file => '/path/to/config.xml',
);
# Check terminal status
my $status = $pepper->checkStatus();
if($status->{ready_for_transactions}) {
print "Terminal ready!"\n";
}
# Process a payment (100.50 EUR)
my $payment = $pepper->doPayment(10_050);
if($payment->{authorized}) {
print "Payment authorized!"\n";
print "Trace: $payment->{trace_number}"\n";
print "Auth: $payment->{authorization_code}"\n";
# Store trace number for potential cancellation
my $trace = $payment->{trace_number};
my $amount = $payment->{amount_charged};
}
# Cancel a payment (automatically handles VOID or REFUND)
my $cancel = $pepper->cancelPayment($trace, $amount);
if($cancel->{success}) {
print "Cancellation successful via $cancel->{method_used}"\n";
}
# End of day settlement
my $settlement = $pepper->endOfDay();
if($settlement->{success}) {
print "Settled $settlement->{transaction_count} transactions"\n";
print ""Total: " . sprintf("%.2f", $settlement->{total_amount} / 100)\n";
}
# MULTI-TERMINAL SUPPORT: Multiple terminals in same process
my $terminal1 = Lib::Pepper::Simple->new(
terminal_type => PEP_TERMINAL_TYPE_GENERIC_ZVT,
terminal_address => '192.168.1.163:20008',
license_file => $license, # Must be SAME as terminal2
config_file => $config, # Must be SAME as terminal2
);
my $terminal2 = Lib::Pepper::Simple->new(
terminal_type => PEP_TERMINAL_TYPE_GENERIC_ZVT,
terminal_address => '192.168.1.164:20008',
license_file => $license, # Must be SAME as terminal1
config_file => $config, # Must be SAME as terminal1
);
# Check process-wide status
my $libStatus = Lib::Pepper::Simple->library_status();
print "Active terminals: $libStatus->{instance_count}"\n";
# Process independent payments
my $payment1 = $terminal1->doPayment(1000);
my $payment2 = $terminal2->doPayment(2000);
DESCRIPTION
Lib::Pepper::Simple provides a high-level, easy-to-use interface for payment terminal operations. It wraps the complexity of the Pepper library into 5 simple methods.
Key Features
- Automatic Initialization
-
Single constructor call handles library initialization, instance creation, configuration, recovery operations, and connection opening.
- Smart Cancellation
-
cancelPayment()automatically tries VOID first (same-day instant cancellation), then falls back to REFUND (settled transaction reversal) if needed. - Clear Return Values
-
All methods return structured hashrefs with clear field names, not raw library objects.
- Automatic Recovery
-
Recovery operations are detected and performed automatically during initialization.
- Production Ready
-
Comprehensive error handling, state validation, and proper cleanup.
METHODS
new(%params)
Creates and fully initializes a payment terminal connection.
Required Parameters:
- terminal_type
-
Terminal type constant, e.g.
PEP_TERMINAL_TYPE_GENERIC_ZVT(118) - terminal_address
-
Terminal IP and port, e.g.
'192.168.1.163:20008' - license_xml OR license_file
-
Either the XML content of the license (
license_xml) or the path to the license file (license_file). Uselicense_xmlwhen storing licenses in a database. - config_xml OR config_file
-
Either the XML content of the configuration (
config_xml) or the path to the config file (config_file). Useconfig_xmlwhen storing configurations in a database.
Optional Parameters:
- library_path
-
Path to libpepcore.so (default: empty string for auto-detect)
- pos_number
-
POS identification string (default: '0001')
- merchant_password
-
Merchant password (default: '000000')
- language
-
Language constant (default:
PEP_LANGUAGE_ENGLISH) - ticket_width
-
Receipt width in characters (default: 40)
- config_byte
-
Terminal config byte. If not specified, automatically calculated based on
ticket_printing_mode:Mode 0 (POS prints):
PEP_CONFIG_BYTE_DISABLE_PRINTER(0x06 - disables terminal printer)Mode 1 (EFT prints):
PEP_CONFIG_BYTE_NORMAL(0x00 - normal operation)Other modes:
PEP_CONFIG_BYTE_NORMAL(0x00 - normal operation)
You can explicitly provide a
config_bytevalue to override this automatic behavior. Available constants:PEP_CONFIG_BYTE_NORMAL,PEP_CONFIG_BYTE_DISABLE_PRINTER. - ticket_printing_mode
-
Controls where transaction receipts are printed (default: 0 - POS/cash register handles printing).
Values:
0 - POS/cash register prints (PEP_TICKET_PRINTING_MODE_POS) - DEFAULT
1 - Terminal prints (PEP_TICKET_PRINTING_MODE_EFT)
2 - Client receipt on terminal only (PEP_TICKET_PRINTING_MODE_CLIENT_ONLY_EFT)
3 - No printing at all (PEP_TICKET_PRINTING_MODE_NONE)
4 - Both terminal and POS (PEP_TICKET_PRINTING_MODE_ECR_AND_TERMINAL)
Default is 0 (POS prints) which disables the terminal's built-in printer and indicates that the POS/cash register will handle receipt printing. This saves paper waste when your cash register already prints transaction details on invoices. Use 1 (EFT) to enable terminal printing if you want the terminal to print receipts.
Note: The module automatically sets the
config_byteparameter toPEP_CONFIG_BYTE_DISABLE_PRINTER(0x06) when mode 0 is used, which helps disable the terminal printer on many devices. This is handled internally and you don't need to setconfig_bytemanually. - callback
-
CODE reference for terminal callbacks (default: silent callback)
- userdata
-
Hashref of custom data passed to callbacks (default: {})
- reph
-
Optional reporting handler object for audit logging. The object must implement a
debuglog(@parts)method that accepts one or more strings and logs them as a single line.If not provided, debug output (when enabled) falls back to STDERR.
Example:
package MyLogger; sub new { bless {}, shift } sub debuglog { my ($self, @parts) = @_; print "LOG: ", join('', @parts), "\n"; } my $logger = MyLogger->new(); my $pepper = Lib::Pepper::Simple->new( ... reph => $logger, ); - operator_id
-
Operator identification string (default: 'OPERATOR01')
Returns: Blessed object reference
Dies on error: Throws exception with detailed error message
Example using file paths:
my $pepper = Lib::Pepper::Simple->new(
terminal_type => PEP_TERMINAL_TYPE_GENERIC_ZVT,
terminal_address => '192.168.1.163:20008',
license_file => '/etc/pepper/license.xml',
config_file => '/etc/pepper/config.xml',
);
Example using XML content (from database):
# Load license and config from database
my $licenseXml = $dbh->selectrow_array(
"SELECT license_xml FROM terminal_configs WHERE id = ?",
undef, $terminal_id
);
my $configXml = $dbh->selectrow_array(
"SELECT config_xml FROM terminal_configs WHERE id = ?",
undef, $terminal_id
);
my $pepper = Lib::Pepper::Simple->new(
terminal_type => PEP_TERMINAL_TYPE_GENERIC_ZVT,
terminal_address => '192.168.1.163:20008',
license_xml => $licenseXml,
config_xml => $configXml,
);
checkStatus($self)
Returns comprehensive terminal status information.
Returns hashref:
- library_initialized
-
Boolean - Pepper library initialized
- instance_configured
-
Boolean - Instance configured
- connection_open
-
Boolean - Connection to terminal open
- terminal_type
-
Terminal type constant
- terminal_address
-
Terminal IP:port string
- instance_id
-
Integer - This terminal's instance ID
- ready_for_transactions
-
Boolean - All checks passed, ready for payments
- last_error
-
Last error message or undef
- process_instance_count
-
Integer - Total number of active terminals in this process (multi-terminal support)
- process_library_initialized
-
Boolean - Library initialized at process level (multi-terminal support)
Example:
my $status = $pepper->checkStatus();
if(!$status->{ready_for_transactions}) {
die "Terminal not ready: $status->{last_error}";
}
library_status($class)
Class method to check process-wide library status. Useful for monitoring multi-terminal systems and debugging initialization issues.
Returns hashref:
- initialized
-
Boolean - Is the Pepper library initialized in this process?
- instance_count
-
Integer - Number of active Lib::Pepper::Simple instances (terminals)
- library_path
-
String - Library path used for initialization (empty string for auto-detect)
- instance_ids
-
Hashref - Next available instance_id per terminal_type
{ 118 => 3, # Next Generic ZVT terminal would get instance_id 3 120 => 2, # Next Hobex ZVT terminal would get instance_id 2 }
Example:
my $status = Lib::Pepper::Simple->library_status();
print "Library initialized: $status->{initialized}"\n";
print "Active terminals: $status->{instance_count}"\n";
if($status->{instance_count} > 0) {
print "Library is in use, cannot safely reinitialize"\n";
}
Multi-Terminal Monitoring:
# Before creating terminals
my $before = Lib::Pepper::Simple->library_status();
die "Library already initialized!" if $before->{initialized};
# Create terminals
my $t1 = Lib::Pepper::Simple->new(...);
my $t2 = Lib::Pepper::Simple->new(...);
# Check current state
my $current = Lib::Pepper::Simple->library_status();
print "Active terminals: $current->{instance_count}"\n"; # 2
# After cleanup
undef $t1;
undef $t2;
my $after = Lib::Pepper::Simple->library_status();
print ""Library finalized: " . (!$after->{initialized})\n"; # 1
doPayment($self, $amount, %options)
Performs a payment transaction.
Parameters:
- $amount (required)
-
Amount in smallest currency unit (cents for EUR/USD)
- transaction_type (optional)
-
Transaction type constant (default:
PEP_TRANSACTION_TYPE_GOODS_PAYMENT) - currency (optional)
-
Currency constant (usually from config)
- options (optional)
-
Hashref of additional transaction options
Returns hashref:
- success
-
Boolean - API call succeeded
-
Boolean - Payment was authorized (check this for actual payment success!)
- amount_charged
-
Actual amount charged in cents (0 if not authorized)
- transaction_result
-
Raw iTransactionResultValue (0 = authorized, -1 = declined/aborted)
- transaction_text
-
Human-readable status text
- trace_number
-
Trace number (CRITICAL: Store this for cancellations!)
-
Authorization code from payment processor
- reference_number
-
Transaction reference number
- terminal_id
-
Terminal identification
- card_type
-
Card brand (VISA, MASTERCARD, etc.)
- card_number
-
Masked card number
- transaction_date
-
Transaction date (YYYY-MM-DD)
- transaction_time
-
Transaction time (HH:MM:SS)
- raw_output
-
Complete output hashref from library
CRITICAL - Payment Authorization Check:
The success field indicates the API call completed successfully. The authorized field indicates the payment was actually authorized.
An aborted or declined payment will have success => 1 but authorized => 0!
Always check: if($payment->{authorized}) { ... }
Example:
my $payment = $pepper->doPayment(10_050); # 100.50 EUR
if($payment->{authorized}) {
# Payment AUTHORIZED - money will be charged
print "Payment successful!"\n";
print "Trace: $payment->{trace_number}"\n";
# Store in database for potential cancellation
store_payment({
invoice_id => 'INV-123',
trace => $payment->{trace_number},
amount => $payment->{amount_charged},
auth_code => $payment->{authorization_code},
});
} else {
# Payment DECLINED or ABORTED - no money charged
print "Payment failed: $payment->{transaction_text}"\n";
}
cancelPayment($self, $trace_number, $amount, %options)
Refunds a previous payment using referenced void (card-not-present).
This method uses Transaction Type 12 (VoidGoodsPayment) with sTransactionReferenceNumberString to perform a referenced reversal that does NOT require the customer's card to be present.
IMPORTANT: Despite being called "VOID", this works AFTER settlement when using the reference number. This is the correct method for card-not-present refunds in ZVT protocol.
Parameters:
- $trace_number (required)
-
Trace number from original payment (sTraceNumberString)
- $amount (required)
-
Amount in cents (must match original transaction amount)
- reference_number (required)
-
Reference number from original payment (sTransactionReferenceNumberString). This is what makes the refund work without requiring card swipe.
CRITICAL: You MUST store this value when processing the original payment!
Returns hashref:
- success
-
Boolean - Refund succeeded
- trace_number
-
New trace number for refund transaction
- amount_refunded
-
Amount refunded to customer in cents
- transaction_text
-
Status text from terminal
- raw_output
-
Complete output hashref from transaction
How It Works:
Performs a referenced void (Transaction Type 12 - VoidGoodsPayment) using the original transaction's reference number (sTransactionReferenceNumberString). This tells the terminal to reverse a specific previous transaction without requiring the customer to swipe their card again.
Key Discovery: Although this uses "VoidGoodsPayment", it works BOTH before and after settlement when you provide the reference number. The ZVT protocol documentation does not make this clear, but testing confirms this is the correct method for card-not-present refunds.
The refunded amount will appear in the customer's account in 3-5 business days.
Important Notes:
Both
trace_numberANDreference_numberfrom the original payment must be stored in your databaseThe refund amount must match the original transaction amount exactly
Customer does NOT need to be present or swipe card
Works before or after end-of-day settlement
Refund appears in customer account in 3-5 business days
Examples:
# Store payment details when processing original transaction
my $payment = $pepper->doPayment($amount);
if($payment->{authorized}) {
save_to_database({
order_id => $order_id,
trace_number => $payment->{trace_number},
reference_number => $payment->{reference_number}, # CRITICAL!
amount => $payment->{amount_charged},
});
}
# Later: Refund the transaction (no card needed)
my $stored = get_from_database($order_id);
my $refund = $pepper->cancelPayment(
$stored->{trace_number},
$stored->{amount},
reference_number => $stored->{reference_number}
);
if($refund->{success}) {
print "Refund successful!"\n";
print "Refund trace: $refund->{trace_number}"\n";
print "Customer will receive refund in 3-5 business days"\n";
update_database($order_id, status => 'refunded');
} else {
print "Refund failed: $refund->{transaction_text}"\n";
}
endOfDay($self, %options)
Performs end-of-day settlement (batch close).
Parameters:
Returns hashref:
- success
-
Boolean - Settlement succeeded
- function_result
-
Raw function result code
- function_text
-
Status text
- transaction_count
-
Number of transactions settled
- total_amount
-
Total amount in cents
- settlement_date
-
Settlement date (YYYY-MM-DD)
- settlement_time
-
Settlement time (HH:MM:SS)
- raw_output
-
Complete output hashref
CRITICAL: Settlement must be run daily to receive payment!
What Settlement Does:
Finalizes all transactions
Triggers money transfer to merchant account
Generates settlement reports
Clears terminal transaction buffer
After settlement, VOID operations become REFUND operations
Example:
my $settlement = $pepper->endOfDay();
if($settlement->{success}) {
print "Settlement successful!"\n";
print "Transactions: $settlement->{transaction_count}"\n";
print "Total: " . sprintf("%.2f EUR",
$settlement->{total_amount} / 100) . "\n";
} else {
print "Settlement failed: $settlement->{function_text}"\n";
}
COMPLETE WORKFLOW EXAMPLE
use Lib::Pepper::Simple;
use Lib::Pepper::Constants qw(:all);
# Morning - Initialize
my $pepper = Lib::Pepper::Simple->new(
terminal_type => PEP_TERMINAL_TYPE_GENERIC_ZVT,
terminal_address => '192.168.1.163:20008',
license_file => '/etc/pepper/license.xml',
config_file => '/etc/pepper/config.xml',
);
# Check status
my $status = $pepper->checkStatus();
die "Not ready" unless $status->{ready_for_transactions};
# During day - Process payment
my $payment = $pepper->doPayment(10_000); # 100.00 EUR
if($payment->{authorized}) {
# Store trace number in database
my $trace = $payment->{trace_number};
my $amount = $payment->{amount_charged};
# If customer cancels same day
my $cancel = $pepper->cancelPayment($trace, $amount);
if($cancel->{success}) {
print "Canceled via $cancel->{method_used}"\n";
}
}
# Evening - Settlement (CRITICAL!)
my $settlement = $pepper->endOfDay();
if($settlement->{success}) {
print "Day closed: $settlement->{transaction_count} transactions"\n";
}
MULTI-TERMINAL SUPPORT
Lib::Pepper::Simple supports multiple payment terminals in the same process, allowing a single application (e.g., PageCamel worker) to manage multiple physical terminals simultaneously.
How It Works
The Pepper C library is a process-wide singleton that can only be initialized once per process. Lib::Pepper::Simple handles this automatically using reference counting:
First instance created → Library initialized, counter = 1
Additional instances created → Reuse initialized library, counter increments
Instances destroyed → Counter decrements, library stays initialized
Last instance destroyed → Library finalized, counter = 0
Requirements
All terminals in the same process MUST use:
Identical
license_xml(orlicense_file)Identical
config_xml(orconfig_file)Identical
library_path(if specified)
Each terminal CAN have:
Different
terminal_address(IP:port) - REQUIRED for different physical terminalsDifferent
terminal_type(Generic ZVT, Hobex ZVT, etc.)Different per-terminal configuration (pos_number, merchant_password, etc.)
Different
instance_id(automatically allocated if not specified)
Instance ID Allocation
Instance IDs are automatically allocated per terminal type:
# Terminal type 118 (Generic ZVT)
my $t1 = Lib::Pepper::Simple->new(terminal_type => 118, ...); # instance_id = 1
my $t2 = Lib::Pepper::Simple->new(terminal_type => 118, ...); # instance_id = 2
# Terminal type 120 (Hobex ZVT)
my $t3 = Lib::Pepper::Simple->new(terminal_type => 120, ...); # instance_id = 1
Different terminal types maintain separate instance_id sequences.
You can manually specify an instance_id:
my $t = Lib::Pepper::Simple->new(
terminal_type => 118,
instance_id => 42, # Manual ID
...
);
WARNING: When manually specifying instance_id, ensure it doesn't conflict with other instances of the same terminal_type. The library will detect collisions.
Configuration Validation
When creating the second (or subsequent) terminal, Lib::Pepper::Simple validates that the configuration matches the first instance:
my $t1 = Lib::Pepper::Simple->new(license_file => $license1, ...); # OK
my $t2 = Lib::Pepper::Simple->new(license_file => $license2, ...); # FAILS!
# Error: Configuration mismatch: Lib::Pepper library already initialized
# with different config
This prevents configuration conflicts that could cause unpredictable behavior.
Checking Library Status
Class method to check process-wide library status:
my $status = Lib::Pepper::Simple->library_status();
Returns hashref:
- initialized
-
Boolean - Library initialized in this process?
- instance_count
-
Integer - Number of active terminal instances
- library_path
-
String - Library path used for initialization
- instance_ids
-
Hashref - Next available instance_id per terminal_type:
{ 118 => 3, # Next Generic ZVT would get ID 3 120 => 2, # Next Hobex ZVT would get ID 2 }
Example:
my $status = Lib::Pepper::Simple->library_status();
print "Active terminals: $status->{instance_count}"\n";
Instance-level status includes process info:
my $status = $terminal->checkStatus();
print "This terminal: instance_id $status->{instance_id}"\n";
print "Total active: $status->{process_instance_count} terminals"\n";
Transaction Isolation
Each terminal maintains completely independent transaction state:
my $payment1 = $terminal1->doPayment(1000); # Terminal 1
my $payment2 = $terminal2->doPayment(2000); # Terminal 2
# Different trace numbers, different transactions
$payment1->{trace_number} ne $payment2->{trace_number}
Cancellations are terminal-specific and use the trace number from the original terminal:
# Refund payment from Terminal 1
$terminal1->cancelPayment($payment1->{trace_number}, 1000,
reference_number => $payment1->{reference_number});
Complete Multi-Terminal Example
use Lib::Pepper::Simple;
use Lib::Pepper::Constants qw(:all);
# Initialize multiple terminals
my $terminalA = Lib::Pepper::Simple->new(
terminal_type => PEP_TERMINAL_TYPE_GENERIC_ZVT,
terminal_address => '192.168.1.163:20008', # Front counter
license_file => '/etc/pepper/license.xml',
config_file => '/etc/pepper/config.xml',
pos_number => '0001',
);
my $terminalB = Lib::Pepper::Simple->new(
terminal_type => PEP_TERMINAL_TYPE_GENERIC_ZVT,
terminal_address => '192.168.1.164:20008', # Drive-through
license_file => '/etc/pepper/license.xml', # SAME!
config_file => '/etc/pepper/config.xml', # SAME!
pos_number => '0002', # Different POS number
);
# Check library status
my $libStatus = Lib::Pepper::Simple->library_status();
print "Library initialized: $libStatus->{initialized}"\n";
print "Active terminals: $libStatus->{instance_count}"\n";
# Process payments independently
my $paymentA = $terminalA->doPayment(1500); # Front counter
if($paymentA->{authorized}) {
print "Front counter: Payment authorized"\n";
save_transaction('terminal_a', $paymentA->{trace_number},
$paymentA->{reference_number}, 1500);
}
my $paymentB = $terminalB->doPayment(2500); # Drive-through
if($paymentB->{authorized}) {
print "Drive-through: Payment authorized"\n";
save_transaction('terminal_b', $paymentB->{trace_number},
$paymentB->{reference_number}, 2500);
}
# Each terminal can do end-of-day independently
my $settlementA = $terminalA->endOfDay();
my $settlementB = $terminalB->endOfDay();
# Cleanup (automatic when objects destroyed)
undef $terminalA; # Counter = 1, library stays initialized
undef $terminalB; # Counter = 0, library finalized
Database Schema for Multi-Terminal
When storing transactions, include terminal identification:
CREATE TABLE payment_transactions (
id SERIAL PRIMARY KEY,
terminal_id VARCHAR(20) NOT NULL, -- 'terminal_a', 'terminal_b', etc.
instance_id INTEGER NOT NULL, -- From checkStatus()->{instance_id}
order_id INTEGER NOT NULL,
trace_number VARCHAR(20) NOT NULL,
reference_number VARCHAR(50) NOT NULL, -- For card-not-present refunds
amount INTEGER NOT NULL,
transaction_date TIMESTAMP DEFAULT NOW(),
INDEX idx_terminal (terminal_id),
INDEX idx_trace (trace_number),
UNIQUE (terminal_id, trace_number)
);
This allows you to identify which physical terminal processed each transaction.
Use Cases
- Retail with Multiple Checkout Lanes
-
Each checkout lane has its own terminal, all managed by a single POS application.
- Drive-Through + Counter Service
-
Fast food restaurant with both drive-through terminal and counter terminal.
- Multi-Location Kiosks
-
Self-service kiosks at different locations, managed by a central application.
- Backup Terminal
-
Primary terminal with automatic failover to backup terminal on connection loss.
Limitations
- Thread Safety
-
Not thread-safe. Do not create instances from different threads simultaneously. If using threads, protect instance creation with a mutex.
- Configuration Flexibility
-
All terminals must use identical license and config. Cannot mix different licenses or configurations in the same process.
- Instance ID Management
-
When manually specifying instance_id, you are responsible for avoiding conflicts.
ERROR HANDLING
Methods return structured hashrefs with error information rather than throwing exceptions for operational failures (payment declined, settlement failed, etc.).
Constructor (new()) and parameter validation errors throw exceptions with croak().
Always check the success field in return values:
my $payment = $pepper->doPayment($amount);
if(!$payment->{success}) {
# API call failed
die "Payment error: $payment->{error}";
}
if(!$payment->{authorized}) {
# Payment declined/aborted
print "Payment not authorized: $payment->{transaction_text}"\n";
}
AUTOMATIC FEATURES
- Recovery Operations
-
Automatically detected and performed during initialization if recovery flag is set.
- VOID→REFUND Fallback
-
cancelPayment()automatically switches from VOID to REFUND for settled transactions. - Connection Management
-
OPEN operation performed automatically. Connection closed in destructor.
- State Validation
-
All operations validate state before execution with clear error messages.
WARNING: AI USE
Warning, this file was generated with the help of the 'Claude' AI (an LLM/large language model by the USA company Anthropic PBC) in November 2025. It was not reviewed line-by-line by a human, only on a functional level. It is therefore not up to the usual code quality and review standards. Different copyright laws may also apply, since the program was not created by humans but mostly by a machine, therefore the laws requiring a human creative process may or may not apply. Laws regarding AI use are changing rapidly. Before using the code provided in this file for any of your projects, make sure to check the current version of your local laws.
SEE ALSO
- Lib::Pepper
-
Low-level library interface
- Lib::Pepper::Instance
-
Terminal instance management
- Lib::Pepper::Constants
-
Constants for terminal types, languages, transaction types, etc.
AUTHOR
Rene Schickbauer, <cavac@cpan.org>
COPYRIGHT AND LICENSE
Copyright (C) 2025 by Rene Schickbauer
This library is free software; you can redistribute it and/or modify it under the same terms as Perl itself.