Below is a differential between the unpatched vulnerable code and the patched update, for reference.
--- a/klamra-paycal-for-aspaclaria/includes/Admin/Page_Connector.php
+++ b/klamra-paycal-for-aspaclaria/includes/Admin/Page_Connector.php
@@ -79,16 +79,22 @@
</tr>
<tr>
<th scope="row"><label for="merchant_id">Merchant ID</label></th>
- <td><input name="merchant_id" id="merchant_id" type="text" class="regular-text" value="<?php echo esc_attr($settings['merchant_id']); ?>"></td>
+ <td>
+ <input name="merchant_id" id="merchant_id" type="text" class="regular-text" value="<?php echo esc_attr($settings['merchant_id']); ?>" placeholder="mrc_...">
+ <p class="description">Merchant ID identifies the PayCal merchant. It is not a license key.</p>
+ </td>
</tr>
<tr>
<th scope="row"><label for="terminal_id">Terminal ID</label></th>
- <td><input name="terminal_id" id="terminal_id" type="text" class="regular-text" value="<?php echo esc_attr($settings['terminal_id']); ?>"></td>
+ <td>
+ <input name="terminal_id" id="terminal_id" type="text" class="regular-text" value="<?php echo esc_attr($settings['terminal_id']); ?>" placeholder="trm_...">
+ <p class="description">Terminal ID identifies the PayCal payment terminal.</p>
+ </td>
</tr>
<tr>
- <th scope="row"><label for="public_key">Public API Key</label></th>
+ <th scope="row"><label for="public_key">Public Key</label></th>
<td>
- <input name="public_key" id="public_key" type="text" class="regular-text" value="<?php echo esc_attr($settings['public_key']); ?>">
+ <input name="public_key" id="public_key" type="text" class="regular-text" value="<?php echo esc_attr($settings['public_key']); ?>" placeholder="pk_...">
<p class="description">אין לשמור כאן Secret Key פרטי. סודות נשמרים רק בפלטפורמה המרכזית.</p>
</td>
</tr>
--- a/klamra-paycal-for-aspaclaria/includes/Admin/Page_Dashboard.php
+++ b/klamra-paycal-for-aspaclaria/includes/Admin/Page_Dashboard.php
@@ -14,10 +14,13 @@
$data = LicenseStore::get();
$mods = array_values(array_filter((array) ($data['modules'] ?? [])));
$license_label = Gate::status_label();
- $license_active = strtolower((string) $license_label) !== 'no license';
+ $license_active = !empty($data['valid']);
+ $license_in_grace = $license_label === 'Grace period';
+ $license_display = $license_active || $license_in_grace ? $license_label : 'לא מחובר';
$bit_enabled = Gate::has('bit');
- $invoices_enabled = Gate::has('invoices');
+ $invoice_settings = get_option('paycal_invoice_settings', []);
+ $invoices_enabled = !is_array($invoice_settings) || !isset($invoice_settings['enabled']) || (string) $invoice_settings['enabled'] === 'yes';
$bit_pending_count = 0;
if (class_exists('\WooCommerce') && $bit_enabled && function_exists('wc_get_orders')) {
@@ -51,12 +54,14 @@
echo ' </div>';
echo ' <div class="paycal-hero-panel">';
echo ' <div class="paycal-hero-stat">';
- echo ' <span class="paycal-hero-label">סטטוס רישוי</span>';
- echo ' <strong>' . esc_html($license_label) . '</strong>';
+ echo ' <span class="paycal-hero-label">רישוי</span>';
+ echo ' <strong>' . esc_html($license_in_grace ? 'License status: Grace period' : $license_display) . '</strong>';
if (!empty($data['expires_at'])) {
echo ' <small>תוקף עד ' . esc_html($data['expires_at']) . '</small>';
+ } elseif ($license_in_grace) {
+ echo ' <small>Optional commercial modules are not fully licensed. Core connector and admin access remain available.</small>';
} else {
- echo ' <small>' . ($license_active ? 'רישיון פעיל במערכת' : 'נדרש חיבור לרישיון') . '</small>';
+ echo ' <small>' . ($license_active ? 'רישיון פעיל במערכת' : 'רישיון מסחרי לא חובר. מודולים שאינם דורשים רישיון ימשיכו לעבוד.') . '</small>';
}
echo ' </div>';
echo ' <div class="paycal-hero-stat">';
@@ -75,8 +80,8 @@
echo '<section class="paycal-stats-grid">';
echo ' <article class="paycal-stat-card">';
echo ' <span class="paycal-stat-label">רישוי</span>';
- echo ' <strong class="paycal-stat-value">' . esc_html($license_label) . '</strong>';
- echo ' <p class="paycal-stat-meta">' . (!empty($data['expires_at']) ? 'תוקף עד ' . esc_html($data['expires_at']) : 'עדיין לא חובר רישיון פעיל') . '</p>';
+ echo ' <strong class="paycal-stat-value">' . esc_html($license_display) . '</strong>';
+ echo ' <p class="paycal-stat-meta">' . ($license_in_grace ? 'Optional commercial modules are not fully licensed. Core connector and admin access remain available.' : (!empty($data['expires_at']) ? 'תוקף עד ' . esc_html($data['expires_at']) : 'רישיון מסחרי לא חובר. מודולים שאינם דורשים רישיון ימשיכו לעבוד.')) . '</p>';
echo ' </article>';
echo ' <article class="paycal-stat-card">';
echo ' <span class="paycal-stat-label">Bit</span>';
@@ -86,7 +91,7 @@
echo ' <article class="paycal-stat-card">';
echo ' <span class="paycal-stat-label">Invoices</span>';
echo ' <strong class="paycal-stat-value">' . ($invoices_enabled ? 'פעיל' : 'לא פעיל') . '</strong>';
- echo ' <p class="paycal-stat-meta">' . ($invoices_enabled ? 'מודול חשבוניות זמין' : 'ניתן להפעיל דרך הרישיון') . '</p>';
+ echo ' <p class="paycal-stat-meta">' . ($invoices_enabled ? 'מודול חשבוניות זמין' : 'כבוי בהגדרות PayCal') . '</p>';
echo ' </article>';
echo ' <article class="paycal-stat-card">';
echo ' <span class="paycal-stat-label">מערכת</span>';
@@ -97,21 +102,18 @@
echo '<section class="paycal-dashboard-grid">';
echo ' <div class="paycal-card paycal-card-main">';
- echo ' <div class="paycal-section-head">';
- echo ' <div><h2>מצב מערכת</h2><p class="paycal-muted">תמונת מצב מהירה של השירותים המרכזיים במערכת.</p></div>';
- echo ' </div>';
echo ' <div class="paycal-kpi">';
- echo ' <span class="paycal-pill ' . ($license_active ? 'is-active' : '') . '" data-status="' . ($license_active ? 'active' : 'inactive') . '"><span class="paycal-dot"></span>רישוי: ' . esc_html($license_label) . '</span>';
+ echo ' <span class="paycal-pill ' . ($license_active ? 'is-active' : '') . '" data-status="' . ($license_active ? 'active' : 'inactive') . '"><span class="paycal-dot"></span>רישוי: ' . esc_html($license_display) . '</span>';
+ echo ' <span class="paycal-pill ' . ($invoices_enabled ? 'is-active' : '') . '" data-status="' . ($invoices_enabled ? 'active' : 'inactive') . '"><span class="paycal-dot"></span>חשבוניות: ' . ($invoices_enabled ? 'פעיל' : 'לא פעיל') . '</span>';
echo ' <span class="paycal-pill ' . ($bit_enabled ? 'is-active' : '') . '" data-status="' . ($bit_enabled ? 'active' : 'inactive') . '"><span class="paycal-dot"></span>Bit: ' . ($bit_enabled ? 'פעיל' : 'לא פעיל') . '</span>';
- echo ' <span class="paycal-pill ' . ($invoices_enabled ? 'is-active' : '') . '" data-status="' . ($invoices_enabled ? 'active' : 'inactive') . '"><span class="paycal-dot"></span>Invoices: ' . ($invoices_enabled ? 'פעיל' : 'לא פעיל') . '</span>';
echo ' <span class="paycal-pill"><span class="paycal-dot"></span>מודולים: <strong>' . esc_html((string) count($mods)) . '</strong></span>';
echo ' </div>';
echo ' <div class="paycal-module-list">';
if ($license_link) {
echo ' <div class="paycal-module-row">';
- echo ' <div><strong>רישיון</strong><span>חיבור רישיון ותוקף</span></div>';
- echo ' <div class="paycal-module-actions"><span class="paycal-badge ' . ($license_active ? 'is-success' : 'is-warning') . '">' . esc_html($license_label) . '</span><a class="paycal-step" href="' . esc_url($license_link) . '">פתח</a></div>';
+ echo ' <div><strong>רישוי</strong><span>רישיון מסחרי לא חובר. מודולים שאינם דורשים רישיון ימשיכו לעבוד.</span></div>';
+ echo ' <div class="paycal-module-actions"><span class="paycal-badge ' . ($license_active ? 'is-success' : 'is-warning') . '">' . esc_html($license_display) . '</span><a class="paycal-step" href="' . esc_url($license_link) . '">פתח</a></div>';
echo ' </div>';
}
if ($bit_link) {
@@ -143,9 +145,8 @@
echo ' </div>';
echo ' <div class="paycal-card">';
- echo ' <div class="paycal-section-head"><div><h2>תמונת מצב</h2><p class="paycal-muted">מה כדאי לעשות עכשיו כדי לקדם את המערכת.</p></div></div>';
echo ' <ul class="paycal-checklist">';
- echo ' <li>' . ($license_active ? 'הרישיון מחובר והמערכת יכולה לטעון מודולים בהתאם.' : 'לחבר רישיון כדי לפתוח מודולים ולמנוע חסימות.') . '</li>';
+ echo ' <li>' . ($license_active ? 'הרישיון מחובר והמערכת יכולה לטעון מודולים בהתאם.' : 'רישיון נדרש רק להפעלת מודולים מסחריים אופציונליים.') . '</li>';
echo ' <li>' . ($bit_enabled ? 'מודול Bit זמין. מומלץ לעבור על הזמנות ממתינות.' : 'להפעיל את Bit אם רוצים זרימת תשלומים מהירה בווקומרס.') . '</li>';
echo ' <li>' . ($invoices_enabled ? 'מודול החשבוניות פעיל וזמין להגדרות.' : 'להפעיל Invoices אם צריך מסמכים וניהול פיננסי מתוך המערכת.') . '</li>';
echo ' </ul>';
--- a/klamra-paycal-for-aspaclaria/includes/Admin/Page_Invoices.php
+++ b/klamra-paycal-for-aspaclaria/includes/Admin/Page_Invoices.php
@@ -3,31 +3,85 @@
defined('ABSPATH') || exit;
-use PayCalPermissionsAccess;
-
-use PayCalLicensingGate;
-
class Page_Invoices {
+ private static function can_access(): bool {
+ return current_user_can('manage_woocommerce')
+ || current_user_can('edit_shop_orders')
+ || current_user_can('manage_options');
+ }
+
public static function render(): void {
- Access::enforce('invoices');
+ if (!self::can_access()) {
+ wp_die(
+ esc_html__('You do not have permission to view PayCal invoices.', 'klamra-paycal-for-aspaclaria'),
+ esc_html__('Permission denied', 'klamra-paycal-for-aspaclaria'),
+ ['response' => 403]
+ );
+ }
+
+ $settings = function_exists('paycal_invoice_get_settings') ? paycal_invoice_get_settings() : [];
+ $enabled = !isset($settings['enabled']) || (string) $settings['enabled'] === 'yes';
+
+ $invoice_ids = get_posts([
+ 'post_type' => 'asp_invoice',
+ 'post_status' => 'publish',
+ 'fields' => 'ids',
+ 'posts_per_page' => 50,
+ 'orderby' => 'date',
+ 'order' => 'DESC',
+ ]);
+ if (!is_array($invoice_ids)) {
+ $invoice_ids = [];
+ }
echo '<div class="wrap paycal-wrap">';
- echo '<div class="paycal-header"><h1>PayCal — חשבוניות</h1><p>מודול החשבוניות (בקרוב / לפי רישיון)</p></div>';
+ echo '<div class="paycal-header"><h1>PayCal Invoices</h1><p>Invoice administration for WooCommerce orders.</p></div>';
+
+ if (!$enabled) {
+ echo '<div class="notice notice-warning"><p>' . esc_html__('Invoice module is currently disabled. Enable it in PayCal settings.', 'klamra-paycal-for-aspaclaria') . '</p></div>';
+ }
echo '<div class="paycal-card">';
+ echo '<p><strong>Module status:</strong> ' . esc_html($enabled ? 'enabled' : 'disabled by settings') . '</p>';
+ echo '<p><a class="button button-primary" href="' . esc_url(admin_url('admin.php?page=paycal-invoices-settings')) . '">' . esc_html__('Open invoice settings', 'klamra-paycal-for-aspaclaria') . '</a></p>';
+ echo '</div>';
- if (!Gate::has('invoices')) {
- echo '<p><strong>סטטוס:</strong> לא פעיל (נדרש רישיון חשבוניות)</p>';
- echo '<p><a class="button button-primary" href="'.esc_url(admin_url('admin.php?page=paycal-license')).'">הזן/בדוק רישיון</a> ';
- echo '<a class="button" href="'.esc_url(admin_url('admin.php?page=paycal-bundles')).'">לערכות</a></p>';
- echo '<p class="paycal-muted">כשתפעילי את מודול החשבוניות, יופיעו כאן ההגדרות והסטטוסים.</p>';
+ echo '<div class="paycal-card">';
+ echo '<h2>' . esc_html__('Invoices', 'klamra-paycal-for-aspaclaria') . '</h2>';
+
+ if (empty($invoice_ids)) {
+ echo '<p>' . esc_html__('No invoices found yet. Invoices will appear here after paid WooCommerce orders are created.', 'klamra-paycal-for-aspaclaria') . '</p>';
echo '</div></div>';
return;
}
- echo '<p><strong>סטטוס:</strong> פעיל ✅</p>';
- echo '<p class="paycal-muted">המודול פעיל ברישיון. אפשר לעצב חשבוניות מתוך מסך ההגדרות.</p>';
- echo '<p><a class="button button-primary" href="'.esc_url(admin_url('admin.php?page=paycal-invoices-settings')).'">פתח הגדרות חשבוניות</a></p>';
+ echo '<table class="widefat striped"><thead><tr>';
+ echo '<th>' . esc_html__('Invoice', 'klamra-paycal-for-aspaclaria') . '</th>';
+ echo '<th>' . esc_html__('Order', 'klamra-paycal-for-aspaclaria') . '</th>';
+ echo '<th>' . esc_html__('Customer', 'klamra-paycal-for-aspaclaria') . '</th>';
+ echo '<th>' . esc_html__('Date', 'klamra-paycal-for-aspaclaria') . '</th>';
+ echo '<th>' . esc_html__('Download', 'klamra-paycal-for-aspaclaria') . '</th>';
+ echo '</tr></thead><tbody>';
+
+ foreach ($invoice_ids as $invoice_id) {
+ $invoice_id = absint($invoice_id);
+ $order_id = absint(get_post_meta($invoice_id, '_order_id', true));
+ $order = $order_id && function_exists('wc_get_order') ? wc_get_order($order_id) : null;
+ $download_url = wp_nonce_url(
+ admin_url('admin-post.php?action=paycal_invoice_download&invoice_id=' . absint($invoice_id)),
+ 'paycal_invoice_download_' . absint($invoice_id)
+ );
+
+ echo '<tr>';
+ echo '<td><a href="' . esc_url(get_edit_post_link($invoice_id)) . '">' . esc_html(get_the_title($invoice_id)) . '</a></td>';
+ echo '<td>' . ($order_id ? '<a href="' . esc_url(admin_url('post.php?post=' . $order_id . '&action=edit')) . '">#' . esc_html((string) $order_id) . '</a>' : esc_html__('Unavailable', 'klamra-paycal-for-aspaclaria')) . '</td>';
+ echo '<td>' . esc_html($order ? trim($order->get_billing_first_name() . ' ' . $order->get_billing_last_name()) : '') . '</td>';
+ echo '<td>' . esc_html(get_the_date('', $invoice_id)) . '</td>';
+ echo '<td><a class="button" href="' . esc_url($download_url) . '">' . esc_html__('Download HTML', 'klamra-paycal-for-aspaclaria') . '</a></td>';
+ echo '</tr>';
+ }
+
+ echo '</tbody></table>';
echo '</div></div>';
}
}
--- a/klamra-paycal-for-aspaclaria/includes/Admin/Page_License.php
+++ b/klamra-paycal-for-aspaclaria/includes/Admin/Page_License.php
@@ -3,10 +3,10 @@
defined('ABSPATH') || exit;
-use PayCalPermissionsAccess;
-
+use PayCalLicensingGate;
use PayCalLicensingLicenseClient;
use PayCalLicensingLicenseStore;
+use PayCalPermissionsAccess;
class Page_License {
public static function render(): void {
@@ -18,15 +18,17 @@
if (!empty($_POST['paycal_action']) && $_POST['paycal_action'] === 'save_license') {
check_admin_referer('paycal_save_license');
- $key = sanitize_text_field($_POST['license_key'] ?? '');
+ $key = sanitize_text_field(wp_unslash($_POST['license_key'] ?? ''));
if (!$key) {
- $err = 'נא להזין מפתח רישוי.';
+ $err = 'Please enter a License Key.';
} else {
$res = LicenseClient::validate_remote($key);
if (!empty($res['ok'])) {
LicenseStore::set($res['data']);
- $msg = 'הרישיון אומת ונשמר בהצלחה.';
+ $msg = 'License verified and saved.';
+ } elseif (LicenseClient::looks_like_connector_credential($key)) {
+ $err = 'זה נראה כמו נתון חיבור של PayCal, לא מפתח רישוי. יש להזין אותו בהגדרות Merchant / Terminal.';
} else {
LicenseStore::set([
'license_key' => $key,
@@ -36,34 +38,56 @@
'validated_at' => time(),
'grace_until' => time() + (7 * DAY_IN_SECONDS),
]);
- $err = 'האימות נכשל: ' . esc_html($res['error'] ?? 'Unknown error');
+ $err = 'License verification failed: ' . esc_html($res['error'] ?? 'Unknown error');
}
}
}
$data = LicenseStore::get();
- $key_val = esc_attr($data['license_key'] ?? '');
- $status = esc_html(($data['valid'] ?? false) ? 'Active' : (LicenseStore::is_in_grace() ? 'Grace' : 'Inactive'));
+ $stored_key = (string) ($data['license_key'] ?? '');
+ $stored_connector_credential = $stored_key !== '' && LicenseClient::looks_like_connector_credential($stored_key);
+ if ($stored_connector_credential) {
+ $data['license_key'] = '';
+ $data['valid'] = false;
+ $data['modules'] = [];
+ $data['expires_at'] = '';
+ $data['grace_until'] = 0;
+ LicenseStore::set($data);
+ $stored_key = '';
+ if (!$err) {
+ $err = 'זה נראה כמו נתון חיבור של PayCal, לא מפתח רישוי. יש להזין אותו בהגדרות Merchant / Terminal.';
+ }
+ }
+ $key_val = esc_attr($stored_key);
+ $raw_status = class_exists(Gate::class) ? Gate::status_label() : 'לא מחובר';
+ $status_text = in_array($raw_status, ['Pending verification', 'Disabled'], true) ? 'לא מחובר' : $raw_status;
+ $status = esc_html($status_text);
$expires = esc_html($data['expires_at'] ?? '');
echo '<div class="wrap paycal-wrap">';
- echo '<div class="paycal-header"><h1>PayCal — License</h1><p>אימות רישיון והפעלת מודולים</p></div>';
+ echo '<div class="paycal-header"><h1>רישוי</h1><p>מפתחות רישוי נדרשים רק להפעלת מודולים מסחריים אופציונליים. נתוני חיבור התשלום מנוהלים בנפרד.</p></div>';
if ($msg) echo '<div class="notice notice-success"><p>' . esc_html($msg) . '</p></div>';
if ($err) echo '<div class="notice notice-error"><p>' . $err . '</p></div>';
echo '<div class="paycal-card">';
- echo '<p><strong>סטטוס:</strong> ' . $status . '</p>';
- if ($expires) echo '<p><strong>תוקף עד:</strong> ' . $expires . '</p>';
+ echo '<h2>רישוי</h2>';
+ echo '<p><strong>' . $status . '</strong></p>';
+ if ($raw_status === 'Grace period') {
+ echo '<p class="description">Optional commercial modules are not fully licensed. Core connector and admin access remain available.</p>';
+ }
+ if ($expires) echo '<p><strong>Expires:</strong> ' . $expires . '</p>';
+ echo '<p class="description">מפתחות רישוי נדרשים רק להפעלת מודולים מסחריים אופציונליים. נתוני חיבור התשלום מנוהלים בנפרד.</p>';
echo '<form method="post">';
wp_nonce_field('paycal_save_license');
echo '<input type="hidden" name="paycal_action" value="save_license" />';
echo '<div class="paycal-field" style="max-width:560px">';
- echo '<label for="license_key">מפתח רישוי</label>';
- echo '<input id="license_key" name="license_key" type="text" value="' . $key_val . '" />';
+ echo '<label for="license_key">License Key</label>';
+ echo '<input id="license_key" name="license_key" type="text" value="' . $key_val . '" placeholder="License token only, not mrc_/trm_/pk_" />';
echo '</div>';
- echo '<p class="submit"><button class="button button-primary">אמת ושמור</button></p>';
+ echo '<p><a href="' . esc_url(admin_url('admin.php?page=paycal-connector')) . '">Open Merchant/Terminal settings</a></p>';
+ echo '<p class="submit"><button class="button button-primary">Verify and save</button></p>';
echo '</form>';
echo '</div></div>';
--- a/klamra-paycal-for-aspaclaria/includes/Admin/Page_Wizard.php
+++ b/klamra-paycal-for-aspaclaria/includes/Admin/Page_Wizard.php
@@ -3,11 +3,11 @@
defined('ABSPATH') || exit;
-use PayCalPermissionsAccess;
-
+use PayCalConnectorConnectorStore;
+use PayCalLicensingGate;
use PayCalLicensingLicenseClient;
use PayCalLicensingLicenseStore;
-use PayCalLicensingGate;
+use PayCalPermissionsAccess;
class Page_Wizard {
public static function render(): void {
@@ -17,30 +17,53 @@
$msg = '';
$err = '';
- // Handle posts
if (!empty($_POST['paycal_wizard_action'])) {
check_admin_referer('paycal_wizard');
+ $action = sanitize_key(wp_unslash($_POST['paycal_wizard_action']));
+
+ if ($action === 'save_connector') {
+ ConnectorStore::save([
+ 'merchant_id' => sanitize_text_field(wp_unslash($_POST['merchant_id'] ?? '')),
+ 'terminal_id' => sanitize_text_field(wp_unslash($_POST['terminal_id'] ?? '')),
+ 'public_key' => sanitize_text_field(wp_unslash($_POST['public_key'] ?? '')),
+ ]);
+ wp_safe_redirect(admin_url('admin.php?page=paycal-setup&step=license'));
+ exit;
+ }
- if ($_POST['paycal_wizard_action'] === 'save_license') {
- $key = sanitize_text_field($_POST['license_key'] ?? '');
- $res = $key ? LicenseClient::validate_remote($key) : ['ok'=>false,'error'=>'נא להזין מפתח'];
+ if ($action === 'save_license') {
+ $key = sanitize_text_field(wp_unslash($_POST['license_key'] ?? ''));
+ if ($key === '') {
+ wp_safe_redirect(admin_url('admin.php?page=paycal-setup&step=modules'));
+ exit;
+ }
+ $res = LicenseClient::validate_remote($key);
if (!empty($res['ok'])) {
LicenseStore::set($res['data']);
wp_safe_redirect(admin_url('admin.php?page=paycal-setup&step=modules'));
exit;
+ }
+
+ if (LicenseClient::looks_like_connector_credential($key)) {
+ $err = 'זה נראה כמו נתון חיבור של PayCal, לא מפתח רישוי. יש להזין אותו בהגדרות Merchant / Terminal.';
} else {
- $err = 'אימות נכשל: ' . esc_html($res['error'] ?? 'Unknown error');
+ $err = 'License verification failed: ' . esc_html($res['error'] ?? 'Unknown error');
}
}
- if ($_POST['paycal_wizard_action'] === 'save_bit') {
- $phone = preg_replace('/D+/', '', (string)($_POST['bit_phone'] ?? ''));
- $display = sanitize_text_field($_POST['bit_display_name'] ?? '');
- $note = sanitize_textarea_field($_POST['bit_note'] ?? '');
+ if ($action === 'skip_license') {
+ wp_safe_redirect(admin_url('admin.php?page=paycal-setup&step=modules'));
+ exit;
+ }
+
+ if ($action === 'save_bit') {
+ $phone = preg_replace('/D+/', '', (string) ($_POST['bit_phone'] ?? ''));
+ $display = sanitize_text_field(wp_unslash($_POST['bit_display_name'] ?? ''));
+ $note = sanitize_textarea_field(wp_unslash($_POST['bit_note'] ?? ''));
if (strlen($phone) < 9) {
- $err = 'מספר הטלפון נראה קצר מדי. בדוק שהזנת מספר ישראלי תקין.';
+ $err = 'The Bit phone number looks too short. Check that you entered a valid phone number.';
} else {
PayCalModulesBitBitSettings::set([
'phone' => $phone,
@@ -52,13 +75,13 @@
}
}
- if ($_POST['paycal_wizard_action'] === 'finish') {
- $msg = 'ההתקנה הסתיימה. אפשר להתחיל לעבוד.';
+ if ($action === 'finish') {
+ $msg = 'Setup is complete.';
}
}
echo '<div class="wrap paycal-wrap paycal-wizard">';
- echo '<div class="paycal-header"><h1>PayCal — Setup Wizard</h1><p>התקנה מהירה: רישוי → מודולים → Bit → סיום</p></div>';
+ echo '<div class="paycal-header"><h1>PayCal Setup Wizard</h1><p>Connect payment credentials, optionally activate commercial modules, then finish setup.</p></div>';
self::steps_nav($step);
@@ -68,112 +91,107 @@
echo '<div class="paycal-card">';
if ($step === 'welcome') {
- echo '<h2>ברוך הבא</h2>';
- echo '<p class="paycal-muted">בוא נבצע התקנה קצרה: רישיון → מודולים → חיבור ביט.</p>';
- echo '<a class="paycal-btn-primary" href="' . esc_url(admin_url('admin.php?page=paycal-setup&step=license')) . '">התחל</a>';
+ echo '<h2>Welcome</h2>';
+ echo '<p class="paycal-muted">This wizard separates PayCal connector credentials from optional commercial license keys.</p>';
+ echo '<a class="paycal-btn-primary" href="' . esc_url(admin_url('admin.php?page=paycal-setup&step=merchant')) . '">Start setup</a>';
+ }
+
+ if ($step === 'merchant') {
+ $settings = ConnectorStore::get();
+ echo '<h2>Merchant / Terminal</h2>';
+ echo '<p class="paycal-muted">These are payment connector credentials. They are not license keys.</p>';
+ echo '<form method="post">';
+ wp_nonce_field('paycal_wizard');
+ echo '<input type="hidden" name="paycal_wizard_action" value="save_connector" />';
+ echo '<div class="paycal-field" style="max-width:560px"><label>Merchant ID</label><input name="merchant_id" type="text" value="' . esc_attr($settings['merchant_id']) . '" placeholder="mrc_..." /></div>';
+ echo '<div class="paycal-field" style="max-width:560px"><label>Terminal ID</label><input name="terminal_id" type="text" value="' . esc_attr($settings['terminal_id']) . '" placeholder="trm_..." /></div>';
+ echo '<div class="paycal-field" style="max-width:560px"><label>Public Key</label><input name="public_key" type="text" value="' . esc_attr($settings['public_key']) . '" placeholder="pk_..." /></div>';
+ echo '<p class="submit"><button class="button button-primary">Save and continue</button></p>';
+ echo '</form>';
}
if ($step === 'license') {
$data = LicenseStore::get();
- $key_val = esc_attr($data['license_key'] ?? '');
+ $stored_key = (string) ($data['license_key'] ?? '');
+ $looks_like_connector = $stored_key !== '' && LicenseClient::looks_like_connector_credential($stored_key);
+ $key_val = $looks_like_connector ? '' : esc_attr($stored_key);
+
+ if ($looks_like_connector) {
+ echo '<div class="notice notice-warning"><p>' . esc_html__('זה נראה כמו נתון חיבור של PayCal, לא מפתח רישוי. יש להזין אותו בהגדרות Merchant / Terminal.', 'klamra-paycal-for-aspaclaria') . '</p></div>';
+ }
- echo '<h2>שלב 1: אימות רישיון</h2>';
+ echo '<h2>License - optional</h2>';
+ echo '<p class="paycal-muted">מפתחות רישוי נדרשים רק להפעלת מודולים מסחריים אופציונליים. נתוני חיבור התשלום מנוהלים בנפרד.</p>';
echo '<form method="post">';
wp_nonce_field('paycal_wizard');
echo '<input type="hidden" name="paycal_wizard_action" value="save_license" />';
echo '<div class="paycal-field" style="max-width:560px">';
echo '<label>License Key</label>';
- echo '<input name="license_key" type="text" value="' . $key_val . '"/>';
+ echo '<input name="license_key" type="text" value="' . $key_val . '" placeholder="License token only, not mrc_/trm_/pk_" />';
echo '</div>';
- echo '<p class="submit"><button class="button button-primary">אמת והמשך</button></p>';
+ echo '<p><a href="' . esc_url(admin_url('admin.php?page=paycal-connector')) . '">Open Merchant/Terminal settings</a></p>';
+ echo '<p class="submit"><button class="button button-primary">Verify and continue</button></p>';
+ echo '</form>';
+ echo '<form method="post">';
+ wp_nonce_field('paycal_wizard');
+ echo '<input type="hidden" name="paycal_wizard_action" value="skip_license" />';
+ echo '<p class="submit"><button class="button">Continue without license</button></p>';
echo '</form>';
}
if ($step === 'modules') {
$mods = LicenseStore::entitlements();
- echo '<h2>שלב 2: מודולים זמינים</h2>';
-
+ echo '<h2>Modules</h2>';
if (empty($mods)) {
- echo '<p>לא נמצאו מודולים פעילים ברישיון. בדוק את הרישיון.</p>';
- echo '<a class="button" href="' . esc_url(admin_url('admin.php?page=paycal-license')) . '">למסך רישוי</a>';
+ echo '<p class="paycal-muted">No optional commercial modules are currently licensed. Core connector and invoice admin access remain available.</p>';
} else {
- echo '<p>לפי הרישיון שלך, אלו המודולים שנפתחו:</p><ul class="paycal-bullets">';
+ echo '<p>Licensed optional modules:</p><ul class="paycal-bullets">';
foreach ($mods as $m) echo '<li>' . esc_html($m) . '</li>';
echo '</ul>';
-
- if (in_array('bit', $mods, true)) {
- wp_safe_redirect(admin_url('admin.php?page=paycal-setup&step=bit'));
- exit;
- } else {
- wp_safe_redirect(admin_url('admin.php?page=paycal-setup&step=finish'));
- exit;
- }
}
+ echo '<p><a class="button button-primary" href="' . esc_url(admin_url('admin.php?page=paycal-setup&step=bit')) . '">Continue to Bit Setup</a> ';
+ echo '<a class="button" href="' . esc_url(admin_url('admin.php?page=paycal-setup&step=finish')) . '">Skip Bit Setup</a></p>';
}
if ($step === 'bit') {
if (!Gate::has('bit')) {
- echo '<p>מודול Bit לא פעיל ברישיון.</p>';
- echo '<a class="button" href="'.esc_url(admin_url('admin.php?page=paycal-bundles')).'">Marketplace</a>';
+ echo '<h2>Bit Setup</h2>';
+ echo '<p class="paycal-muted">Bit is an optional commercial module and is not currently licensed.</p>';
+ echo '<p><a class="button button-primary" href="' . esc_url(admin_url('admin.php?page=paycal-setup&step=finish')) . '">Continue to finish</a> ';
+ echo '<a class="button" href="'.esc_url(admin_url('admin.php?page=paycal-bundles')).'">Marketplace</a></p>';
} else {
$s = PayCalModulesBitBitSettings::get();
$phone = esc_attr($s['phone'] ?? '');
$display = esc_attr($s['display_name'] ?? '');
- $note = esc_textarea($s['note'] ?? 'העברה בביט לפי סכום ההזמנה. לאחר התשלום, ההזמנה תטופל.');
-
- echo '<h2>שלב 3: Bit Setup</h2>';
- echo '<p class="paycal-muted">הגדרה מהירה + תצוגה מקדימה של QR.</p>';
+ $note = esc_textarea($s['note'] ?? 'Transfer with Bit according to the order total. After payment, the order will be reviewed.');
+ echo '<h2>Bit Setup</h2>';
echo '<div class="paycal-two-col">';
-
echo '<div class="paycal-card">';
- echo '<h2>פרטי קבלת תשלום</h2><p class="paycal-muted">הפרטים יוצגו ללקוח במסך התשלום.</p>';
-
+ echo '<h2>Payment receiving details</h2>';
echo '<form method="post">';
wp_nonce_field('paycal_wizard');
echo '<input type="hidden" name="paycal_wizard_action" value="save_bit" />';
-
- echo '<div class="paycal-field" style="margin-bottom:12px">';
- echo '<label>טלפון לקבלת תשלום (Bit)</label>';
- echo '<input name="bit_phone" id="paycal_bit_phone" type="text" value="'.$phone.'" placeholder="05XXXXXXXX" />';
- echo '</div>';
-
- echo '<div class="paycal-field" style="margin-bottom:12px">';
- echo '<label>שם שיוצג ללקוח (אופציונלי)</label>';
- echo '<input name="bit_display_name" id="paycal_bit_display" type="text" value="'.$display.'" placeholder="שם העסק / שם לקבלת תשלום" />';
- echo '</div>';
-
- echo '<div class="paycal-field" style="margin-bottom:12px">';
- echo '<label>טקסט הנחיות ללקוח</label>';
- echo '<textarea name="bit_note" rows="4" id="paycal_bit_note">'.$note.'</textarea>';
- echo '</div>';
-
- echo '<p class="submit" style="margin-top:14px">';
- echo '<button class="button button-primary">שמור והמשך</button> ';
- echo '<a class="button" href="'.esc_url(admin_url('admin.php?page=paycal-bundles')).'">חזרה למרקטפלייס</a>';
- echo '</p>';
-
+ echo '<div class="paycal-field" style="margin-bottom:12px"><label>Bit phone</label><input name="bit_phone" id="paycal_bit_phone" type="text" value="'.$phone.'" placeholder="05XXXXXXXX" /></div>';
+ echo '<div class="paycal-field" style="margin-bottom:12px"><label>Display name</label><input name="bit_display_name" id="paycal_bit_display" type="text" value="'.$display.'" /></div>';
+ echo '<div class="paycal-field" style="margin-bottom:12px"><label>Customer instructions</label><textarea name="bit_note" rows="4" id="paycal_bit_note">'.$note.'</textarea></div>';
+ echo '<p class="submit" style="margin-top:14px"><button class="button button-primary">Save and continue</button></p>';
echo '</form>';
echo '</div>';
-
- echo '<div class="paycal-card">';
- echo '<h2>תצוגה מקדימה (QR)</h2><p class="paycal-muted">Preview בלבד – בשלב הבא נציג אותו במסך תשלום להזמנה.</p>';
- echo '<div class="paycal-qr"><div id="paycal_qr_box" data-phone="'.esc_attr($phone).'"></div></div>';
- echo '</div>';
-
+ echo '<div class="paycal-card"><h2>Preview</h2><div class="paycal-qr"><div id="paycal_qr_box" data-phone="'.esc_attr($phone).'"></div></div></div>';
echo '</div>';
}
}
if ($step === 'finish') {
- echo '<h2>סיום</h2>';
- echo '<p class="paycal-muted">המערכת מוכנה. אם Bit Gateway כבר אצלך פעיל, ההזמנות יעברו לסטטוס “ממתין לתשלום בביט”.</p>';
+ echo '<h2>Finish</h2>';
+ echo '<p class="paycal-muted">Setup is ready. You can return to the dashboard or adjust settings at any time.</p>';
echo '<form method="post">';
wp_nonce_field('paycal_wizard');
echo '<input type="hidden" name="paycal_wizard_action" value="finish" />';
- echo '<p class="submit"><button class="button button-primary">סיום</button></p>';
+ echo '<p class="submit"><button class="button button-primary">Finish</button></p>';
echo '</form>';
- echo '<p><a class="button" href="'.esc_url(admin_url('admin.php?page=paycal-dashboard')).'">חזרה לדשבורד</a></p>';
+ echo '<p><a class="button" href="'.esc_url(admin_url('admin.php?page=paycal-dashboard')).'">Back to dashboard</a></p>';
}
echo '</div></div>';
@@ -182,10 +200,11 @@
private static function steps_nav(string $current): void {
$steps = [
'welcome' => 'Welcome',
- 'license' => 'License',
+ 'merchant' => 'Merchant / Terminal',
+ 'license' => 'License - optional',
'modules' => 'Modules',
- 'bit' => 'Bit Setup',
- 'finish' => 'Finish',
+ 'bit' => 'Bit Setup',
+ 'finish' => 'Finish',
];
echo '<div class="paycal-steps">';
--- a/klamra-paycal-for-aspaclaria/includes/Connector/SyncClient.php
+++ b/klamra-paycal-for-aspaclaria/includes/Connector/SyncClient.php
@@ -41,7 +41,7 @@
'merchant_id' => $settings['merchant_id'] ?? '',
'terminal_id' => $settings['terminal_id'] ?? '',
'site_url' => home_url('/'),
- 'plugin_version' => defined('PAYCAL_VERSION') ? PAYCAL_VERSION : '1.1.4',
+ 'plugin_version' => defined('PAYCAL_VERSION') ? PAYCAL_VERSION : '1.1.5',
]),
]);
--- a/klamra-paycal-for-aspaclaria/includes/Licensing/Gate.php
+++ b/klamra-paycal-for-aspaclaria/includes/Licensing/Gate.php
@@ -6,8 +6,9 @@
class Gate {
public static function has(string $module): bool {
$data = LicenseStore::get();
+ $license_key = (string) ($data['license_key'] ?? '');
- if (empty($data['license_key'])) return false;
+ if (empty($license_key) || (class_exists(LicenseClient::class) && LicenseClient::looks_like_connector_credential($license_key))) return false;
if (!empty($data['valid'])) {
return in_array($module, (array)($data['modules'] ?? []), true);
@@ -22,9 +23,12 @@
public static function status_label(): string {
$data = LicenseStore::get();
- if (empty($data['license_key'])) return 'No license';
+ $license_key = (string) ($data['license_key'] ?? '');
+ if (!empty($data['disabled'])) return 'Disabled';
+ if (empty($license_key) || (class_exists(LicenseClient::class) && LicenseClient::looks_like_connector_credential($license_key))) return 'לא מחובר';
if (!empty($data['valid'])) return 'Active';
- if (LicenseStore::is_in_grace()) return 'Grace';
- return 'Inactive';
+ if (LicenseStore::is_in_grace()) return 'Grace period';
+ if (empty($data['validated_at'])) return 'Pending verification';
+ return 'Disabled';
}
}
--- a/klamra-paycal-for-aspaclaria/includes/Licensing/LicenseClient.php
+++ b/klamra-paycal-for-aspaclaria/includes/Licensing/LicenseClient.php
@@ -6,7 +6,21 @@
class LicenseClient {
const ENDPOINT = 'https://paycal.online/wp-json/wooboompay/v1/license/validate';
+ public static function looks_like_connector_credential(string $value): bool {
+ $value = trim($value);
+ return (bool) preg_match('/^(mrc_|trm_|pk_)/i', $value);
+ }
+
public static function validate_remote(string $license_key): array {
+ $license_key = trim($license_key);
+ if ($license_key === '') {
+ return ['ok' => false, 'error' => 'License key is required'];
+ }
+
+ if (self::looks_like_connector_credential($license_key)) {
+ return ['ok' => false, 'error' => 'זה נראה כמו נתון חיבור של PayCal, לא מפתח רישוי. יש להזין אותו בהגדרות Merchant / Terminal.'];
+ }
+
if (!function_exists('wp_remote_post')) {
error_log('[PayCal] WordPress HTTP API unavailable during license validation.');
return ['ok' => false, 'error' => 'WordPress HTTP API unavailable'];
--- a/klamra-paycal-for-aspaclaria/includes/Licensing/LicenseStore.php
+++ b/klamra-paycal-for-aspaclaria/includes/Licensing/LicenseStore.php
@@ -20,7 +20,11 @@
}
public static function license_key(): string {
- return (string) (self::get()['license_key'] ?? '');
+ $key = (string) (self::get()['license_key'] ?? '');
+ if (class_exists(LicenseClient::class) && LicenseClient::looks_like_connector_credential($key)) {
+ return '';
+ }
+ return $key;
}
public static function entitlements(): array {
--- a/klamra-paycal-for-aspaclaria/includes/Modules/Invoices/InvoicesModule.php
+++ b/klamra-paycal-for-aspaclaria/includes/Modules/Invoices/InvoicesModule.php
@@ -4,13 +4,9 @@
defined('ABSPATH') || exit;
use PayCalCorePlugin;
-use PayCalLicensingGate;
-
class InvoicesModule {
public static function init(): void {
if (!class_exists('WooCommerce')) return;
- // Gate by entitlement (invoices). If you want invoices always available, remove this check.
- if (!class_exists(Gate::class) || !Gate::has('invoices')) return;
if (!defined('PAYCAL_INV_PATH')) define('PAYCAL_INV_PATH', PAYCAL_PATH . 'includes/Modules/Invoices/Legacy/');
if (!defined('PAYCAL_INV_URL')) define('PAYCAL_INV_URL', PAYCAL_URL . 'includes/Modules/Invoices/Legacy/');
--- a/klamra-paycal-for-aspaclaria/includes/Modules/Invoices/Legacy/admin/settings.php
+++ b/klamra-paycal-for-aspaclaria/includes/Modules/Invoices/Legacy/admin/settings.php
@@ -1,23 +1,31 @@
<?php
if (!defined('ABSPATH')) exit;
+function paycal_invoices_admin_can_access(): bool {
+ return current_user_can('manage_woocommerce')
+ || current_user_can('edit_shop_orders')
+ || current_user_can('manage_options');
+}
+
add_action('admin_menu', function () {
- // If PayCal Core menu exists, attach there. Otherwise attach under WooCommerce.
- $parent = 'woocommerce';
- if (isset($GLOBALS['menu'])) {
- foreach ($GLOBALS['menu'] as $m) {
- if (!empty($m[2]) && $m[2] === 'paycal-dashboard') { $parent = 'paycal-dashboard'; break; }
+ $parent = 'woocommerce';
+ if (isset($GLOBALS['menu'])) {
+ foreach ($GLOBALS['menu'] as $m) {
+ if (!empty($m[2]) && $m[2] === 'paycal-dashboard') {
+ $parent = 'paycal-dashboard';
+ break;
}
}
+ }
- add_submenu_page(
- $parent,
- 'PayCal חשבוניות',
- 'חשבוניות',
- 'manage_options',
- 'paycal-invoices-settings',
- 'paycal_invoices_settings_page'
- );
+ add_submenu_page(
+ $parent,
+ 'PayCal Invoices',
+ 'Invoices',
+ 'read',
+ 'paycal-invoices-settings',
+ 'paycal_invoices_settings_page'
+ );
});
add_action('admin_enqueue_scripts', function($hook){
@@ -30,13 +38,20 @@
});
function paycal_invoices_settings_page() {
- if (!current_user_can('manage_options')) return;
+ if (!paycal_invoices_admin_can_access()) {
+ wp_die(
+ esc_html__('You do not have permission to manage PayCal invoices.', 'klamra-paycal-for-aspaclaria'),
+ esc_html__('Permission denied', 'klamra-paycal-for-aspaclaria'),
+ ['response' => 403]
+ );
+ }
$saved = false;
if (!empty($_POST['paycal_inv_action']) && $_POST['paycal_inv_action'] === 'save') {
check_admin_referer('paycal_inv_save');
$s = [
+ 'enabled' => !empty($_POST['enabled']) && $_POST['enabled'] === 'yes' ? 'yes' : 'no',
'layout' => sanitize_key($_POST['layout'] ?? 'modern'),
'primary_color' => sanitize_hex_color($_POST['primary_color'] ?? '#1366D6') ?: '#1366D6',
'accent_color' => sanitize_hex_color($_POST['accent_color'] ?? '#23C5F2') ?: '#23C5F2',
@@ -49,6 +64,10 @@
'trigger_status' => in_array(($_POST['trigger_status'] ?? 'completed'), ['processing','completed'], true) ? $_POST['trigger_status'] : 'completed',
'advanced_mode' => !empty($_POST['advanced_mode']) ? 1 : 0,
'custom_html' => wp_kses_post(wp_unslash($_POST['custom_html'] ?? '')),
+ 'israel_allocation_threshold_2025' => max(0, (float)($_POST['israel_allocation_threshold_2025'] ?? 20000)),
+ 'israel_allocation_threshold_2026_h1' => max(0, (float)($_POST['israel_allocation_threshold_2026_h1'] ?? 10000)),
+ 'israel_allocation_threshold_current' => max(0, (float)($_POST['israel_allocation_threshold_current'] ?? 5000)),
+ 'israel_allocation_current_from' => sanitize_text_field($_POST['israel_allocation_current_from'] ?? '2026-06-01'),
];
update_option('paycal_invoice_settings', $s, false);
$saved = true;
@@ -57,66 +76,78 @@
$s = paycal_invoice_get_settings();
echo '<div class="wrap paycal-inv-wrap">';
- echo '<div class="paycal-inv-hero"><h1>PayCal — חשבוניות</h1><p>עיצוב חשבונית נגיש ויפה, בלי ידע בעיצוב.</p></div>';
+ echo '<div class="paycal-inv-hero"><h1>PayCal Invoices</h1><p>Invoice settings for WooCommerce orders.</p></div>';
- if ($saved) echo '<div class="notice notice-success"><p>נשמר ✅</p></div>';
+ if ($saved) echo '<div class="notice notice-success"><p>Settings saved.</p></div>';
echo '<form method="post">';
wp_nonce_field('paycal_inv_save');
echo '<input type="hidden" name="paycal_inv_action" value="save" />';
echo '<div class="paycal-inv-grid">';
-
- // Left: settings
echo '<div class="paycal-inv-card">';
- echo '<h2>כללי</h2>';
+ echo '<h2>General</h2>';
+
+ echo '<div class="paycal-field"><label>Enable PayCal invoices</label>';
+ echo '<select name="enabled"><option value="yes" '.selected($s['enabled'] ?? 'yes','yes',false).'>yes</option><option value="no" '.selected($s['enabled'] ?? 'yes','no',false).'>no</option></select>';
+ echo '<p class="description">When disabled, invoices are not generated automatically, but administrators can still view existing invoices.</p>';
+ echo '</div>';
- echo '<div class="paycal-field"><label>שם העסק</label><input type="text" name="business_name" value="'.esc_attr($s['business_name']).'"></div>';
+ echo '<div class="paycal-field"><label>Business name</label><input type="text" name="business_name" value="'.esc_attr($s['business_name']).'"></div>';
- echo '<div class="paycal-field"><label>פרטי עסק (כתובת/ח.פ/טלפון)</label>';
+ echo '<div class="paycal-field"><label>Business details</label>';
wp_editor($s['business_details'], 'business_details', ['textarea_name'=>'business_details', 'textarea_rows'=>4, 'media_buttons'=>false]);
echo '</div>';
echo '<div class="paycal-row2">';
- echo '<div class="paycal-field"><label>צבע ראשי</label><input class="paycal-color" type="text" name="primary_color" value="'.esc_attr($s['primary_color']).'"></div>';
- echo '<div class="paycal-field"><label>צבע משני</label><input class="paycal-color" type="text" name="accent_color" value="'.esc_attr($s['accent_color']).'"></div>';
+ echo '<div class="paycal-field"><label>Primary color</label><input class="paycal-color" type="text" name="primary_color" value="'.esc_attr($s['primary_color']).'"></div>';
+ echo '<div class="paycal-field"><label>Accent color</label><input class="paycal-color" type="text" name="accent_color" value="'.esc_attr($s['accent_color']).'"></div>';
echo '</div>';
- // Logo
$logo_url = $s['logo_id'] ? wp_get_attachment_image_url((int)$s['logo_id'], 'medium') : '';
- echo '<div class="paycal-field"><label>לוגו</label>';
+ echo '<div class="paycal-field"><label>Logo</label>';
echo '<div class="paycal-logo-row">';
echo '<input type="hidden" id="paycal_logo_id" name="logo_id" value="'.esc_attr((string)$s['logo_id']).'">';
- echo '<button type="button" class="button" id="paycal_logo_pick">בחר לוגו</button>';
- echo '<button type="button" class="button" id="paycal_logo_clear">נקה</button>';
+ echo '<button type="button" class="button" id="paycal_logo_pick">Choose logo</button>';
+ echo '<button type="button" class="button" id="paycal_logo_clear">Clear</button>';
echo '</div>';
- echo '<div class="paycal-logo-preview">'.($logo_url ? '<img src="'.esc_url($logo_url).'" alt="logo" />' : '<span class="paycal-muted">אין לוגו</span>').'</div>';
+ echo '<div class="paycal-logo-preview">'.($logo_url ? '<img src="'.esc_url($logo_url).'" alt="logo" />' : '<span class="paycal-muted">No logo selected</span>').'</div>';
echo '</div>';
- echo '<div class="paycal-field"><label>מתי ליצור חשבונית אוטומטית?</label>';
+ echo '<div class="paycal-field"><label>Automatic invoice trigger</label>';
echo '<select name="trigger_status"><option value="processing" '.selected($s['trigger_status'],'processing',false).'>Processing</option><option value="completed" '.selected($s['trigger_status'],'completed',false).'>Completed</option></select>';
echo '</div>';
echo '<div class="paycal-row2">';
- echo '<label class="paycal-check"><input type="checkbox" name="show_sku" '.checked((int)$s['show_sku'],1,false).'> הצג SKU</label>';
- echo '<label class="paycal-check"><input type="checkbox" name="show_notes" '.checked((int)$s['show_notes'],1,false).'> הצג הערת לקוח</label>';
+ echo '<label class="paycal-check"><input type="checkbox" name="show_sku" '.checked((int)$s['show_sku'],1,false).'> Show SKU</label>';
+ echo '<label class="paycal-check"><input type="checkbox" name="show_notes" '.checked((int)$s['show_notes'],1,false).'> Show customer note</label>';
echo '</div>';
echo '<h2 style="margin-top:18px">Footer</h2>';
wp_editor($s['footer_html'], 'footer_html', ['textarea_name'=>'footer_html', 'textarea_rows'=>3, 'media_buttons'=>false]);
- echo '<h2 style="margin-top:18px">מצב מתקדם (למתקדמים בלבד)</h2>';
- echo '<label class="paycal-check"><input type="checkbox" name="advanced_mode" '.checked((int)$s['advanced_mode'],1,false).'> הפעל HTML מותאם</label>';
- echo '<p class="paycal-muted">במצב מתקדם אפשר לכתוב HTML ולהשתמש בפלייסהולדרים כמו {{order_number}}, {{total}}.</p>';
+ echo '<h2 style="margin-top:18px">Advanced custom HTML</h2>';
+ echo '<label class="paycal-check"><input type="checkbox" name="advanced_mode" '.checked((int)$s['advanced_mode'],1,false).'> Enable custom HTML</label>';
+ echo '<p class="paycal-muted">Available placeholders include {{order_number}}, {{total}}, {{billing_name}}, and {{invoice_number}}.</p>';
echo '<textarea name="custom_html" rows="10" style="width:100%;font-family:ui-monospace,Menlo,monospace" placeholder="Advanced custom HTML...">'.esc_textarea($s['custom_html']).'</textarea>';
- echo '<p class="submit"><button class="button button-primary">שמור</button></p>';
+ echo '<h2 style="margin-top:18px">Israel Invoices allocation thresholds</h2>';
+ echo '<p class="paycal-muted">Thresholds are configured before VAT in ILS and can be updated when legal requirements change.</p>';
+ echo '<div class="paycal-row2">';
+ echo '<div class="paycal-field"><label>2025 threshold before VAT</label><input type="number" min="0" step="0.01" name="israel_allocation_threshold_2025" value="'.esc_attr((string)($s['israel_allocation_threshold_2025'] ?? 20000)).'"></div>';
+ echo '<div class="paycal-field"><label>2026 Jan-May threshold before VAT</label><input type="number" min="0" step="0.01" name="israel_allocation_threshold_2026_h1" value="'.esc_attr((string)($s['israel_allocation_threshold_2026_h1'] ?? 10000)).'"></div>';
+ echo '</div>';
+ echo '<div class="paycal-row2">';
+ echo '<div class="paycal-field"><label>Current threshold before VAT</label><input type="number" min="0" step="0.01" name="israel_allocation_threshold_current" value="'.esc_attr((string)($s['israel_allocation_threshold_current'] ?? 5000)).'"></div>';
+ echo '<div class="paycal-field"><label>Current threshold starts</label><input type="date" name="israel_allocation_current_from" value="'.esc_attr((string)($s['israel_allocation_current_from'] ?? '2026-06-01')).'"></div>';
+ echo '</div>';
+
+ echo '<p class="submit"><button class="button button-primary">Save settings</button></p>';
echo '</div>';
- // Right: preview
echo '<div class="paycal-inv-card">';
- echo '<h2>תצוגה מקדימה</h2>';
- echo '<p class="paycal-muted">התצוגה משתמשת בהזמנה האחרונה באתר (אם קיימת), או בדאטה לדוגמה.</p>';
+ echo '<h2>Preview</h2>';
+ echo '<p class="paycal-muted">The preview uses the latest WooCommerce order when one exists.</p>';
$sample_order_id = 0;
if (class_exists('WooCommerce')) {
@@ -125,18 +156,15 @@
}
if ($sample_order_id) {
- // Create temporary HTML preview from order (no invoice needed)
- $fake_invoice_id = 0;
- $html = paycal_invoice_render_html($fake_invoice_id, $sample_order_id);
+ $html = paycal_invoice_render_html(0, $sample_order_id);
} else {
- $html = '<div dir="rtl" style="font-family:Arial;padding:16px;border:1px dashed #E3ECFA;border-radius:14px;background:#F5F9FF">אין הזמנות עדיין לתצוגה. אחרי שתיווצר הזמנה, תופיע כאן תצוגה אמיתית.</div>';
+ $html = '<div dir="rtl" style="font-family:Arial;padding:16px;border:1px dashed #E3ECFA;border-radius:14px;background:#F5F9FF">No orders yet. A real preview will appear after an order is created.</div>';
}
echo '<div class="paycal-preview">'.$html.'</div>';
+ echo '</div>';
- echo '</div>'; // right
-
- echo '</div>'; // grid
+ echo '</div>';
echo '</form>';
echo '</div>';
}
--- a/klamra-paycal-for-aspaclaria/includes/Modules/Invoices/Legacy/includes/cpt.php
+++ b/klamra-paycal-for-aspaclaria/includes/Modules/Invoices/Legacy/includes/cpt.php
@@ -2,32 +2,168 @@
if (!defined('ABSPATH')) exit;
add_action('init', function () {
- register_post_type('asp_invoice', [
- 'label' => 'חשבוניות',
- 'public' => false,
- 'show_ui' => true,
- 'menu_icon' => 'dashicons-media-text',
- 'supports' => ['title'],
- ]);
+ register_post_type('asp_invoice', [
+ 'label' => 'PayCal Invoices',
+ 'public' => false,
+ 'show_ui' => true,
+ 'menu_icon' => 'dashicons-media-text',
+ 'supports' => ['title'],
+ ]);
});
+function paycal_invoice_admin_download_url(int $invoice_id): string {
+ return wp_nonce_url(
+ admin_url('admin-post.php?action=paycal_invoice_download&invoice_id=' . absint($invoice_id)),
+ 'paycal_invoice_download_' . absint($invoice_id)
+ );
+}
+
add_filter('manage_asp_invoice_posts_columns', function($cols){
- $cols['order'] = 'הזמנה';
- $cols['download'] = 'הורדה';
+ $cols['order'] = __('Order', 'klamra-paycal-for-aspaclaria');
+ $cols['allocation'] = __('Allocation', 'klamra-paycal-for-aspaclaria');
+ $cols['download'] = __('Download', 'klamra-paycal-for-aspaclaria');
return $cols;
});
add_action('manage_asp_invoice_posts_custom_column', function($col, $post_id){
+ $invoice_id = absint($post_id);
+
if ($col === 'order') {
- $oid = (int)get_post_meta($post_id, '_order_id', true);
+ $oid = absint(get_post_meta($invoice_id, '_order_id', true));
if ($oid) {
echo '<a href="'.esc_url(admin_url('post.php?post='.$oid.'&action=edit')).'">#'.esc_html((string)$oid).'</a>';
} else {
- echo '—';
+ echo '—';
}
}
+
+ if ($col === 'allocation') {
+ $oid = absint(get_post_meta($invoice_id, '_order_id', true));
+ $order = $oid && function_exists('wc_get_order') ? wc_get_order($oid) : null;
+ echo esc_html(function_exists('paycal_invoice_get_allocation_badge') ? paycal_invoice_get_allocation_badge($invoice_id, $order) : '');
+ }
+
if ($col === 'download') {
- $url = wp_nonce_url(admin_url('admin-post.php?action=paycal_invoice_download&invoice_id='.$post_id), 'paycal_inv_'.$post_id);
- echo '<a class="button" href="'.esc_url($url).'">HTML להדפסה</a>';
+ echo '<a class="button" href="'.esc_url(paycal_invoice_admin_download_url($invoice_id)).'">' . esc_html__('Download invoice', 'klamra-paycal-for-aspaclaria') . '</a>';
}
}, 10, 2);
+
+add_action('admin_notices', function () {
+ $screen = function_exists('get_current_screen') ? get_current_screen() : null;
+ if (!$screen || $screen->post_type !== 'asp_invoice' || $screen->base !== 'post') {
+ return;
+ }
+
+ echo '<div class="notice notice-info"><p>' . esc_html__('This invoice is generated from a WooCommerce order. Edit billing/order details from the linked WooCommerce order.', 'klamra-paycal-for-aspaclaria') . '</p></div>';
+});
+
+add_action('add_meta_boxes', function () {
+ add_meta_box(
+ 'paycal_invoice_order_box',
+ __('Linked WooCommerce order', 'klamra-paycal-for-aspaclaria'),
+ 'paycal_invoice_render_order_meta_box',
+ 'asp_invoice',
+ 'normal',
+ 'high'
+ );
+
+ add_meta_box(
+ 'paycal_invoice_allocation_box',
+ __('Israel Invoices allocation', 'klamra-paycal-for-aspaclaria'),
+ 'paycal_invoice_render_allocation_meta_box',
+ 'asp_invoice',
+ 'side',
+ 'default'
+ );
+});
+
+function paycal_invoice_render_order_meta_box(WP_Post $post): void {
+ $invoice_id = absint($post->ID);
+ $order_id = absint(get_post_meta($invoice_id, '_order_id', true));
+ $order = $order_id && function_exists('wc_get_order') ? wc_get_order($order_id) : null;
+
+ if (!$order) {
+ echo '<p>' . esc_html__('No linked WooCommerce order was found for this invoice.', 'klamra-paycal-for-aspaclaria') . '</p>';
+ return;
+ }
+
+ $customer_name = trim($order->get_billing_first_name() . ' ' . $order->get_billing_last_name());
+ $allocation_badge = function_exists('paycal_invoice_get_allocation_badge') ? paycal_invoice_get_allocation_badge($invoice_id, $order) : '';
+
+ echo '<table class="widefat striped"><tbody>';
+ echo '<tr><th>' . esc_html__('Order number', 'klamra-paycal-for-aspaclaria') . '</th><td>#' . esc_html((string) $order->get_order_number()) . '</td></tr>';
+ echo '<tr><th>' . esc_html__('Customer name', 'klamra-paycal-for-aspaclaria') . '</th><td>' . esc_html($customer_name ?: __('Guest customer', 'klamra-paycal-for-aspaclaria')) . '</td></tr>';
+ echo '<tr><th>' . esc_html__('Order total', 'klamra-paycal-for-aspaclaria') . '</th><td>' . wp_kses_post($order->get_formatted_order_total()) . '</td></tr>';
+ echo '<tr><th>' . esc_html__('Order status', 'klamra-paycal-for-aspaclaria') . '</th><td>' . esc_html(wc_get_order_status_name($order->get_status())) . '</td></tr>';
+ echo '<tr><th>' . esc_html__('Allocation status', 'klamra-paycal-for-aspaclaria') . '</th><td><strong>' . esc_html($allocation_badge) . '</strong></td></tr>';
+ echo '</tbody></table>';
+
+ echo '<p>';
+ echo '<a class="button button-primary" href="' . esc_url(admin_url('post.php?post=' . $order_id . '&action=edit')) . '">' . esc_html__('View order', 'klamra-paycal-for-aspaclaria') . '</a> ';
+ echo '<a class="button" href="' . esc_url(paycal_invoice_admin_download_url($invoice_id)) . '">' . esc_html__('Download invoice', 'klamra-paycal-for-aspaclaria') . '</a>';
+ echo '</p>';
+}
+
+function paycal_invoice_render_allocation_meta_box(WP_Post $post): void {
+ $invoice_id = absint($post->ID);
+ $order_id = absint(get_post_meta($invoice_id, '_order_id', true));
+ $order = $order_id && function_exists('wc_get_order') ? wc_get_order($order_id) : null;
+ $allocation_number = (string) get_post_meta($invoice_id, '_paycal_israel_allocation_number', true);
+ $allocation_required = (string) get_post_meta($invoice_id, '_paycal_israel_allocation_required', true);
+ if ($allocation_required === '' && $order && function_exists('paycal_invoice_is_allocation_required')) {
+ $allocation_required = paycal_invoice_is_allocation_required($order) ? 'yes' : 'no';
+ }
+
+ wp_nonce_field('paycal_invoice_allocation_save', 'paycal_invoice_allocation_nonce');
+ echo '<p><strong>' . esc_html(function_exists('paycal_invoice_get_allocation_badge') ? paycal_invoice_get_allocation_badge($invoice_id, $order) : '') . '</strong></p>';
+ if ($allocation_required === 'yes' && trim($allocation_number) === '') {
+ echo '<p class="notice notice-warning" style="padding:8px;margin:8px 0">' . esc_html__('Allocation number required. Invoice generation is not blocked.', 'klamra-paycal-for-aspaclaria') . '</p>';
+ }
+ echo '<p><label for="paycal_israel_allocation_number">' . esc_html__('Israel Invoices allocation number', 'klamra-paycal-for-aspaclaria') . '</label></p>';
+ echo '<input id="paycal_israel_allocation_number" name="paycal_israel_allocation_number" type="text" style="width:100%" value="' . esc_attr($allocation_number) . '" />';
+ echo '<p class="description">' . esc_html__('Required only for eligible tax invoices above the legal threshold. This field may be filled manually until automatic Tax Authority integration is implemented.', 'klamra-paycal-for-aspaclaria') . '</p>';
+}
+
+add_action('save_post_asp_invoice', function ($post_id) {
+ if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) {
+ return;
+ }
+ if (!current_user_can('manage_woocommerce') && !current_user_can('edit_shop_orders') && !current_user_can('manage_options')) {
+ return;
+ }
+ $nonce = sanitize_text_field(wp_unslash($_POST['paycal_invoice_allocation_nonce'] ?? ''));
+ if (!$nonce || !wp_verify_nonce($nonce, 'paycal_invoice_allocation_save')) {
+ return;
+ }
+
+ $allocation_number = sanitize_text_field(wp_unslash($_POST['paycal_israel_allocation_number'] ?? ''));
+ update_post_meta($post_id, '_paycal_israel_allocation_number', $allocation_number);
+
+ $order_id = absint(get_post_meta($post_id, '_order_id', true));
+ $order = $order_id && function_exists('wc_get_order') ? wc_get_order($order_id) : null;
+ $required = $order && function_exists('paycal_invoice_is_allocation_required') && paycal_invoice_is_allocation_required($order);
+ update_post_meta($post_id, '_paycal_israel_allocation_required', $required ? 'yes' : 'no');
+ update_post_meta($post_id, '_paycal_israel_allocation_status', $allocation_number !== '' ? 'added' : ($required ? 'missing' : 'not_required'));
+});
+
+add_action('woocommerce_admin_order_data_after_order_details', function ($order) {
+ if (!$order instanceof WC_Order) {
+ return;
+ }
+
+ $invoice_ids = get_posts([
+ 'post_type' => 'asp_invoice',
+ 'post_status' => 'publish',
+ 'meta_key' => '_order_id',
+ 'meta_value' => $order->get_id(),
+ 'fields' => 'ids',
+ 'posts_per_page' => 1,
+ ]);
+
+ $invoice_id = !empty($invoice_ids) ? absint($invoice_ids[0]) : 0;
+ $label = $invoice_id && function_exists('paycal_invoice_get_allocation_badge')
+ ? paycal_invoice_get_allocation_badge($invoice_id, $order)
+ : (function_exists('paycal_invoice_is_allocation_required') && paycal_invoice_is_allocation_required($order) ? 'Allocation number required' : 'Allocation number not required');
+
+ echo '<p class="form-field form-field-wide"><strong>' . esc_html__('PayCal Israel Invoices:', 'klamra-paycal-for-aspaclaria') . '</strong> ' . esc_html($label) . '</p>';
+});
--- a/klamra-paycal-for-aspaclaria/includes/Modules/Invoices/Legacy/includes/order-hook.php
+++ b/klamra-paycal-for-aspaclaria/includes/Modules/Invoices/Legacy/includes/order-hook.php
@@ -4,6 +4,9 @@
function paycal_invoice_trigger_status(): string {
$s = get_option('paycal_invoice_settings', []);
if (!is_array($s)) $s = [];
+