Claude Skill

lw-elallas-integration

Integrate with or extend "Elállás for WooCommerce"

LLM Mart · 0 points · 0 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download lonsdale201-wp-agent-skills-lw-plugins_lw-elallas-integration-52f6020.zip · 11 KB
Part of lonsdale201/wp-agent-skills — 226 skills

Install

skills CLI npx skills add https://github.com/Lonsdale201/wp-agent-skills/tree/main/lw-plugins/lw-elallas-integration
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install lonsdale201-wp-agent-skills@llmmart
Git git clone https://github.com/Lonsdale201/wp-agent-skills.git

The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole lonsdale201/wp-agent-skills collection as a plugin from our marketplace. Git is the plain clone.

Skill manifest

Elállás for WooCommerce — integration & extension

For developers making a plugin/theme work with Elállás for WooCommerce (elallas-for-woo, by uptools.io) — the online right-of-withdrawal (elállási jog) button + case-management plugin for WooCommerce, built for EU Directive 2023/2673 and 415/2025. (XII. 23.) Korm. rendelet. It manages a withdrawal "case" per order in its own tables and fires a small, clean set of elallas_* hooks for integrators.

This is an LW-family plugin — consume hooks, don't edit it

Namespace LightweightPlugins\Elallas, tables wp_lw_elallas_*, and it integrates with LW Site Manager — it's part of the LightweightPlugins family. Integrate by consuming its elallas_* hooks and reading its data; never patch the plugin. If you need an extension point that doesn't exist, request it upstream rather than editing.

Detect it:

if ( defined( 'ELALLAS_FOR_WOO_VERSION' ) ) { /* Elállás for WooCommerce active (1.0.12 tested) */ }

It requires WooCommerce 8.0+ and is HPOS-safe — every hook hands you a real \WC_Order, so use $order->get_*() (never post meta) — see wc-hpos-compatibility.

Extension point 1 — eligibility, deadline, B2B & order numbers

The first four filters decide whether an order may start a withdrawal and how the deadline is computed; they receive the \WC_Order. elallas_resolve_order_number runs earlier, before the order exists, and maps the customer-entered display order number to a WooCommerce order ID.

// Final say on eligibility (runs after the built-in checks).
add_filter( 'elallas_is_order_eligible', function ( bool $eligible, \WC_Order $order ): bool {
    // e.g. never allow withdrawal for a "digital-only" order:
    return $eligible && ! my_order_is_digital_only( $order );
}, 10, 2 );

// Override the withdrawal window (default: option 'deadline_days', 14).
add_filter( 'elallas_deadline_days', function ( int $days, \WC_Order $order ): int {
    return my_is_extended_returns_member( $order ) ? 30 : $days;
}, 10, 2 );

// Override the B2B/B2C heuristic (default: true if company name OR VAT number is set).
add_filter( 'elallas_is_order_b2b', function ( bool $is_b2b, \WC_Order $order ): bool {
    return $is_b2b || my_customer_is_business( $order->get_customer_id() );
}, 10, 2 );

// Override the delivery date used as a deadline basis.
add_filter( 'elallas_delivery_date', function ( $delivery, \WC_Order $order ) {
    return my_carrier_delivered_at( $order ) ?: $delivery;
}, 10, 2 );

// Resolve a customer-entered/display order number from another numbering plugin.
add_filter( 'elallas_resolve_order_number', function ( int $order_id, string $number ): int {
    if ( $order_id > 0 ) {
        return $order_id;
    }

    return my_numbering_plugin_find_order_id( $number ) ?: 0; // 0 = let Elállás fall through.
}, 10, 2 );

Verified: elallas_is_order_eligible at EligibilityChecker.php:67; elallas_deadline_days at :119 (default from Options::get('deadline_days', 14)); elallas_is_order_b2b at B2BDetector.php:40 (heuristic: company OR VAT filled); elallas_delivery_date at OrderAdapter.php:166; elallas_resolve_order_number at OrderAdapter.php:65.

Good to know about the built-in eligibility (so your filter composes correctly): by default only orders whose status is in eligible_statuses (default ['processing','completed']) qualify; an order with an already-open case is refused; logged-in users may only act on their own orders (guests fall back to an email match); and an expired deadline does NOT hard-block by default — it's flagged for manual review unless the expired_handling option is set to 'block' (EligibilityChecker.php:32-72). Your elallas_is_order_eligible filter is the final gate, so returning true can re-allow something the built-in checks denied — return $eligible && your_condition to only ever narrow.

Order identification uses the customer-visible order number, not always the internal WooCommerce order ID. In 1.0.12 the plugin first runs elallas_resolve_order_number, then supports WooCommerce Sequential Order Numbers Pro/free via find_order_by_order_number(), and only finally falls back to treating the entered value as the native order ID. The order details button passes $order->get_order_number() and the prefill keeps non-numeric prefixes/suffixes, so compatibility code must not cast the form value with absint().

Extension point 2 — react to the case lifecycle

A withdrawal case is created, optionally confirmed (two-step flow), then moved through admin statuses. Hook these to sync to a CRM, notify, or trigger invoicing/refund workflows.

// A new withdrawal case was submitted.
add_action( 'elallas_case_created', function ( int $case_id, int $order_id ): void {
    my_crm_open_return_ticket( $case_id, $order_id );
}, 10, 2 );

// The customer confirmed (two-step flow). Status is now auto_confirmed or manual_review.
add_action( 'elallas_case_confirmed', function ( int $case_id ): void { /* ... */ } );

// Any status transition (admin or system).
add_action( 'elallas_case_status_changed', function ( int $case_id, string $old, string $new, string $message ): void {
    if ( 'refund_pending' === $new ) { my_queue_refund( $case_id ); }

    if ( '' !== $message ) {
        my_crm_add_customer_visible_note( $case_id, $message );
    }
}, 10, 4 );

