# Extending Groups Discount for WooCommerce

This plugin exposes a small, stable API so any membership / user-segmentation
plugin can plug in as a **discount audience source**. Built-in providers are
WordPress Roles and Groups; everything else (Paid Memberships Pro, MemberPress,
WooCommerce Memberships, Restrict Content Pro, Ultimate Member, custom systems)
can be added as an add-on without forking the plugin.

This guide is aimed at developers shipping a paid or private add-on.

---

## TL;DR

1. Implement `WGD_Audience_Provider` (5 methods).
2. Register your provider via the `wgd_audience_providers` filter.
3. That's it — your audiences appear in the **Audiences** tab and in the
   **Categories** matrix automatically. Discounts are computed by core.

```php
add_filter( 'wgd_audience_providers', function ( array $providers ) {
    $providers[] = new My_Awesome_WGD_Provider();
    return $providers;
} );
```

---

## The contract: `WGD_Audience_Provider`

```php
interface WGD_Audience_Provider {
    public function get_id();                                     // string slug
    public function get_label();                                  // human label
    public function is_available();                               // bool
    public function list_audiences();                             // array of {id, name}
    public function get_user_audiences( $user_id );               // array of {id, name}
}
```

### `get_id()`

A unique slug. Used as part of the option key, so keep it short, lowercase and
URL-safe (`a-z0-9_`). Examples: `pmpro`, `memberpress`, `wc_memberships`,
`my_company_crm`.

> **Never reuse** a built-in id (`wp_roles`, `groups`) — they'll be silently
> overwritten.

### `get_label()`

Display name shown in the admin UI (provider header in the Audiences tab,
column header in the Categories matrix, status pill in the Add-ons tab). Use
`__()` so it is translatable.

### `is_available()`

Return `true` only if the underlying source can actually be queried. Typical
implementations check that the partner plugin's main class/function exists:

```php
public function is_available() {
    return defined( 'PMPRO_VERSION' ) && function_exists( 'pmpro_getAllLevels' );
}
```

When `is_available()` returns `false` the provider disappears from the UI but
its option keys are preserved (re-enabling the partner plugin restores
everything).

### `list_audiences()`

Returns the **full catalogue** the admin can configure discounts for. Used to
render:

* the per-audience inputs in the Audiences tab
* the columns in the Categories matrix

Each entry must be a *plain object* (or `stdClass`) with at minimum:

| Field | Type | Notes |
|---|---|---|
| `id` | `int\|string` | Stable identifier. Used in option keys. |
| `name` | `string` | Translated, human-readable label. |

Extra fields are allowed but currently ignored — feel free to attach
provider-specific metadata if your add-on uses it internally.

### `get_user_audiences( $user_id )`

Returns the subset of `list_audiences()` the given user belongs to, **in
priority order** (first item wins when the *"first / last"* tie-break is used).
Performance matters — this runs on every product price render for logged-in
users. Cache aggressively when the source plugin allows it.

---

## Where data lives

Audience configuration is stored as plain `wp_options`. You **don't** read or
write these yourself — core does it. But it's useful to know the format if you
ever need to debug, export or migrate data:

| Setting | Option key |
|---|---|
| Base discount per audience | `wgd-aud-{provider_id}-{audience_id}` |
| Per-category override | `wgd-cat-{category_id}-{provider_id}-{audience_id}` |

`{provider_id}` and `{audience_id}` are normalised through
`WGD_Audience_Registry::sanitize_key_segment()` (lowercase, `a-z0-9_-` only).
Picking IDs that match this naturally avoids surprises.

---

## Worked example: Paid Memberships Pro

```php
<?php
/**
 * Plugin Name: Groups Discount for WooCommerce — PMPro
 * Requires Plugins: woo-groups-discount, paid-memberships-pro
 */

defined( 'ABSPATH' ) || exit;

add_filter( 'wgd_audience_providers', function ( array $providers ) {
    $providers[] = new WGD_Provider_PMPro();
    return $providers;
} );

class WGD_Provider_PMPro implements WGD_Audience_Provider {

    public function get_id() {
        return 'pmpro';
    }

    public function get_label() {
        return __( 'Paid Memberships Pro', 'wgd-pmpro' );
    }

    public function is_available() {
        return defined( 'PMPRO_VERSION' )
            && function_exists( 'pmpro_getAllLevels' )
            && function_exists( 'pmpro_getMembershipLevelsForUser' );
    }

    public function list_audiences() {
        if ( ! $this->is_available() ) {
            return array();
        }
        $levels = pmpro_getAllLevels( true, true ); // include hidden, with names
        $out    = array();
        foreach ( (array) $levels as $level ) {
            $out[] = (object) array(
                'id'   => (int) $level->id,
                'name' => $level->name,
            );
        }
        return $out;
    }

    public function get_user_audiences( $user_id ) {
        if ( ! $this->is_available() ) {
            return array();
        }
        $levels = pmpro_getMembershipLevelsForUser( $user_id );
        $out    = array();
        foreach ( (array) $levels as $level ) {
            $out[] = (object) array(
                'id'   => (int) $level->id,
                'name' => $level->name,
            );
        }
        return $out;
    }
}
```

