# Phase 2 — Subscriptions and effective entitlements

## Status

The development subscription for `absolutems` is active. The provider-neutral
billing foundation is implemented; a live provider adapter is intentionally
not selected yet.

## Source of truth

The Flask control plane is authoritative for:

- subscription status and billing period;
- plan metadata;
- application catalog;
- applications enabled for each tenant;
- storage, AI token, request and job limits.

WordPress receives a signed snapshot through:

`GET /api/v1/platform/entitlements`

The snapshot is cached in the non-autoloaded WordPress option
`ams_wp_flask_entitlements`. WordPress uses it only for UI and scope requests;
Flask always rechecks the live subscription and app binding before dispatch.

## Enforcement

An external app request requires all of:

1. valid site JWT and unused `jti`;
2. active or trialing subscription;
3. enabled `tenant_apps` binding;
4. matching `app:<app_id>:invoke` scope.

An inactive subscription disables every app even if a stale WordPress snapshot
still says it is enabled.

## AbsoluteMS development plan

Enabled apps:

- ABN Lookup
- Leave Forms
- WP Invoices

Not included examples:

- AroFlo Connector
- Invoice Mail Intake
- Voice Ordering

Limits:

- storage: 1024 MB
- AI tokens: 100,000/month
- requests: 120/minute
- WP Invoices jobs: 1,000/month, tracked

## Operations

Apply additive schema:

```bash
venv/bin/python scripts/migrate_control_plane.py
```

Seed the AbsoluteMS development subscription:

```bash
venv/bin/python scripts/seed_absolutems_entitlements.py
```

Synchronise WordPress:

```bash
wp ams-bridge sync-entitlements \
  --path=/var/www/html/absolutems \
  --allow-root
```

Verify enforcement:

```bash
wp ams-bridge app-access-test \
  --path=/var/www/html/absolutems \
  --allow-root
```

Expected:

- enabled ABN Lookup test: HTTP 200
- non-entitled AroFlo Connector test: HTTP 403

## WordPress UI

All new visible copy is in English.

- Dashboard shows subscription status, plan, enabled-app count and last sync.
- Settings provides **Sync subscription and apps**.
- Apps renders the real catalog and marks each app as **Enabled** or
  **Not included**.
- Plan limits are displayed from the signed Flask response.

## Next work

- Select and connect the live payment provider.
- Configure provider price-to-plan mappings.
- Translate the provider's native webhook signature and payload into the
  platform adapter contract.

## Billing adapter contract

Endpoint:

`POST /api/v1/platform/billing/webhooks/<provider>`

Required headers:

- `X-Billing-Timestamp`: current Unix timestamp;
- `X-Billing-Signature`: HMAC-SHA256 of `<timestamp>.<raw-body>`.

The secret is read from `BILLING_WEBHOOK_SECRET_<PROVIDER>`. The accepted clock
window is five minutes. Provider event IDs are persisted and processed only
once; reuse with different content is rejected.

Supported subscription statuses are `active`, `trialing`, `past_due`,
`canceled`, `paused`, and `unpaid`. A `past_due` event receives a configurable
grace period from `BILLING_GRACE_PERIOD_DAYS` (default: three days). Once the
grace period expires, app dispatch is denied by the normal entitlement check.

Price IDs must exist in `billing_product_mappings`; an unknown price cannot
silently grant a plan.

## Stripe preview adapter

Stripe has a native adapter at:

`POST /api/v1/platform/billing/webhooks/stripe`

It validates the standard `Stripe-Signature` header with
`STRIPE_WEBHOOK_SECRET` and translates Stripe subscription events into the
internal contract. Initially supported native events:

- `customer.subscription.created`;
- `customer.subscription.updated`;
- `customer.subscription.deleted`.

Stripe configuration uses `STRIPE_SECRET_KEY`, `STRIPE_PUBLISHABLE_KEY`, and
`STRIPE_WEBHOOK_SECRET`. Safe readiness metadata is available through the
JWT-protected `GET /api/v1/platform/billing/status`; key values are never
returned.

The AbsoluteMS development connection remains non-billable in `trialing`
status. Customer/subscription IDs and price mappings remain empty until real
prices and Stripe test-mode resources are deliberately created.