// Fired specifically for invoicing integrations.
add_action( 'elallas_invoicing_case_created', function ( int $case_id, int $order_id ): void { /* ... */ }, 10, 2 );

Verified: elallas_case_created($case_id, $order_id) at CaseService.php:83; elallas_case_confirmed($case_id) at :124; elallas_case_status_changed($case_id, $old_status, $new_status, $message) at :182; elallas_invoicing_case_created at Integrations/Invoicing.php:63.

The fourth $message argument is the optional admin note sent to the customer in the status-update e-mail. It is populated by the admin case detail form; REST and LW Site Manager status updates currently call change_status() without that message, so expect an empty string in automation-triggered transitions.

$case_id is a row ID in the custom wp_lw_elallas_cases table — NOT a post ID. Don't call get_post()/get_post_meta() on it. Read case data through the plugin's repositories/data or the tables (see reference.md). To find cases from an order, read the order meta the plugin writes: _lw_elallas_has_case ('yes'), _lw_elallas_case_ids (array of case IDs), _lw_elallas_deadline_status (CaseService.php:205-213). Treat that meta as read-only — let the plugin write it.

Case statuses (the CaseStatus set)

received → (auto_confirmed if the deadline is within, else manual_review) → admin moves it to accepted / rejected / awaiting_return / goods_received / refund_pending / closed / cancelled. All ten are the valid values elallas_case_status_changed will report; validate against them with the plugin's CaseStatus::is_valid(). Verified at CaseStatus.php:17-26. (Separately, DeadlineStatus is within / expired / unknown.)

Product/category/tag withdrawal exceptions

The plugin can mark products, product categories and product tags as excluded from withdrawal using _lw_elallas_excluded ('yes' / 'no') and _lw_elallas_exclusion_reason (reason key). Product-level meta wins first; otherwise ProductExclusion::evaluate() checks product_cat and product_tag term meta. Valid reason keys are unsealed, custom, digital, service, hygiene, perishable, sealed.

This does not auto-block the withdrawal flow. OrderSnapshotBuilder writes eligibility_flag = 'excepted' and eligibility_note = <reason label> into the case-item snapshot so the admin notification, case detail and order panel can flag it for manual review. If your integration imports products or bulk-edits exclusions, use the same meta keys via normal WP meta APIs, keep reason keys in the known set, and let the plugin build the snapshot.

Extension point 3 — customize the PDF & emails

// Filter the withdrawal PDF HTML before rendering (dompdf).
add_filter( 'elallas_pdf_html', function ( string $html, array $context ): string {
    return str_replace( '{{my_token}}', esc_html( my_value() ), $html );
}, 10, 2 );

Verified at PdfRenderer.php:40. The plugin's emails render through the standard woocommerce_email_header / woocommerce_email_footer hooks, so your existing WC email customizations apply — see wc-emails-classic.

PDF rendering uses the scoped LightweightPlugins\Elallas\Vendor\Dompdf\Dompdf class in release builds and falls back to global \Dompdf\Dompdf only for Composer-dependency installs. A PDF failure is logged and returns an empty string; it should not break case creation or e-mail sending.

Extension point 4 — multilingual output paths

The plugin detects WPML, Polylang and TranslatePress, but the important integration detail is runtime translation, not raw option reads. Admin-entered user-facing strings (button_label, confirm_label, legal_declaration, legal_confirmation, email_customer_extra) are explicitly registered/looked up for WPML and Polylang under the elallas-for-woo string context and printed through Multilingual::translate_option_string() / translate_string() on every output path; TranslatePress can translate the rendered output.

Do not add those option keys as WPML <admin-texts> in a compatibility layer. The plugin's wpml-config.xml intentionally declares only the withdrawal-exception product/term meta as copy and the [elallas_button] label shortcode attribute as translatable. Declaring the options too would double-register strings and fight the runtime translation path.

The stored withdrawal_page_id is resolved through Multilingual::object_id() so [elallas_button], the WooCommerce order button and front-end asset loading target the translated withdrawal page. Case submissions store the WPML/Polylang language code, and customer e-mails / status e-mails / PDFs switch to that case language while rendering; admin notifications switch to the shop default language.

Extension point 5 — elallas_boot

add_action( 'elallas_boot', function ( $plugin ): void {
    // Runs once the plugin has booted — safe place to wire your integration.
}, 10, 1 );

Verified at Plugin.php:64.

LW Site Manager abilities it exposes

If LW Site Manager is active, the plugin registers a elallas ability category with elallas/get-case, elallas/list-cases, elallas/update-case-status, and elallas/get-audit-log (via lw_site_manager_register_abilities / lw_site_manager_register_categories, with a fallback direct registration on wp_abilities_api_init). So agents/automation can read and drive cases through Site Manager without touching the tables. Verified at SiteManager/Integration.php:34-39. See lw-site-manager-overview and wp-abilities-api.

WooCommerce logging

The plugin writes to WooCommerce -> Status -> Logs with source elallas-for-woo. warning/error are always written; notice/info/debug require the "Debug logging" option. Context is scrubbed for common PII keys, so integration code should log only identifiers (case_id, order_id, display order_number, status, exception class), never email, IP, user-agent, names, notes or bank-account data.

