# Extrova · Developer hooks

Contract version: `EXTROVA_API_VERSION` = `1.0`. Breaking changes only in a major version, after a deprecation cycle (`_deprecated_hook()`).

All PHP classes live under the `Extrova\` namespace. Short names below:
`Extra`, `Selection`, `Context` (`Core\`), `Plugin`, `Container`.

## Bootstrapping an add-on

```php
add_action( 'extrova_loaded', function ( \Extrova\Plugin $plugin ) {
	if ( version_compare( EXTROVA_API_VERSION, '1.0', '<' ) ) {
		return;
	}
	add_filter( 'extrova_service_providers', function ( array $providers ) {
		$providers[] = new My_Provider(); // implements Extrova\ServiceProviderInterface
		return $providers;
	} );
} );
```

Sequence: `plugins_loaded:10` → requirements → `extrova_loaded` → providers filter → every `register()` → `init:5` → every `boot()` → `extrova_init`.
`$container->extend( 'pricing.calculator', fn( $inner, $c ) => new MyCalculator( $inner ) )` can decorate a service inside `register()`.

Service ids: `settings`, `repository`, `schema`, `migrations`, `registry.types`, `registry.pricing`, `registry.conditions`, `registry.locations`, `rules.evaluator`, `pricing.tax`, `pricing.calculator`, `selection.store`, `context.factory`, `extra.service`, `goals.free_shipping`, `goals.service`, `cart.fee_applier`, `storeapi.cart_extension`, `storeapi.update_callback`, `storeapi.cart_validation`, `blocks.integration`, `blocks.registrar`, `classic.checkout`, `classic.assets`, `order.writer`, `order.display`, `order.privacy`, `admin.upsell`, `admin.assets`, `admin.page`, `admin.links`, `rest.extras`, `rest.settings`, `rest.notices`, `api.version`.

## PHP actions

| Hook | Arguments | When |
|---|---|---|
| `extrova_loaded` | `Plugin $plugin` | Before providers are filtered and registered. |
| `extrova_init` | `Container $container` | After every provider booted (`init:5`). |
| `extrova_selections_updated` | `array $selections, array $previous, string $source, Context $context` | Selections changed. `$source`: `store_api`, `classic`. Arrays are `extra_id => Selection`. |
| `extrova_order_extras_saved` | `WC_Order $order, array $snapshot, Context $context` | Snapshot written. `$context->offered_extras` = extras offered. May fire several times for one Checkout block draft order. |
| `extrova_before_fees` | `WC_Cart $cart, Context $context` | Before the extras fees are added. |
| `extrova_after_fees` | `WC_Cart $cart, Context $context` | After the extras fees are added. |
| `extrova_admin_enqueue` | `string $hook_suffix` | After the admin app is enqueued (load add-on scripts that depend on `extrova-admin`). |
| `extrova_register_rest_routes` | `string $namespace` | After the Lite REST routes (`extrova/v1`). |

## PHP filters

| Hook | Signature (filtered value first) |
|---|---|
| `extrova_service_providers` | `( array $providers ): array` — `ServiceProviderInterface[]` or class names |
| `extrova_register_extra_types` | `( array $types ): array` — `slug => Types\ExtraTypeInterface` |
| `extrova_register_pricing_methods` | `( array $methods ): array` — `slug => Pricing\PricingMethodInterface` |
| `extrova_register_conditions` | `( array $conditions ): array` — `slug => Rules\ConditionInterface` |
| `extrova_register_locations` | `( array $locations ): array` — `slug => [ 'label' => string, 'contexts' => string[] ]` |
| `extrova_goals` | `( array $goals, Context $context ): array` — `Goals\GoalInterface[]` |
| `extrova_extras` | `( array $extras, Context $context ): array` — `Extra[]` after loading |
| `extrova_extra_is_visible` | `( bool $visible, Extra $extra, Context $context ): bool` |
| `extrova_extra_price` | `( string $amount, Extra $extra, Selection $selection, Context $context ): string` — as entered by the store, before net conversion |
| `extrova_percentage_base` | `( string $base_amount, Extra $extra, Context $context ): string` |
| `extrova_fee_args` | `( array $args, Extra $extra, Selection $selection, Context $context ): array` — `id, name, amount, taxable, tax_class` |
| `extrova_apply_to_cart` | `( bool $apply, WC_Cart $cart ): bool` — false by default on recurring carts |
| `extrova_validate_selection` | `( true\|WP_Error $result, Extra $extra, ?Selection $selection, Context $context ): true\|WP_Error` |
| `extrova_sanitized_value` | `( mixed $value, mixed $raw, Extra $extra ): mixed` |
| `extrova_cart_extension_data` | `( array $data, Context $context ): array` |
| `extrova_order_snapshot_item` | `( array $item, Extra $extra, Selection $selection, WC_Order $order ): array` |
| `extrova_order_display_rows` | `( array $rows, WC_Order $order, string $surface ): array` — `admin`, `email`, `email_plain`, `account`, `confirmation` |
| `extrova_extra_schema` | `( array $schema ): array` — JSON Schema of an extra |
| `extrova_settings_schema` | `( array $schema ): array` |
| `extrova_admin_script_data` | `( array $data ): array` |
| `extrova_classic_allowed_html` | `( array $allowed ): array` — tags and attributes kept by `wp_kses()` in the classic checkout markup of the extras (add-on types: `img`, `accept`...) |
| `extrova_load_frontend_assets` | `( bool $load, string $location ): bool` — `checkout_block`, `checkout_classic` |
| `extrova_manage_capability` | `( string $cap ): string` — default `manage_woocommerce` |
| `extrova_is_pro_active` | `( bool $active ): bool` — true hides every upsell |
| `extrova_upgrade_url` | `( string $url, string $placement ): string` |

### Interfaces

- `Types\ExtraTypeInterface` (+ `Types\AbstractExtraType`, which adds `preview_selections()` and `fee_name()`).
- `Pricing\PricingMethodInterface` (+ `Pricing\AbstractPricingMethod::sanitize()`), optional `Pricing\NetAmountPricingInterface` for methods whose result is already net of tax.
- `Rules\ConditionInterface` (+ `Rules\AbstractCondition::sanitize()`). Composite conditions call `$evaluator->passes( $conditions, $context )`.
- `Goals\GoalInterface`.
- `Core\ExtraRepositoryInterface`.

Unknown types, pricing methods or conditions are preserved in the configuration and the extra is hidden (fail-closed).

### Stable classes and constants (contract 1.x)

Besides the interfaces above, these classes are public API. Their names and the members listed keep their signatures and behaviour for the whole 1.x line; changes only in a major version, after a deprecation cycle. Anything not listed here (other classes, `private`/`@internal` members) may change in any release.

| Class | Stable members |
|---|---|
| `Admin\Capability` | `name(): string` (capability after the `extrova_manage_capability` filter) and `check(): bool` (current user has it; usable as a REST `permission_callback`). |
| `Core\Money` | `decimal( string\|float $amount ): string` (store decimals, `.` separator, for storage and the Store API), `to_minor( string\|float $amount ): string` (minor units) and `plain( string\|float $amount ): string` (formatted price without HTML). |
| `Core\ConflictException` | Extends `\RuntimeException`. Thrown by repositories with optimistic locking when another user saved first (the REST layer answers 409). Add-ons may throw and catch it. |
| `Core\OptionExtraRepository` | Constant `OPTION` = `'extrova_extras'` (name of the option, see Data). Read the extras through the `repository` service, not the option. |
| `Registry\ConditionRegistry` | Service `registry.conditions`: `get( string $slug ): ?Rules\ConditionInterface`, `all(): array`, `has( string $slug ): bool`. The same members on `registry.types`, `registry.pricing` and `registry.locations`. |
| `Core\Extra`, `Core\Selection`, `Core\Context` | Value objects passed to hooks: their public properties; `Extra::is_active()`, `Extra::to_array()`, `Extra::with( array $changes ): Extra`; `Context::subtotal()`, `Context::get( string $key, mixed $fallback = null )`, `Context::with_data( string $key, mixed $value ): Context` and the constant `Context::LOCATION_ANY`. |
| `Core\ContextFactory` | Class of the `context.factory` service (add-ons may type constructor arguments with it): `current( string $location ): Context`, `from_cart( ?WC_Cart $cart, string $location ): Context`. |
| `Core\ExtraService` | Class of the `extra.service` service (add-ons may type constructor arguments with it): `get_visible_extras( Context ): array` (`id => Extra`), `get_selected( Context ): array` (`id => Selection`), `get_selections_for( array $extras ): array`, `get_price( Extra, Selection, Context ): Core\PriceResult`, `price_previews( Extra, Context ): array` (`key => Core\PriceResult`), `has_active_extras( string $location ): bool`, `is_supported( Extra ): bool`. |
| `Core\PriceResult` | Value object returned by `extra.service`: public properties `amount`, `taxable`, `tax_class`, `display_amount`, `tax`; `zero()`, `is_zero()`. |
| `Rules\RuleEvaluator` | Class of the `rules.evaluator` service (add-ons may type constructor arguments with it): `passes( array $conditions, Context $context ): bool`, `has_unknown( array $conditions ): bool`. |
| `Core\Schema` | Service `schema`: `normalize_condition( array $condition ): array` and `normalize_pricing( array $pricing ): array` (normalise add-on data the same way as the Lite). |
| `Container`, `ServiceProviderInterface`, `Plugin` | `Container::set()`, `has()`, `get()`, `extend()`; `ServiceProviderInterface::register( Container )` and `boot( Container )` (see Bootstrapping); the global function `extrova(): Plugin` and `Plugin::container(): Container` (the container outside a provider). |
| Services | `context.factory` (`Core\ContextFactory`), `extra.service` (`Core\ExtraService`), `rules.evaluator` (`Rules\RuleEvaluator`), `schema` (`Core\Schema`), `registry.*` (`Registry\ConditionRegistry` members), `repository` (`Core\ExtraRepositoryInterface`): the members listed in this table. |

## Data

- Option `extrova_extras`: `{ schema_version, revision, extras[] }` (autoload off). `meta.pro.*` is reserved for the Pro add-on.
- Option `extrova_settings`, option `extrova_version`, user meta `extrova_welcome_dismissed`.
- Session key `extrova_selections`.
- Fees: id `extrova_{extra_id}`. Fee item meta: `_extrova_extra_id`, `_extrova_type`, `_extrova_value`.
- Order meta (CRUD/HPOS): `_extrova_extras` (snapshot v1) and one `_extrova_extra_id` row per accepted extra (usable in `wc_get_orders()` `meta_query`).

## Store API

- Cart response: `extensions['extrova']` = `{ extras[], selections{}, goals[], errors{}, display }`. Prices in minor units; `extras[].prices` keys: `base`, option ids, tip presets.
- Update: `extensionCartUpdate( { namespace: 'extrova', data: { action: 'set'|'clear'|'resync', selections: { ext_id: value } } } )`.

## JavaScript hooks (`wp.hooks`)

| Hook | Type | Use |
|---|---|---|
| `extrova.frontend.typeComponents` | filter `( map ) => map` | `type => Component` for the Checkout block. Component props: `extra, value, onChange( value, { debounce } ), onCommit(), ids, currency, invalid`. Optional static `usesFieldset( extra )`. |
| `extrova.frontend.beforeUpdate` | filter `( data ) => data` | Payload of `extensionCartUpdate`. |
| `extrova.frontend.resyncTriggers` | filter `( deps[] ) => deps[]` | When it includes `payment_method`, a `resync` request is sent when the active payment method changes. |
| `extrova.admin.extraTypes` | filter | `[ { value, label, Config, defaults, hasPricing( extra ), taxableByDefault } ]` |
| `extrova.admin.pricingMethods` | filter | `[ { value, label, Fields? } ]` |
| `extrova.admin.conditionTypes` | filter | `[ { value, label, Component, defaults, isProMarker? } ]` (replace the Pro markers) |
| `extrova.admin.locations` | filter | `[ { value, label } ]` |
| `extrova.admin.tabs` | filter | `[ { name, title, Component } ]` |
| Slots `ExtrovaExtraEditorAfter`, `ExtrovaSettingsAfter` | SlotFill | `fillProps`: `{ extra, setExtra }` / `{ settings, setSettings }`. Register fills with `registerPlugin( name, { scope: 'extrova', render } )`. |

Script handles to depend on: `extrova-checkout-frontend`, `extrova-admin`, `extrova-classic`.

## Templates

Override in `yourtheme/extrova/`: `classic/extras.php`, `classic/free-shipping-bar.php`, `order/order-extras.php`, `emails/order-extras.php`, `emails/plain/order-extras.php`.
