# Niiwaa Commercial Offers WordPress Plugin

This plugin is the WordPress receiver and presentation layer for the public
Niiwaa and Niixam commercial catalog. The Symfony application pushes a signed,
versioned projection to WordPress. The shortcode only reads that local
projection; it never makes a synchronous request to the application and it
never contains editable commercial values.

## Installation and configuration

1. Create a zip archive from `integrations/wordpress` that retains the
   `niiwaa-commercial-offers/` top-level directory.
2. Install and activate it on a non-production WordPress copy.
3. Define the shared HMAC secret outside the WordPress database, preferably in
   `wp-config.php`:

   ```php
   define('NIIWAA_COMMERCIAL_OFFERS_API_SECRET', 'replace-with-a-long-random-secret');
   ```

   The `NIIWAA_COMMERCIAL_OFFERS_API_SECRET` environment variable is also
   supported when PHP-FPM exposes it. The secret is never accepted through the
   administration interface, stored in options, or sent to browsers.
4. In **Settings > Niiwaa Offers**, set only the HMAC key ID. It must match the
   application key map. There is no WordPress-side application URL, timeout,
   refresh button, or cache setting in the push architecture.

Existing pre-push settings containing endpoint or cache fields are safely
ignored when the key ID is next saved. Deactivation and uninstall intentionally
preserve the key ID and last validated projection; remove them manually only
when this integration is retired.

## Push endpoint and authentication

The application posts to:

```text
POST https://wordpress.example/wp-json/niiwaa-commercial-offers/v1/catalog
```

It must supply these headers:

| Header | Requirement |
| --- | --- |
| `X-Niiwaa-Key-Id` | Exact configured key ID |
| `X-Niiwaa-Timestamp` | Unix seconds, within 300 seconds |
| `X-Niiwaa-Nonce` | 32–128 `[A-Za-z0-9_-]` characters, single-use for 300 seconds |
| `X-Niiwaa-Signature` | Lowercase SHA-256 HMAC hex |

The canonical input is exactly:

```text
v1\nPOST\n/wp-json/niiwaa-commercial-offers/v1/catalog\n{timestamp}\n{nonce}\n{sha256(raw body)}
```

The signature is `hash_hmac('sha256', canonical, secret)`. The receiver uses
constant-time comparison, rejects stale or malformed authentication, and claims
the nonce atomically before processing the payload. It returns no authentication
detail to unauthenticated callers.

## Payload, idempotency, and acknowledgement

The request body is strict JSON with this outer envelope:

```json
{
  "schemaVersion": "1",
  "revision": 42,
  "generatedAt": "2026-07-23T10:00:00+00:00",
  "catalogs": {"fr": {"...": "schema version 2 catalog"}, "en": {"...": "schema version 2 catalog"}}
}
```

Both embedded catalogs must be present, have schema version `"2"`, match their
locale, and contain only the documented offer, price, fixed-30-day credit, and
discount fields. Unknown, missing, malformed, oversized, or non-UTC fields
reject the complete push with `400`.

The SHA-256 digest of the exact raw body is persisted with the revision:

- A greater revision atomically replaces the whole `fr`/`en` projection and
  returns `200 {"status":"accepted","revision":...,"digest":"..."}`.
- The same revision and digest is an idempotent retry and returns `200` with
  `status: "unchanged"`.
- A lower revision, or the same revision with another digest, returns `409` and
  leaves the stored projection unchanged.

The projection is stored in one non-autoloaded WordPress option. The receiver
locks the option row in a database transaction, so a stale retry cannot replace
a newer accepted projection.

## Shortcode and Elementor

Add an Elementor **Shortcode** widget with either:

```text
[niiwaa_commercial_offers locale="fr"]
[niiwaa_commercial_offers locale="en" product="niixam"]
```

The shortcode reads the last accepted local projection. Until the first valid
push, it renders a controlled unavailable message instead of fallback prices.
Offer labels, amounts, fixed-30-day credits, and discounts always originate
from Symfony. The output is escaped, semantic, and carries the selected `lang`
attribute for French and English accessibility.

Offers are grouped by product so the combined Niiwaa + Niixam bundles cannot be
confused with standalone Niixam plans. Each offer links to the WordPress quote
form with its stable offer code, and the footer links existing customers to the
localized production application login. These navigation destinations are
presentation concerns; commercial values remain exclusively Symfony-owned.

## Validation and packaging

Run from `integrations/wordpress`:

```bash
find niiwaa-commercial-offers -name '*.php' -print0 | xargs -0 -n1 php -l
php niiwaa-commercial-offers/tests/ValidatorTest.php
php niiwaa-commercial-offers/tests/RendererTest.php
zip -r niiwaa-commercial-offers.zip niiwaa-commercial-offers -x '*.DS_Store'
unzip -l niiwaa-commercial-offers.zip
```

The dependency-free test checks the strict payload boundary. It does not prove
WordPress REST routing, database locking, or a live deployment; qualify those
on a non-production WordPress copy before enabling the application publisher.
