# 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.

## 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
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.