That's the entire add-on. Ship it as its own plugin (so users can install/
update it independently), declare a `Requires Plugins:` header so WordPress
guards activation order, and you're done.

---

## Worked example: a fully custom source

If your audiences come from somewhere exotic — an external CRM, a custom user
meta field, a SaaS API — the shape is identical. The only thing that changes
is `get_user_audiences()`:

```php
public function get_user_audiences( $user_id ) {
    // Imagine each user has one or more "tier" values stored in user meta.
    $tiers = get_user_meta( $user_id, 'crm_tiers', true );
    if ( ! is_array( $tiers ) ) {
        return array();
    }
    $out = array();
    foreach ( $tiers as $tier_id ) {
        $out[] = (object) array(
            'id'   => $tier_id,
            'name' => $this->tier_label( $tier_id ),
        );
    }
    return $out;
}
```

`list_audiences()` would return the full tier catalogue, fetched from your
API (cached with `wp_cache_*` or a transient).

---

## Additional extension points

The plugin ships a few filters that complement the provider interface — useful
if your add-on does more than expose audiences:

### `wgd_user_audiences`

Modify the **flattened** list of audiences a user belongs to, after every
provider has been queried. Use this to:

* hide audiences in certain contexts (geo, A/B test, debug-as-user feature)
* inject synthetic audiences without registering a full provider

```php
add_filter( 'wgd_user_audiences', function ( array $audiences, $user_id ) {
    if ( my_user_is_admin_previewing_as_vip( $user_id ) ) {
        $audiences[] = array(
            'provider' => 'groups',
            'id'       => 5,
            'name'     => 'VIP (preview)',
        );
    }
    return $audiences;
}, 10, 2 );
```

### `wgd_discount_value`

Final adjustment of the computed price right before it is returned. Receives
the discounted price, the original price and the product. Use sparingly — this
runs on every product view.

```php
add_filter( 'wgd_discount_value', function ( $price, $original, $product ) {
    // Floor at 1 EUR — never sell below the floor regardless of audience.
    return max( 1.0, (float) $price );
}, 10, 3 );
```

### `wgd_addons`

Promotes your add-on in the **Add-ons** tab of the core plugin. Useful for
in-store cross-sells or to surface a free CTA for an early-access add-on.

```php
add_filter( 'wgd_addons', function ( array $addons ) {
    $addons[] = array(
        'slug'        => 'my_addon',
        'name'        => 'My Audience Source',
        'description' => 'Plug your CRM tiers into discount audiences.',
        'url'         => 'https://example.com/buy',
    );
    return $addons;
} );
```

When a provider with the same `slug` is already registered, the card is
automatically marked as "Active" and the CTA is hidden.

---

## Testing your add-on

A short smoke checklist for any new provider:

1. **Activation order** — install and activate the partner plugin, then the
   core plugin, then your add-on. Confirm your audiences appear in
   `WooCommerce → Groups Discount → Audiences`.
2. **Deactivate the partner plugin** — confirm `is_available()` returns
   `false` and your section disappears from the UI without errors.
3. **Set a discount** for one audience and verify the price changes for a
   logged-in user belonging to that audience.
4. **Multi-membership user** — assign a user to two audiences across two
   providers, then exercise each `wgd-ifseveral` policy (higher / lower /
   first / last).
5. **Category override** — set a per-category value for your audience and
   confirm it takes precedence over the audience default.
6. **Uninstall** — confirm your add-on cleans up its own option keys on
   `register_uninstall_hook`. Core options (`wgd-aud-{your_id}-*`) are *yours*
   to remove.

---

## Naming, packaging, distribution

* Prefix your classes (`WGD_Provider_*` is reserved for built-ins — use your
  own prefix like `MyCompany_WGD_Provider_*`).
* Ship as a standalone plugin, never as a mu-plugin or a snippet inside the
  partner plugin.
* Declare both partner and core in `Requires Plugins:` so WP gates activation.
* Bundle PHP 7.2+ syntax — core targets the same minimum.

---

## Roadmap & feedback

If you build a provider you'd like considered for the official Add-ons list
(promoted in the in-plugin Add-ons tab via `wgd_addons`), open an issue or
contact us at https://www.eggemplo.com/.

Pull requests for new built-in providers are **not** accepted — by design, the
core ships with only WordPress Roles and Groups, and every other source lives
as an independent add-on.