Critical rules

  • Never edit the plugin. Consume elallas_* hooks; request missing extension points upstream. (LW-family rule.)
  • Detect with defined('ELALLAS_FOR_WOO_VERSION') and degrade gracefully when absent.
  • Customer-entered order numbers are display numbers. Support custom order numbering through elallas_resolve_order_number; never absint() the public form value.
  • elallas_case_status_changed has 4 args in 1.0.12+ — register accepted args as 4 if you need the customer-visible status note.
  • $case_id is a custom-table row ID, not a post. Use the plugin's data layer / tables, not get_post_meta.
  • elallas_is_order_eligible is the final gate — compose with $eligible && your_condition to narrow; returning bare true can re-enable orders the built-in checks (status, open case, ownership, expired-blocked) denied.
  • Order meta (_lw_elallas_*) is read-only for you — the plugin owns those writes; read to discover cases, don't set them.
  • Withdrawal-exception product/term meta is configuration, not a denial decision — it flags case items as excepted; final decision remains in the case workflow.
  • HPOS-safe: work with the passed \WC_Order via getters, never post meta.
  • Multilingual strings are runtime-translated — do not read/store raw option strings for customer output and do not duplicate them as WPML admin-texts.
  • Case statuses are the CaseStatus enum (ten values) — don't invent statuses; validate with CaseStatus::is_valid().
  • B2B detection is a heuristic (company/VAT presence) the merchant overrides per case — treat elallas_is_order_b2b as advisory, not authoritative legal classification.

Common mistakes

// WRONG — treating the case id as a post
$note = get_post_meta( $case_id, 'note', true );   // case_id is a wp_lw_elallas_cases row id, not a post

// WRONG — re-allowing everything (ignores built-in eligibility)
add_filter( 'elallas_is_order_eligible', '__return_true' );   // bypasses status/deadline/ownership checks

// RIGHT — only narrow
add_filter( 'elallas_is_order_eligible', fn( $ok, $order ) => $ok && my_extra_check( $order ), 10, 2 );

// WRONG — writing the plugin's order meta yourself
$order->update_meta_data( '_lw_elallas_has_case', 'yes' );   // the plugin owns this write

// WRONG — breaking stores with custom/sequential order numbers
$order_id = absint( $_POST['order_number'] ?? 0 );            // customer sees display number, not always WC ID

// RIGHT — let Elállás or your filter resolve the display number
add_filter( 'elallas_resolve_order_number', 'my_resolve_display_order_number', 10, 2 );

