update
This commit is contained in:
@@ -0,0 +1,40 @@
|
||||
# AZA License Bridge & WP Desk Flexible Subscriptions
|
||||
|
||||
## Kontext
|
||||
|
||||
Auf der Produktivseite ist **WP Desk Flexible Subscriptions** aktiv, nicht das klassische Plugin **WooCommerce Subscriptions**.
|
||||
Die öffentliche Woo-REST-Route `/wp-json/wc/v3/subscriptions` kann fehlen oder durch Hosting/WAF blockiert werden – unabhängig davon liefert Flexible Subscriptions laut WP-Desk-Doku u. a.:
|
||||
|
||||
- `next_payment_date_gmt` (REST)
|
||||
- `billing_period` als **Einzelbuchstabe**: `D`, `W`, `M`, `Y`
|
||||
- `billing_interval` (Integer)
|
||||
|
||||
## Root Cause (Kurz)
|
||||
|
||||
1. **AZA-Hetzner** soll **keine** Abo-Perioden mehr ausschließlich über Shop-REST laden müssen.
|
||||
2. Die **WordPress-Bridge** kennt das Subscription-Objekt lokal und kann **next payment + billing** an den Server schicken.
|
||||
3. Backend (`wc_period_payload.py`) normalisiert **`M/D/W/Y` → month/day/week/year** für die Periodenberechnung.
|
||||
|
||||
## Endpunkte auf Hetzner (nach Deploy des Backend-Codes)
|
||||
|
||||
| Methode | Pfad | Auth |
|
||||
|--------|------|------|
|
||||
| POST | `/wc/provision` | `X-WC-Secret` = `WC_PROVISION_SECRET` |
|
||||
| POST | `/wc/subscription_period` | gleiches Secret |
|
||||
|
||||
`subscription_period` aktualisiert nur `current_period_start`, `current_period_end`, `updated_at` für Zeilen `subscription_id LIKE 'wc_sub_%'` und `status = active`.
|
||||
|
||||
## Plugin-Verhalten (Repo-Stand)
|
||||
|
||||
- `woocommerce_subscription_status_active` Priorität **10**: Lizenz-Provisioning (wie bisher).
|
||||
- Dieselbe Action Priorität **20**: `aza_lb_after_subscription_active_push_period` → POST `/wc/subscription_period`.
|
||||
|
||||
Flexible Subscriptions setzt in der Regel dieselbe Hook-Familie voraus (WCS-kompatibel); wenn der Hook auf einer Installation **nicht** feuert, bitte WP-Desk-Doku oder Support zu den **Status-Wechsel-Hooks** prüfen und ggf. einen weiteren Hook ergänzen (ohne AZA-Core zu ändern).
|
||||
|
||||
## Logs
|
||||
|
||||
`wp-content/uploads/aza-logs/license-bridge.log` — Einträge `PERIOD PUSH …` ohne Klartext-Secrets.
|
||||
|
||||
## Snippet
|
||||
|
||||
Zusätzlich: `snippets/phase1f-period-endpoint-snippet.php` (Referenz für Code-Snippets-Plugin).
|
||||
@@ -2,7 +2,7 @@
|
||||
/**
|
||||
* Plugin Name: AZA License Bridge
|
||||
* Description: Verknüpft WooCommerce-Abo-Käufe mit dem AZA Hetzner-Lizenz-Backend.
|
||||
* Version: 1.0.1
|
||||
* Version: 1.0.2
|
||||
* Author: AZA MedWork
|
||||
* Requires PHP: 7.4
|
||||
* Requires at least: 5.8
|
||||
@@ -171,6 +171,133 @@ function aza_lb_get_billing_email_from_subscription( $subscription ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// ── Phase 1f: Abrechnungsperiode → Hetzner (ohne Woo /wc/v3/subscriptions von AZA aus) ──
|
||||
|
||||
/**
|
||||
* Liest nächste Zahlung / Billing aus Subscription-Objekt oder Post-Meta.
|
||||
* Kompatibel mit WooCommerce-Subscriptions-APIs und WP Desk Flexible Subscriptions (u. a. billing_period M/D/W/Y).
|
||||
*
|
||||
* @param object $subscription
|
||||
* @return array Teilmenge für POST /wc/subscription_period
|
||||
*/
|
||||
function aza_lb_collect_period_fields_from_subscription( $subscription ) {
|
||||
$fields = array();
|
||||
if ( ! is_object( $subscription ) || ! method_exists( $subscription, 'get_id' ) ) {
|
||||
return $fields;
|
||||
}
|
||||
$sid = (int) $subscription->get_id();
|
||||
if ( $sid <= 0 ) {
|
||||
return $fields;
|
||||
}
|
||||
|
||||
if ( method_exists( $subscription, 'get_date' ) ) {
|
||||
$next = $subscription->get_date( 'next_payment' );
|
||||
if ( is_numeric( $next ) ) {
|
||||
$fields['next_payment_date'] = (int) $next;
|
||||
} elseif ( is_string( $next ) && $next !== '' && $next !== '0' ) {
|
||||
$fields['next_payment_date'] = $next;
|
||||
}
|
||||
}
|
||||
|
||||
if ( empty( $fields['next_payment_date'] ) ) {
|
||||
$meta_keys = array( '_schedule_next_payment', 'wps_next_payment_date' );
|
||||
foreach ( $meta_keys as $meta_key ) {
|
||||
$raw = get_post_meta( $sid, $meta_key, true );
|
||||
if ( is_string( $raw ) && $raw !== '' ) {
|
||||
$fields['next_payment_date'] = $raw;
|
||||
break;
|
||||
}
|
||||
if ( is_numeric( $raw ) && (int) $raw > 0 ) {
|
||||
$fields['next_payment_date'] = (int) $raw;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ( method_exists( $subscription, 'get_billing_period' ) ) {
|
||||
$fields['billing_period'] = (string) $subscription->get_billing_period();
|
||||
} else {
|
||||
$bp = get_post_meta( $sid, '_billing_period', true );
|
||||
if ( $bp !== '' && $bp !== null ) {
|
||||
$fields['billing_period'] = (string) $bp;
|
||||
}
|
||||
}
|
||||
|
||||
if ( method_exists( $subscription, 'get_billing_interval' ) ) {
|
||||
$fields['billing_interval'] = (int) $subscription->get_billing_interval();
|
||||
} else {
|
||||
$bi = get_post_meta( $sid, '_billing_interval', true );
|
||||
if ( $bi !== '' && $bi !== null ) {
|
||||
$fields['billing_interval'] = (int) $bi;
|
||||
}
|
||||
}
|
||||
|
||||
if ( method_exists( $subscription, 'get_status' ) ) {
|
||||
$fields['subscription_status'] = (string) $subscription->get_status();
|
||||
}
|
||||
|
||||
return $fields;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sendet nur Periodenfelder an Hetzner (POST /wc/subscription_period, X-WC-Secret).
|
||||
*
|
||||
* @param object $subscription
|
||||
* @return void
|
||||
*/
|
||||
function aza_lb_push_subscription_period_to_backend( $subscription ) {
|
||||
$api_url = rtrim( (string) get_option( 'aza_lb_api_url', 'https://api.aza-medwork.ch' ), '/' );
|
||||
$wc_secret = (string) get_option( 'aza_lb_wc_secret', '' );
|
||||
if ( $wc_secret === '' || ! is_object( $subscription ) || ! method_exists( $subscription, 'get_id' ) ) {
|
||||
return;
|
||||
}
|
||||
$sub_numeric = (int) $subscription->get_id();
|
||||
if ( $sub_numeric <= 0 ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$order_id = 0;
|
||||
if ( function_exists( 'aza_lb_get_parent_order_id_from_subscription' ) ) {
|
||||
$order_id = (int) aza_lb_get_parent_order_id_from_subscription( $subscription );
|
||||
}
|
||||
|
||||
$period = aza_lb_collect_period_fields_from_subscription( $subscription );
|
||||
if ( empty( $period['next_payment_date'] ) ) {
|
||||
aza_lb_log( "PERIOD PUSH SKIP: sub={$sub_numeric} — kein next_payment ermittelbar" );
|
||||
return;
|
||||
}
|
||||
|
||||
$payload = array_merge(
|
||||
array(
|
||||
'wc_subscription_id' => $sub_numeric,
|
||||
'wc_order_id' => $order_id,
|
||||
),
|
||||
$period
|
||||
);
|
||||
|
||||
$url = $api_url . '/wc/subscription_period';
|
||||
$response = wp_remote_post(
|
||||
$url,
|
||||
array(
|
||||
'timeout' => 20,
|
||||
'headers' => array(
|
||||
'Content-Type' => 'application/json',
|
||||
'X-WC-Secret' => $wc_secret,
|
||||
'User-Agent' => 'AZA-License-Bridge/1.0-flex-period',
|
||||
),
|
||||
'body' => wp_json_encode( $payload ),
|
||||
)
|
||||
);
|
||||
|
||||
if ( is_wp_error( $response ) ) {
|
||||
aza_lb_log( 'PERIOD PUSH ERR: ' . $response->get_error_message() );
|
||||
return;
|
||||
}
|
||||
|
||||
$code = (int) wp_remote_retrieve_response_code( $response );
|
||||
aza_lb_log( "PERIOD PUSH sub={$sub_numeric} HTTP {$code}" );
|
||||
}
|
||||
|
||||
// ── Provisioning Request ──────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
@@ -326,6 +453,97 @@ function aza_lb_on_subscription_active( $subscription ) {
|
||||
aza_lb_log( "PROVISION OK: sub={$sub_id}, order={$order_id}, key={$license_key}, status={$status}" );
|
||||
}
|
||||
|
||||
/**
|
||||
* Nach Provision (oder bei reaktivem „active“): Perioden an Hetzner senden.
|
||||
* Läuft mit Priorität 20 nach aza_lb_on_subscription_active (10).
|
||||
*/
|
||||
add_action( 'woocommerce_subscription_status_active', 'aza_lb_after_subscription_active_push_period', 20, 1 );
|
||||
|
||||
function aza_lb_after_subscription_active_push_period( $subscription ) {
|
||||
aza_lb_push_subscription_period_to_backend( $subscription );
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscription-Objekt laden (WCS oder Flexible Subscriptions / shop_subscription Order).
|
||||
*
|
||||
* @param int $sub_id
|
||||
* @return object|null
|
||||
*/
|
||||
function aza_lb_get_subscription_object( $sub_id ) {
|
||||
$sub_id = (int) $sub_id;
|
||||
if ( $sub_id <= 0 ) {
|
||||
return null;
|
||||
}
|
||||
if ( function_exists( 'wcs_get_subscription' ) ) {
|
||||
$sub = wcs_get_subscription( $sub_id );
|
||||
if ( $sub ) {
|
||||
return $sub;
|
||||
}
|
||||
}
|
||||
if ( function_exists( 'wc_get_order' ) ) {
|
||||
$order = wc_get_order( $sub_id );
|
||||
if ( $order && method_exists( $order, 'get_type' ) ) {
|
||||
$type = (string) $order->get_type();
|
||||
if ( $type === 'shop_subscription' || $type === 'subscription' ) {
|
||||
return $order;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// ── Admin: manueller Perioden-Push (keine Zahlung, nur Hetzner-Sync) ─────────
|
||||
|
||||
add_action( 'admin_menu', function () {
|
||||
add_management_page(
|
||||
'AZA Period Sync',
|
||||
'AZA Period Sync',
|
||||
'manage_options',
|
||||
'aza-lb-period-sync',
|
||||
'aza_lb_period_sync_admin_page'
|
||||
);
|
||||
} );
|
||||
|
||||
function aza_lb_period_sync_admin_page() {
|
||||
if ( ! current_user_can( 'manage_options' ) ) {
|
||||
return;
|
||||
}
|
||||
$notice = '';
|
||||
if ( isset( $_POST['aza_lb_period_sync_nonce'] ) && wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['aza_lb_period_sync_nonce'] ) ), 'aza_lb_period_sync' ) ) {
|
||||
$sub_id = isset( $_POST['wc_subscription_id'] ) ? (int) $_POST['wc_subscription_id'] : 0;
|
||||
$sub = aza_lb_get_subscription_object( $sub_id );
|
||||
if ( ! $sub ) {
|
||||
$notice = 'Subscription nicht gefunden oder kein Abo-Typ.';
|
||||
} else {
|
||||
$result = aza_lb_push_subscription_period_to_backend( $sub );
|
||||
if ( is_wp_error( $result ) ) {
|
||||
$notice = 'Fehler: ' . esc_html( $result->get_error_message() );
|
||||
} else {
|
||||
$notice = 'Perioden-Push ausgelöst (sub=' . (int) $sub_id . '). Log: uploads/aza-logs/license-bridge.log';
|
||||
}
|
||||
}
|
||||
}
|
||||
?>
|
||||
<div class="wrap">
|
||||
<h1>AZA Period Sync (Hetzner)</h1>
|
||||
<p>Sendet nur <code>current_period_*</code> an <code>POST /wc/subscription_period</code>. Keine Zahlung, keine Kundenänderung.</p>
|
||||
<?php if ( $notice !== '' ) : ?>
|
||||
<div class="notice notice-info"><p><?php echo esc_html( $notice ); ?></p></div>
|
||||
<?php endif; ?>
|
||||
<form method="post">
|
||||
<?php wp_nonce_field( 'aza_lb_period_sync', 'aza_lb_period_sync_nonce' ); ?>
|
||||
<table class="form-table">
|
||||
<tr>
|
||||
<th scope="row"><label for="wc_subscription_id">Woo Subscription ID</label></th>
|
||||
<td><input type="number" name="wc_subscription_id" id="wc_subscription_id" min="1" required class="regular-text" /></td>
|
||||
</tr>
|
||||
</table>
|
||||
<?php submit_button( 'Perioden an Hetzner senden' ); ?>
|
||||
</form>
|
||||
</div>
|
||||
<?php
|
||||
}
|
||||
|
||||
// ── Admin-Anzeige ─────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
<?php
|
||||
/**
|
||||
* AZA Phase 1f – Woo Subscription Perioden → Hetzner (OHNE Woo /wc/v3/subscriptions REST)
|
||||
*
|
||||
* Nicht automatisch aktivieren: Inhalt z. B. per „Code Snippets“ einbinden oder ins
|
||||
* Plugin `aza-license-bridge` übernehmen. Keine Secrets in Theme-Dateien.
|
||||
*
|
||||
* Voraussetzungen:
|
||||
* - gleiche Optionen wie License Bridge: aza_lb_api_url, aza_lb_wc_secret
|
||||
* - WC_PROVISION_SECRET auf dem Server = aza_lb_wc_secret
|
||||
*
|
||||
* Endpunkte:
|
||||
* - POST {api}/wc/subscription_period
|
||||
* Header: X-WC-Secret, Content-Type: application/json
|
||||
* Body: siehe aza_lb_send_subscription_period()
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sendet aktuelle Abrechnungsperiode für eine WooCommerce-Subscription-ID an AZA.
|
||||
*
|
||||
* @param WC_Subscription|object $subscription Subscription-Objekt (WCS).
|
||||
* @return array|WP_Error
|
||||
*/
|
||||
function aza_lb_send_subscription_period( $subscription ) {
|
||||
if ( ! is_object( $subscription ) || ! method_exists( $subscription, 'get_id' ) ) {
|
||||
return new WP_Error( 'aza_lb_invalid_sub', 'Kein Subscription-Objekt.' );
|
||||
}
|
||||
|
||||
$sub_numeric = (int) $subscription->get_id();
|
||||
if ( $sub_numeric <= 0 ) {
|
||||
return new WP_Error( 'aza_lb_bad_sub_id', 'Ungültige Subscription-ID.' );
|
||||
}
|
||||
|
||||
$api_url = rtrim( (string) get_option( 'aza_lb_api_url', 'https://api.aza-medwork.ch' ), '/' );
|
||||
$wc_secret = (string) get_option( 'aza_lb_wc_secret', '' );
|
||||
if ( $wc_secret === '' ) {
|
||||
return new WP_Error( 'aza_lb_no_secret', 'WC Provision Secret nicht konfiguriert.' );
|
||||
}
|
||||
|
||||
$order_id = 0;
|
||||
if ( function_exists( 'aza_lb_get_parent_order_id_from_subscription' ) ) {
|
||||
$order_id = (int) aza_lb_get_parent_order_id_from_subscription( $subscription );
|
||||
} elseif ( method_exists( $subscription, 'get_parent_id' ) ) {
|
||||
$order_id = (int) $subscription->get_parent_id();
|
||||
}
|
||||
|
||||
$payload = array(
|
||||
'wc_subscription_id' => $sub_numeric,
|
||||
'wc_order_id' => $order_id,
|
||||
);
|
||||
|
||||
// WooCommerce Subscriptions: nächste Zahlung & Abrechnungsrhythmus
|
||||
if ( method_exists( $subscription, 'get_date' ) ) {
|
||||
$next = $subscription->get_date( 'next_payment' );
|
||||
if ( is_string( $next ) && $next !== '' ) {
|
||||
$payload['next_payment_date'] = $next;
|
||||
}
|
||||
}
|
||||
|
||||
if ( method_exists( $subscription, 'get_billing_period' ) ) {
|
||||
$payload['billing_period'] = (string) $subscription->get_billing_period();
|
||||
}
|
||||
if ( method_exists( $subscription, 'get_billing_interval' ) ) {
|
||||
$payload['billing_interval'] = (int) $subscription->get_billing_interval();
|
||||
}
|
||||
|
||||
if ( method_exists( $subscription, 'get_status' ) ) {
|
||||
$payload['subscription_status'] = (string) $subscription->get_status();
|
||||
}
|
||||
|
||||
$url = $api_url . '/wc/subscription_period';
|
||||
|
||||
$response = wp_remote_post(
|
||||
$url,
|
||||
array(
|
||||
'timeout' => 20,
|
||||
'headers' => array(
|
||||
'Content-Type' => 'application/json',
|
||||
'X-WC-Secret' => $wc_secret,
|
||||
'User-Agent' => 'AZA-License-Bridge-Period/1.0',
|
||||
),
|
||||
'body' => wp_json_encode( $payload ),
|
||||
)
|
||||
);
|
||||
|
||||
if ( is_wp_error( $response ) ) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
$code = (int) wp_remote_retrieve_response_code( $response );
|
||||
|
||||
if ( $code >= 200 && $code < 300 ) {
|
||||
$data = json_decode( (string) wp_remote_retrieve_body( $response ), true );
|
||||
return is_array( $data ) ? $data : array( 'ok' => true );
|
||||
}
|
||||
|
||||
return new WP_Error( 'aza_lb_period_failed', 'HTTP ' . $code );
|
||||
}
|
||||
|
||||
/**
|
||||
* Beispiel-Hook: nach Zahlung Perioden pushen.
|
||||
* Bei Bedarf Cron oder Admin-Aktion ergänzen.
|
||||
*/
|
||||
add_action(
|
||||
'woocommerce_subscription_payment_complete',
|
||||
function ( $subscription ) {
|
||||
if ( function_exists( 'aza_lb_send_subscription_period' ) ) {
|
||||
aza_lb_send_subscription_period( $subscription );
|
||||
}
|
||||
},
|
||||
10,
|
||||
1
|
||||
);
|
||||
Reference in New Issue
Block a user