Cross-references

  • wc-hpos-compatibility — the plugin is HPOS-safe; keep your integration HPOS-safe too.
  • wc-sequential-order-numbers-pro — display order numbers must resolve before the withdrawal flow can identify an order.
  • wc-order-lifecycle-and-items — for reacting to the underlying WooCommerce order/refund side.
  • lw-site-manager-overview / wp-abilities-api — the elallas/* abilities it exposes.
  • wpml-string-translation / wpml-config — understand the runtime string registration and the intentional wpml-config.xml scope.
  • See reference.md for the full hook table, the 4-table schema, the order meta keys, and the status/options enums.

What this skill does NOT cover

  • The plugin's admin UI / case workflow as an end user — this is a developer-integration skill.
  • The legal/compliance interpretation of Directive 2023/2673 or 415/2025 Korm. rendelet — that's the plugin's domain, not this skill's.
  • Editing the plugin's internals — out of scope by design (consume hooks).
  • The withdrawal-form Gutenberg block internals (blocks/withdrawal-form) — beyond noting it exists.

References

Files (wp-agent-skills)
  • reference.md 10.7 KB
    # lw-elallas-integration — reference
    
    Full extension surface of **Elállás for WooCommerce** (`elallas-for-woo` 1.0.12, `LightweightPlugins\Elallas`). Integrate by consuming these hooks / reading these tables — never by editing the plugin.
    
    ## All `elallas_*` hooks
    
    | Hook | Type | Signature | Fires / purpose | Source |
    |---|---|---|---|---|
    | `elallas_boot` | action | `($plugin)` | After the plugin boots — wire your integration here | Plugin.php:64 |
    | `elallas_is_order_eligible` | filter | `(bool $eligible, \WC_Order $order)` | Final gate on whether an order may start a withdrawal | EligibilityChecker.php:67 |
    | `elallas_deadline_days` | filter | `(int $days, \WC_Order $order)` | The withdrawal window in days (default `deadline_days` option = 14) | EligibilityChecker.php:119 |
    | `elallas_is_order_b2b` | filter | `(bool $is_b2b, \WC_Order $order)` | B2B/B2C classification (default: company OR VAT present) | B2BDetector.php:40 |
    | `elallas_resolve_order_number` | filter | `(int $order_id, string $number)` | Resolve a customer-entered/display order number to a WooCommerce order ID; return `0` to fall through | Woo/OrderAdapter.php:65 |
    | `elallas_delivery_date` | filter | `(mixed $delivery, \WC_Order $order)` | Delivery date used as a deadline basis | Woo/OrderAdapter.php:166 |
    | `elallas_case_created` | action | `(int $case_id, int $order_id)` | A withdrawal case was created (status `received`) | Domain/CaseService.php:83 |
    | `elallas_case_confirmed` | action | `(int $case_id)` | Customer confirmed (two-step); status now `auto_confirmed` or `manual_review` | Domain/CaseService.php:124 |
    | `elallas_case_status_changed` | action | `(int $case_id, string $old_status, string $new_status, string $message)` | Any status transition (admin/system), optionally with a customer-visible admin note | Domain/CaseService.php:182 |
    | `elallas_invoicing_case_created` | action | `(int $case_id, int $order_id)` | Invoicing-integration entry point | Integrations/Invoicing.php:63 |
    | `elallas_pdf_html` | filter | `(string $html, array $context)` | Withdrawal PDF HTML before dompdf render | Pdf/PdfRenderer.php:40 |
    
    It also fires the standard WooCommerce email hooks (`woocommerce_email_header` / `woocommerce_email_footer` / `_footer_text`) and uses WPML/Polylang runtime string translation for admin-entered customer-facing strings.
    
    ## Order number resolution
    
    `OrderAdapter::get_order_by_number()` resolves the order number the customer sees and types. In 1.0.12 the order-details button uses `$order->get_order_number()`, and the identify prefill keeps prefixes/suffixes instead of casting to an integer.
    
    Resolution order:
    
    1. `elallas_resolve_order_number` filter (`0` means unresolved / fall through).
    2. WooCommerce Sequential Order Numbers Pro/free helpers: `wc_seq_order_number_pro()` or `wc_seq_order_number()` with `find_order_by_order_number()`.
    3. Native WooCommerce order ID fallback by stripping non-digits.
    
    Compatibility rule: if another order-numbering plugin is present, hook `elallas_resolve_order_number`; do not rely on `absint( $number )` anywhere customer input is involved.
    
    ## Case lifecycle (CaseStatus)
    
    Values (`includes/Models/CaseStatus.php:17-26`) — validate with `CaseStatus::is_valid()`, label with `CaseStatus::label()`:
    
    ```
    received          # created
    auto_confirmed    # confirmed while deadline is 'within'
    manual_review     # confirmed while deadline is expired/unknown
    accepted          # admin
    rejected          # admin
    awaiting_return   # admin — waiting for goods back
    goods_received    # admin
    refund_pending    # admin — refund due
    closed            # admin — terminal
    cancelled         # admin/customer — terminal
    ```
    
    Flow: `received` → (`auto_confirmed` | `manual_review`) → admin moves to any of `accepted`/`rejected`/`awaiting_return`/`goods_received`/`refund_pending`/`closed`/`cancelled`. `DeadlineStatus` (separate, `includes/Models/DeadlineStatus.php:17-19`): `within` / `expired` / `unknown`.
    
    `elallas_case_status_changed` passes a fourth `$message` argument. The admin case detail form can populate it as a note sent to the customer in the status-update e-mail. REST and LW Site Manager status updates currently do not pass this message, so integrations must tolerate an empty string.
    
    ## Order meta the plugin writes (read-only for integrators)
    
    Set in `CaseService::update_order_meta()` (CaseService.php:205-213). Read to discover cases from an order; do not write these yourself.
    
    | Meta key | Value |
    |---|---|
    | `_lw_elallas_has_case` | `'yes'` once a case exists |
    | `_lw_elallas_case_ids` | array of case IDs (ints) |
    | `_lw_elallas_deadline_status` | `within` / `expired` / `unknown` |
    
    ## Custom tables (schema from includes/Database/Schema.php)
    
    All prefixed `{$wpdb->prefix}lw_elallas_`. `case_id` is a row id in `..._cases`, **not** a post id.
    
    ### `lw_elallas_cases` (Schema.php:66-97)
    `id`, `case_number` (UNIQUE), `order_id`, `order_number`, `customer_id`, `customer_email_hash` CHAR(64), `customer_email_encrypted` TEXT, `status` (default `received`), `withdrawal_type` (`full`/…), `submitted_at`, `confirmed_at`, `deadline_status` (default `unknown`), `order_created_at`, `order_completed_at`, `delivery_date`, `ip_hash`, `user_agent_hash`, `source_url`, `language`, `assigned_admin_id`, `customer_note` TEXT, `bank_account_encrypted` TEXT, `created_at`, `updated_at`. Keys on `order_id`, `customer_id`, `status`, `deadline_status`.
    
    **Privacy:** email, bank account, IP and user-agent are **hashed/encrypted at rest** — there is no plaintext email column (`customer_email_hash` + `customer_email_encrypted`). Don't expect to `SELECT` a readable email; go through the plugin's data layer if it exposes decryption, or match on the hash.
    
    ### `lw_elallas_case_items` (Schema.php:109-126)
    `id`, `case_id`, `order_item_id`, `product_id`, `variation_id`, `product_name_snapshot`, `sku_snapshot`, `qty_ordered`, `qty_withdrawn`, `line_total_snapshot` DECIMAL(19,4), `tax_total_snapshot`, `eligibility_flag` (default `eligible`), `eligibility_note`. Snapshots are captured at case creation — they don't change if the product later changes.
    
    ### `lw_elallas_events` (audit log, Schema.php:137-149)
    `id`, `case_id`, `event_type` (e.g. `case_created`, `case_confirmed`, `status_changed`), `actor_type` (`system`/`admin`/`customer`), `actor_id`, `message` TEXT, `metadata_json` LONGTEXT, `created_at`. Every lifecycle step logs a row here (`EventRepository::log`).
    
    ### `lw_elallas_documents` (Schema.php:160-170)
    `id`, `case_id`, `document_type` (default `withdrawal_statement`), `file_path`, `file_hash` CHAR(64), `token`, `created_at`.
    
    ## Product/category/tag withdrawal exceptions
    
    Configured through product meta and product-category/product-tag term meta:
    
    | Meta key | Scope | Value |
    |---|---|---|
    | `_lw_elallas_excluded` | product, `product_cat`, `product_tag` | `'yes'` / `'no'` |
    | `_lw_elallas_exclusion_reason` | product, `product_cat`, `product_tag` | `unsealed`, `custom`, `digital`, `service`, `hygiene`, `perishable`, `sealed`, or `''` |
    
    Resolution: product meta wins first, then `ProductExclusion::taxonomy_exclusion()` checks `product_cat` and `product_tag` term meta. Matching items are written into `lw_elallas_case_items` with `eligibility_flag = 'excepted'` and the reason label in `eligibility_note`.
    
    Important: an exception flag does not auto-block submission. It marks the item/case for manual review and powers admin/e-mail warnings.
    
    ## LW Site Manager abilities (category `elallas`)
    
    Registered in `includes/SiteManager/Integration.php:34-39` on `lw_site_manager_register_categories` + `lw_site_manager_register_abilities`, with a fallback direct registration on `wp_abilities_api_init` (priority 20) so the abilities exist even if Site Manager's own hooks didn't fire:
    
    - `elallas/get-case`
    - `elallas/list-cases`
    - `elallas/update-case-status`
    - `elallas/get-audit-log`
    
    Prefer these (via LW Site Manager / the WP Abilities API — see `lw-site-manager-overview`, `wp-abilities-api`) over direct table access when you need to read or drive cases programmatically.
    
    `elallas/update-case-status` accepts `case_id` and `status` only. It drives `Domain\CaseService::change_status()` but does not currently expose the customer-visible status message.
    
    ## Multilingual behavior
    
    The multilingual integration is centralized in `includes/Integrations/Multilingual.php`:
    
    - Registers option strings `legal_declaration`, `legal_confirmation`, `button_label`, `confirm_label`, `email_customer_extra` under context `elallas-for-woo`.
    - Translates output via `Multilingual::translate_option_string()` / `translate_string()`; output paths should not print raw option values.
    - Resolves translated page IDs via `Multilingual::object_id()`, so `[elallas_button]`, the order button and asset loading target the translated withdrawal page.
    - Stores the submission language using the WPML/Polylang language code where available, falling back to locale.
    - Switches WPML language while rendering customer confirmation e-mails, status e-mails and PDFs; admin notification switches to the default shop language.
    
    `wpml-config.xml` intentionally declares only:
    
    - `_lw_elallas_excluded` and `_lw_elallas_exclusion_reason` product/term meta as `copy`.
    - The `[elallas_button]` `label` shortcode attribute as translatable.
    
    Do not add the admin-entered option strings as WPML `<admin-texts>` in another compatibility layer; they are runtime-registered so WPML and Polylang share the same output path and background renders can switch language explicitly.
    
    ## WooCommerce logging
    
    `Support\Logger` writes to WooCommerce logs with source `elallas-for-woo`.
    
    - `error` / `warning` are always written.
    - `notice` / `info` / `debug` require the `logging_enabled` option.
    - Context is scrubbed for common PII keys (`email`, `customer_email`, `ip`, `user_agent`, `name`, `bank_account`, `customer_note`, etc.).
    
    Use IDs and status values in integration logs; never pass plaintext customer data into the logger context.
    
    ## Built-in eligibility (so your `elallas_is_order_eligible` filter composes correctly)
    
    From `EligibilityChecker::check()` (EligibilityChecker.php:32-72), an order is denied by default when any of these hold — your filter runs last and can only be trusted to *narrow* if you `&&` your condition:
    
    - Order status not in `eligible_statuses` option (default `['processing','completed']`).
    - An open case already exists for the order.
    - A logged-in user is acting on an order that isn't theirs (guests must match the order email).
    - Deadline `expired` AND `expired_handling` option = `'block'` (otherwise expiry only flags manual review, never hard-blocks).
    
    ## Detection & requirements
    
    - `defined( 'ELALLAS_FOR_WOO_VERSION' )` — active check (constant `'1.0.12'`, `elallas-for-woo.php:31`).
    - Namespace `LightweightPlugins\Elallas`; requires WooCommerce 8.0+, PHP 8.0+, WP 6.4+.
    - HPOS-safe — hooks hand you a `\WC_Order`; use getters, not post meta.
    
  • SKILL.md 18.1 KB
    ---
    name: lw-elallas-integration
    description: Integrate with or extend "Elállás for WooCommerce"
      (elallas-for-woo), the LW-family EU/HU withdrawal case-management plugin
      for WooCommerce. Covers its elallas_* hooks, eligibility/deadline/B2B
      /delivery/order-number filters (including elallas_resolve_order_number),
      the 4-arg elallas_case_status_changed lifecycle action, PDF/email hooks,
      multilingual WPML/Polylang output, custom tables/order meta, WooCommerce
      logging, and LW Site Manager abilities. Use when building withdrawal-case
      compatibility, syncing cases to CRM/invoicing, supporting custom order
      numbers, or customizing its PDF/emails without editing the plugin.
    metadata:
      wp-skills-author: "Soczó Kristóf"
      wp-skills-contact: "mailto:lonsdale201@hotmail.com"
      wp-skills-plugin: "elallas-for-woo"
      wp-skills-plugin-version-tested: "1.0.12"
      wp-skills-php-min: "8.0"
      wp-skills-last-updated: "2026-07-09"
    ---
    
    # Elállás for WooCommerce — integration & extension
    
    For developers making a plugin/theme work with **Elállás for WooCommerce** (`elallas-for-woo`, by uptools.io) — the online right-of-withdrawal (elállási jog) button + case-management plugin for WooCommerce, built for EU Directive 2023/2673 and 415/2025. (XII. 23.) Korm. rendelet. It manages a **withdrawal "case"** per order in its own tables and fires a small, clean set of `elallas_*` hooks for integrators.
    
    ## This is an LW-family plugin — consume hooks, don't edit it
    
    Namespace `LightweightPlugins\Elallas`, tables `wp_lw_elallas_*`, and it integrates with LW Site Manager — it's part of the LightweightPlugins family. **Integrate by consuming its `elallas_*` hooks and reading its data; never patch the plugin.** If you need an extension point that doesn't exist, request it upstream rather than editing.
    
    Detect it:
    
    ```php
    if ( defined( 'ELALLAS_FOR_WOO_VERSION' ) ) { /* Elállás for WooCommerce active (1.0.12 tested) */ }
    ```
    
    It requires WooCommerce 8.0+ and is HPOS-safe — every hook hands you a real `\WC_Order`, so use `$order->get_*()` (never post meta) — see `wc-hpos-compatibility`.
    
    ## Extension point 1 — eligibility, deadline, B2B & order numbers
    
    The first four filters decide **whether an order may start a withdrawal** and how the deadline is computed; they receive the `\WC_Order`. `elallas_resolve_order_number` runs earlier, before the order exists, and maps the customer-entered display order number to a WooCommerce order ID.
    
    ```php
    // Final say on eligibility (runs after the built-in checks).
    add_filter( 'elallas_is_order_eligible', function ( bool $eligible, \WC_Order $order ): bool {
        // e.g. never allow withdrawal for a "digital-only" order:
        return $eligible && ! my_order_is_digital_only( $order );
    }, 10, 2 );
    
    // Override the withdrawal window (default: option 'deadline_days', 14).
    add_filter( 'elallas_deadline_days', function ( int $days, \WC_Order $order ): int {
        return my_is_extended_returns_member( $order ) ? 30 : $days;
    }, 10, 2 );
    
    // Override the B2B/B2C heuristic (default: true if company name OR VAT number is set).
    add_filter( 'elallas_is_order_b2b', function ( bool $is_b2b, \WC_Order $order ): bool {
        return $is_b2b || my_customer_is_business( $order->get_customer_id() );
    }, 10, 2 );
    
    // Override the delivery date used as a deadline basis.
    add_filter( 'elallas_delivery_date', function ( $delivery, \WC_Order $order ) {
        return my_carrier_delivered_at( $order ) ?: $delivery;
    }, 10, 2 );
    
    // Resolve a customer-entered/display order number from another numbering plugin.
    add_filter( 'elallas_resolve_order_number', function ( int $order_id, string $number ): int {
        if ( $order_id > 0 ) {
            return $order_id;
        }
    
        return my_numbering_plugin_find_order_id( $number ) ?: 0; // 0 = let Elállás fall through.
    }, 10, 2 );
    ```
    
    Verified: `elallas_is_order_eligible` at [EligibilityChecker.php:67](EligibilityChecker.php); `elallas_deadline_days` at [:119](EligibilityChecker.php) (default from `Options::get('deadline_days', 14)`); `elallas_is_order_b2b` at [B2BDetector.php:40](B2BDetector.php) (heuristic: company OR VAT filled); `elallas_delivery_date` at [OrderAdapter.php:166](OrderAdapter.php); `elallas_resolve_order_number` at [OrderAdapter.php:65](OrderAdapter.php).
    
    Good to know about the built-in eligibility (so your filter composes correctly): by default only orders whose status is in `eligible_statuses` (default `['processing','completed']`) qualify; an order with an already-open case is refused; logged-in users may only act on their own orders (guests fall back to an email match); and **an expired deadline does NOT hard-block by default** — it's flagged for manual review unless the `expired_handling` option is set to `'block'` ([EligibilityChecker.php:32-72](EligibilityChecker.php)). Your `elallas_is_order_eligible` filter is the final gate, so returning `true` can re-allow something the built-in checks denied — return `$eligible && your_condition` to only ever narrow.
    
    Order identification uses the customer-visible order number, not always the internal WooCommerce order ID. In 1.0.12 the plugin first runs `elallas_resolve_order_number`, then supports WooCommerce Sequential Order Numbers Pro/free via `find_order_by_order_number()`, and only finally falls back to treating the entered value as the native order ID. The order details button passes `$order->get_order_number()` and the prefill keeps non-numeric prefixes/suffixes, so compatibility code must not cast the form value with `absint()`.
    
    ## Extension point 2 — react to the case lifecycle
    
    A withdrawal **case** is created, optionally confirmed (two-step flow), then moved through admin statuses. Hook these to sync to a CRM, notify, or trigger invoicing/refund workflows.
    
    ```php
    // A new withdrawal case was submitted.
    add_action( 'elallas_case_created', function ( int $case_id, int $order_id ): void {
        my_crm_open_return_ticket( $case_id, $order_id );
    }, 10, 2 );
    
    // The customer confirmed (two-step flow). Status is now auto_confirmed or manual_review.
    add_action( 'elallas_case_confirmed', function ( int $case_id ): void { /* ... */ } );
    
    // Any status transition (admin or system).
    add_action( 'elallas_case_status_changed', function ( int $case_id, string $old, string $new, string $message ): void {
        if ( 'refund_pending' === $new ) { my_queue_refund( $case_id ); }
    
        if ( '' !== $message ) {
            my_crm_add_customer_visible_note( $case_id, $message );
        }
    }, 10, 4 );
    
    // Fired specifically for invoicing integrations.
    add_action( 'elallas_invoicing_case_created', function ( int $case_id, int $order_id ): void { /* ... */ }, 10, 2 );
    ```
    
    Verified: `elallas_case_created($case_id, $order_id)` at [CaseService.php:83](CaseService.php); `elallas_case_confirmed($case_id)` at [:124](CaseService.php); `elallas_case_status_changed($case_id, $old_status, $new_status, $message)` at [:182](CaseService.php); `elallas_invoicing_case_created` at [Integrations/Invoicing.php:63](Invoicing.php).
    
    The fourth `$message` argument is the optional admin note sent to the customer in the status-update e-mail. It is populated by the admin case detail form; REST and LW Site Manager status updates currently call `change_status()` without that message, so expect an empty string in automation-triggered transitions.
    
    **`$case_id` is a row ID in the custom `wp_lw_elallas_cases` table — NOT a post ID.** Don't call `get_post()`/`get_post_meta()` on it. Read case data through the plugin's repositories/data or the tables (see reference.md). To find cases from an order, read the order meta the plugin writes: `_lw_elallas_has_case` (`'yes'`), `_lw_elallas_case_ids` (array of case IDs), `_lw_elallas_deadline_status` ([CaseService.php:205-213](CaseService.php)). Treat that meta as read-only — let the plugin write it.
    
    ### Case statuses (the `CaseStatus` set)
    
    `received` → (`auto_confirmed` if the deadline is `within`, else `manual_review`) → admin moves it to `accepted` / `rejected` / `awaiting_return` / `goods_received` / `refund_pending` / `closed` / `cancelled`. All ten are the valid values `elallas_case_status_changed` will report; validate against them with the plugin's `CaseStatus::is_valid()`. Verified at [CaseStatus.php:17-26](CaseStatus.php). (Separately, `DeadlineStatus` is `within` / `expired` / `unknown`.)
    
    ## Product/category/tag withdrawal exceptions
    
    The plugin can mark products, product categories and product tags as excluded from withdrawal using `_lw_elallas_excluded` (`'yes'` / `'no'`) and `_lw_elallas_exclusion_reason` (reason key). Product-level meta wins first; otherwise `ProductExclusion::evaluate()` checks `product_cat` and `product_tag` term meta. Valid reason keys are `unsealed`, `custom`, `digital`, `service`, `hygiene`, `perishable`, `sealed`.
    
    This does **not** auto-block the withdrawal flow. `OrderSnapshotBuilder` writes `eligibility_flag = 'excepted'` and `eligibility_note = <reason label>` into the case-item snapshot so the admin notification, case detail and order panel can flag it for manual review. If your integration imports products or bulk-edits exclusions, use the same meta keys via normal WP meta APIs, keep reason keys in the known set, and let the plugin build the snapshot.
    
    ## Extension point 3 — customize the PDF & emails
    
    ```php
    // Filter the withdrawal PDF HTML before rendering (dompdf).
    add_filter( 'elallas_pdf_html', function ( string $html, array $context ): string {
        return str_replace( '{{my_token}}', esc_html( my_value() ), $html );
    }, 10, 2 );
    ```
    
    Verified at [PdfRenderer.php:40](PdfRenderer.php). The plugin's emails render through the standard `woocommerce_email_header` / `woocommerce_email_footer` hooks, so your existing WC email customizations apply — see `wc-emails-classic`.
    
    PDF rendering uses the scoped `LightweightPlugins\Elallas\Vendor\Dompdf\Dompdf` class in release builds and falls back to global `\Dompdf\Dompdf` only for Composer-dependency installs. A PDF failure is logged and returns an empty string; it should not break case creation or e-mail sending.
    
    ## Extension point 4 — multilingual output paths
    
    The plugin detects WPML, Polylang and TranslatePress, but the important integration detail is **runtime translation**, not raw option reads. Admin-entered user-facing strings (`button_label`, `confirm_label`, `legal_declaration`, `legal_confirmation`, `email_customer_extra`) are explicitly registered/looked up for WPML and Polylang under the `elallas-for-woo` string context and printed through `Multilingual::translate_option_string()` / `translate_string()` on every output path; TranslatePress can translate the rendered output.
    
    Do not add those option keys as WPML `<admin-texts>` in a compatibility layer. The plugin's `wpml-config.xml` intentionally declares only the withdrawal-exception product/term meta as `copy` and the `[elallas_button]` `label` shortcode attribute as translatable. Declaring the options too would double-register strings and fight the runtime translation path.
    
    The stored `withdrawal_page_id` is resolved through `Multilingual::object_id()` so `[elallas_button]`, the WooCommerce order button and front-end asset loading target the translated withdrawal page. Case submissions store the WPML/Polylang language code, and customer e-mails / status e-mails / PDFs switch to that case language while rendering; admin notifications switch to the shop default language.
    
    ## Extension point 5 — `elallas_boot`
    
    ```php
    add_action( 'elallas_boot', function ( $plugin ): void {
        // Runs once the plugin has booted — safe place to wire your integration.
    }, 10, 1 );
    ```
    
    Verified at [Plugin.php:64](Plugin.php).
    
    ## LW Site Manager abilities it exposes
    
    If LW Site Manager is active, the plugin registers a `elallas` ability category with `elallas/get-case`, `elallas/list-cases`, `elallas/update-case-status`, and `elallas/get-audit-log` (via `lw_site_manager_register_abilities` / `lw_site_manager_register_categories`, with a fallback direct registration on `wp_abilities_api_init`). So agents/automation can read and drive cases through Site Manager without touching the tables. Verified at [SiteManager/Integration.php:34-39](Integration.php). See `lw-site-manager-overview` and `wp-abilities-api`.
    
    ## WooCommerce logging
    
    The plugin writes to WooCommerce -> Status -> Logs with source `elallas-for-woo`. `warning`/`error` are always written; `notice`/`info`/`debug` require the "Debug logging" option. Context is scrubbed for common PII keys, so integration code should log only identifiers (`case_id`, `order_id`, display `order_number`, status, exception class), never email, IP, user-agent, names, notes or bank-account data.
    
    ## Critical rules
    
    - **Never edit the plugin.** Consume `elallas_*` hooks; request missing extension points upstream. (LW-family rule.)
    - **Detect with `defined('ELALLAS_FOR_WOO_VERSION')`** and degrade gracefully when absent.
    - **Customer-entered order numbers are display numbers.** Support custom order numbering through `elallas_resolve_order_number`; never `absint()` the public form value.
    - **`elallas_case_status_changed` has 4 args in 1.0.12+** — register accepted args as `4` if you need the customer-visible status note.
    - **`$case_id` is a custom-table row ID, not a post.** Use the plugin's data layer / tables, not `get_post_meta`.
    - **`elallas_is_order_eligible` is the final gate** — compose with `$eligible && your_condition` to narrow; returning bare `true` can re-enable orders the built-in checks (status, open case, ownership, expired-blocked) denied.
    - **Order meta (`_lw_elallas_*`) is read-only for you** — the plugin owns those writes; read to discover cases, don't set them.
    - **Withdrawal-exception product/term meta is configuration, not a denial decision** — it flags case items as `excepted`; final decision remains in the case workflow.
    - **HPOS-safe**: work with the passed `\WC_Order` via getters, never post meta.
    - **Multilingual strings are runtime-translated** — do not read/store raw option strings for customer output and do not duplicate them as WPML admin-texts.
    - **Case statuses are the `CaseStatus` enum** (ten values) — don't invent statuses; validate with `CaseStatus::is_valid()`.
    - **B2B detection is a heuristic** (company/VAT presence) the merchant overrides per case — treat `elallas_is_order_b2b` as advisory, not authoritative legal classification.
    
    ## Common mistakes
    
    ```php
    // WRONG — treating the case id as a post
    $note = get_post_meta( $case_id, 'note', true );   // case_id is a wp_lw_elallas_cases row id, not a post
    
    // WRONG — re-allowing everything (ignores built-in eligibility)
    add_filter( 'elallas_is_order_eligible', '__return_true' );   // bypasses status/deadline/ownership checks
    
    // RIGHT — only narrow
    add_filter( 'elallas_is_order_eligible', fn( $ok, $order ) => $ok && my_extra_check( $order ), 10, 2 );
    
    // WRONG — writing the plugin's order meta yourself
    $order->update_meta_data( '_lw_elallas_has_case', 'yes' );   // the plugin owns this write
    
    // WRONG — breaking stores with custom/sequential order numbers
    $order_id = absint( $_POST['order_number'] ?? 0 );            // customer sees display number, not always WC ID
    
    // RIGHT — let Elállás or your filter resolve the display number
    add_filter( 'elallas_resolve_order_number', 'my_resolve_display_order_number', 10, 2 );
    ```
    
    ## Cross-references
    
    - **`wc-hpos-compatibility`** — the plugin is HPOS-safe; keep your integration HPOS-safe too.
    - **`wc-sequential-order-numbers-pro`** — display order numbers must resolve before the withdrawal flow can identify an order.
    - **`wc-order-lifecycle-and-items`** — for reacting to the underlying WooCommerce order/refund side.
    - **`lw-site-manager-overview`** / **`wp-abilities-api`** — the `elallas/*` abilities it exposes.
    - **`wpml-string-translation`** / **`wpml-config`** — understand the runtime string registration and the intentional `wpml-config.xml` scope.
    - See `reference.md` for the full hook table, the 4-table schema, the order meta keys, and the status/options enums.
    
    ## What this skill does NOT cover
    
    - **The plugin's admin UI / case workflow as an end user** — this is a developer-integration skill.
    - **The legal/compliance interpretation** of Directive 2023/2673 or 415/2025 Korm. rendelet — that's the plugin's domain, not this skill's.
    - **Editing the plugin's internals** — out of scope by design (consume hooks).
    - **The withdrawal-form Gutenberg block internals** (`blocks/withdrawal-form`) — beyond noting it exists.
    
    ## References
    
    - Eligibility/deadline: [includes/Domain/EligibilityChecker.php](EligibilityChecker.php) (`elallas_is_order_eligible` 67, `elallas_deadline_days` 119, default eligible statuses 81).
    - B2B: [includes/Domain/B2BDetector.php:40](B2BDetector.php).
    - Product/category/tag exceptions: [includes/Admin/ProductFields.php](ProductFields.php), [includes/Admin/TermFields.php](TermFields.php), [includes/Domain/ProductExclusion.php](ProductExclusion.php), [includes/Domain/OrderSnapshotBuilder.php](OrderSnapshotBuilder.php).
    - Order numbers/delivery: [includes/Woo/OrderAdapter.php](OrderAdapter.php) (`elallas_resolve_order_number` 65, Sequential helper lookup 74-85, `elallas_delivery_date` 166).
    - Case lifecycle + order meta: [includes/Domain/CaseService.php](CaseService.php) (created 83, confirmed 124, status_changed 182, order meta 205-213).
    - Case statuses: [includes/Models/CaseStatus.php:17-26](CaseStatus.php); deadline statuses: `includes/Models/DeadlineStatus.php:17-19`.
    - Multilingual: [includes/Integrations/Multilingual.php](Multilingual.php), [wpml-config.xml](wpml-config.xml), [includes/Frontend/Shortcodes.php](Shortcodes.php), [includes/Frontend/SubmissionContext.php](SubmissionContext.php), [includes/Emails/EmailManager.php](EmailManager.php).
    - PDF filter/logging: [includes/Pdf/PdfRenderer.php:40](PdfRenderer.php), [includes/Support/Logger.php](Logger.php); invoicing: [includes/Integrations/Invoicing.php:63](Invoicing.php); boot: [includes/Plugin.php:64](Plugin.php).
    - Tables + Site Manager abilities: [includes/Database/Schema.php](Schema.php), [includes/SiteManager/Integration.php:34-39](Integration.php).
    - Official documentation: <https://developer.woocommerce.com/docs/features/high-performance-order-storage/>
    - Verified source paths:
      - `wp-content/plugins/elallas-for-woo/elallas-for-woo.php`
      - `wp-content/plugins/elallas-for-woo/includes/Woo/Hooks.php`
    

Comments (0)

Sign in to join the conversation.

No comments yet.

Reviews (0)

No reviews yet.

Related