# Donation Receipts (Brevo) – MU‑Plugin Build Spec for Bedrock/Sage

A small MU‑plugin that lets admins find WooCommerce **donation** orders and send branded, trackable **donation receipts** via **Brevo (Sendinblue) Transactional**. Includes delivery/bounce tracking via Brevo webhooks, an admin table with filters + bulk send, and an optional auto‑send on order status.

> Scope: business logic lives in a plugin (not the Sage theme). Compatible with Bedrock (Composer), WooCommerce, and Action Scheduler.

---

## Goals
- Select customers with donation orders and **send receipts** (manual & auto)
- **Track** status: `sent`, `delivered`, `soft_bounced`, `hard_bounced`, `complained`, `opened`, `clicked`, `error`
- **Editable templates** (HTML + text) with variables (name, amount, org, order number/date)
- **Brevo** API for send; **Brevo transactional webhook** for events

---

## Project Layout (Bedrock)
```
app/mu-plugins/donation-receipts/
├─ donation-receipts.php              # bootstrap (plugin header, includes)
├─ src/
│  ├─ Plugin.php                      # boot order, hooks
│  ├─ Admin/
│  │  ├─ Menu.php                     # add pages/tabs
│  │  ├─ ListTable.php                # WP_List_Table for receipts
│  │  ├─ Settings.php                 # org/from/reply-to, Brevo key, region, etc.
│  │  └─ TemplateEditor.php           # code editor (HTML/Text)
│  ├─ Domain/
│  │  ├─ Detector.php                 # isDonationOrder(), donationTotal()
│  │  ├─ ReceiptService.php           # render, queue, send via Brevo
│  │  ├─ Events.php                   # map webhook -> statuses
│  │  └─ Repo.php                     # CRUD for custom table
│  ├─ Infra/
│  │  ├─ BrevoClient.php              # thin wrapper around getbrevo/brevo-php
│  │  ├─ ActionSchedulerJobs.php      # async jobs
│  │  ├─ Rest.php                     # REST routes (webhook + previews)
│  │  └─ DB.php                       # dbDelta install/upgrade
│  └─ Utils/
│     ├─ Replacer.php                 # {{variables}} -> values
│     └─ Sanitizers.php
├─ views/
│  ├─ email-html.php                  # fallback PHP template (or Blade if Acorn detected)
│  └─ email-text.php
├─ assets/
│  └─ admin.css
├─ readme.md
└─ composer.json                      # local autoload (PSR-4) if desired
```

---

## Composer Requirements
Add to the **root Bedrock** `composer.json` (or plugin-level composer if you prefer):

```json
{
  "require": {
    "getbrevo/brevo-php": "^1.0"
  }
}
```

Then:
```bash
composer update getbrevo/brevo-php
```

---

## Settings & Secrets
Create a plugin settings screen (Capability: `manage_woocommerce`).

- **Organization Name** (string)
- **From Email** / **From Name**
- **Reply-To** (optional)
- **Brevo API Key** (stored hashed/obscured; use `ApplicationPassword` field type or `update_option` w/ `autoload=no`)
- **Auto-send on status**: `[ ] processing` `[x] completed`
- **Donation identification**: by **Category** `donation` (default) or product meta key `_is_donation` = `yes`
- **Templates**:
  - Subject: `Thanks for your donation (Order {{order_number}})`
  - HTML body (CodeMirror/textarea)
  - Text body

### Available Template Variables
```
{{recipient_name}}
{{recipient_email}}
{{donation_amount}}
{{donation_currency}}
{{organization_name}}
{{order_number}}
{{donation_date}}
{{site_name}}
{{site_url}}
```

---

## Database Table
Create on activation/boot (dbDelta). Use Bedrock prefix from `$wpdb->prefix`.

**Table**: `{prefix}donation_receipt_emails`

Columns:
- `id` BIGINT unsigned PK
- `order_id` BIGINT unsigned INDEX
- `customer_id` BIGINT unsigned INDEX NULL
- `email` VARCHAR(190) INDEX
- `status` VARCHAR(32) INDEX  (enum-like)
- `message_id` VARCHAR(128) UNIQUE NULL  (Brevo `messageId`)
- `payload` LONGTEXT NULL     (json of last event)
- `sent_at` DATETIME NULL
- `updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
- `meta` LONGTEXT NULL        (json – template id, subject, etc.)

---

## Donation Detection
Two supported strategies (configurable):
1) **Category**: any order with line items in product category **donation**
2) **Meta**: product meta `_is_donation = yes`

Helpers:
```php
bool isDonationOrder(int $order_id);
float donationTotal(int $order_id); // sum donation line items only
```

---

## Send Flow (Action Scheduler)
- Hook: `woocommerce_order_status_completed` (and/or `processing`) → if donation → queue `dr_send_receipt_job($order_id)`
- Manual send/resend from admin table also queues the job
- Job steps:
  1. Build context (`recipient_name`, `amount`, etc.)
  2. Render templates (HTML/Text)
  3. Call **Brevo** `POST /v3/smtp/email` with subject/body (or template ID + params)
  4. Persist `status=sent`, `message_id`, `sent_at`

---

## Brevo Integration
### Minimal Client Wrapper
```php
namespace DR\Infra;

use Brevo\Api\TransactionalEmailsApi;
use Brevo\Configuration;
use Brevo\Model\SendSmtpEmail;

class BrevoClient {
  private TransactionalEmailsApi $api;

  public function __construct(string $apiKey) {
    $config = Configuration::getDefaultConfiguration()->setApiKey('api-key', $apiKey);
    $this->api = new TransactionalEmailsApi(null, $config);
  }

  /** @return array{messageId?:string} */
  public function send(string $toEmail, string $toName, string $subject, string $html, string $text, string $fromEmail, string $fromName, ?array $params = null): array {
    $email = new SendSmtpEmail([
      'to'       => [[ 'email' => $toEmail, 'name' => $toName ]],
      'sender'   => [ 'email' => $fromEmail, 'name' => $fromName ],
      'subject'  => $subject,
      'htmlContent' => $html,
      'textContent' => $text,
      'params'   => $params ?? []
    ]);
    $resp = $this->api->sendTransacEmail($email);
    return [ 'messageId' => $resp->getMessageId() ];
  }
}
```

> If you prefer **Brevo templates**, store a `template_id` in settings and call `SendSmtpEmail(['templateId' => 123, 'params' => [...]])` instead of passing html/text.

---

## Webhook (Transactional Events)
Create a public **REST route** to receive Brevo events.

```
Route:  /wp-json/dr/v1/brevo-hook
Method: POST
Auth:   optional secret query param ?key=XXXXX or Basic Auth; also allow IP allowlist if you use a WAF.
```

Controller responsibilities:
- Accept a **batch** of events (Brevo posts arrays)
- For each item, extract `messageId`, `event`, `ts` and **map to status**
- Update row by `message_id`; persist `payload`

**Event → Status mapping** (examples):
- `delivered` → `delivered`
- `soft_bounce` → `soft_bounced`
- `hard_bounce` → `hard_bounced`
- `spam` → `complained`
- `opened` → `opened` (first open can be captured as `opened_first_at` if you add the column)
- `click` → `clicked`
- `error`/`blocked`/`invalid_email` → `error`

Add a small SNS‑style **"subscription test"** handler: if payload contains `event === 'test'`, return 200.

---

## Admin UI
Menu: **WooCommerce → Donation Receipts**

**Tabs**
1) **Receipts** (table)
   - Filters: date range (order date), min/max amount, product/category, customer email, status
   - Columns: Order #, Customer, Email, Amount, Status, Sent At, Last Update, Actions
   - Row Actions: Preview, Send/Resend, View Order
   - Bulk Actions: Send, Resend, Export CSV

2) **Settings** (as above)

3) **Templates** (subject/html/text with preview & test send)

**Preview & Test Send**
- AJAX endpoint renders with sample order data; “Send test to me” uses WP user email

---

## Hooks
```php
// Auto-queue when completed/processing
add_action('woocommerce_order_status_completed', 'DR\\Domain\\Events::maybe_queue_receipt', 10, 1);
add_action('woocommerce_order_status_processing', 'DR\\Domain\\Events::maybe_queue_receipt', 10, 1);

// Action Scheduler job
add_action('dr_send_receipt_job', 'DR\\Domain\\ReceiptService::handle', 10, 1);
```

---

## Public API (for Codex & tests)
```php
// Queue a receipt for an order (no-op if not a donation)
DR\Domain\ReceiptService::queue(int $order_id): void;

// Force send immediately (sync; use in CLI/tests only)
DR\Domain\ReceiptService::sendNow(int $order_id): array; // returns ['messageId' => '...']

// Repository helpers
DR\Domain\Repo::upsertByMessageId(string $messageId, array $data): void;
DR\Domain\Repo::findByOrder(int $order_id): array; // latest or all
```

---

## WP‑CLI (optional)
```bash
wp dr receipts find --email=foo@bar.com --since=2025-01-01
wp dr receipts send 12345              # order id
wp dr receipts resend --order=12345
wp dr receipts export --since=2025-01-01 > receipts.csv
```

---

## Security Notes
- Capability‑gate all admin routes: `manage_woocommerce`
- Nonces on all form submissions
- Sanitize templates; allowlist HTML tags or use KSES with a widened rule set
- Webhook: require secret or Basic Auth; rate‑limit; log failures

---

## Minimal Bootstrap (outline)
```php
// app/mu-plugins/donation-receipts/donation-receipts.php
/**
 * Plugin Name: Donation Receipts (Brevo)
 * Description: WooCommerce donation receipt sender with Brevo + webhook tracking.
 */

require __DIR__ . '/vendor/autoload.php'; // if you ship a plugin-local composer

// Fallback simple loader if you autoload via Bedrock root composer
spl_autoload_register(function($class){
  if (strpos($class, 'DR\\') !== 0) return;
  $path = __DIR__ . '/src/' . str_replace('\\', '/', substr($class, 3)) . '.php';
  if (file_exists($path)) require $path;
});

add_action('plugins_loaded', ['\\DR\\Plugin', 'boot']);
```

---

## Acceptance Checklist
- [ ] Identify donation orders (category/meta) accurately
- [ ] Admin table lists donation orders with filters
- [ ] Manual send/resend works; records messageId
- [ ] Auto-send on order status (toggle) queues job
- [ ] Brevo webhook updates status to delivered/bounced/etc.
- [ ] Templates editable; variables render correctly
- [ ] Preview + test send works
- [ ] CSV export works
- [ ] Roles/permissions enforced
- [ ] Error handling + retries on API failure

---

## Stretch
- PDF receipt attached (Dompdf/Browsershot)
- Sequenced reminders (e.g., when order placed but no receipt yet)
- Multi‑org headers/signatures per product/category
- Per‑template A/B subject/body

---

## Dev Notes (Bedrock/Sage niceties)
- If Acorn is present, you can render **Blade** templates inside the plugin; keep a PHP fallback so the plugin doesn’t hard‑depend on Sage
- Keep business logic isolated from theme; only expose shortcodes/blocks if you later want a front‑end “Resend my receipt” portal
- Use Woo’s **Action Scheduler** for all network calls to keep checkout paths fast

---

## Pseudocode – Key Pieces
### Queue on status change
```php
function maybe_queue_receipt(int $order_id){
  if (!Detector::isDonationOrder($order_id)) return;
  as_enqueue_async_action('dr_send_receipt_job', [$order_id], 'dr');
}
```

### Send job
```php
function handle(int $order_id){
  $order = wc_get_order($order_id);
  $email = $order->get_billing_email();
  $name  = trim($order->get_billing_first_name().' '.$order->get_billing_last_name());
  $amount= Detector::donationTotal($order_id);

  [$subject,$html,$text,$params] = TemplateRenderer::build($order, $amount);

  $client = new BrevoClient(Settings::apiKey());
  $res    = $client->send($email,$name,$subject,$html,$text, Settings::fromEmail(), Settings::fromName(), $params);

  Repo::logSent($order_id, $email, $res['messageId'] ?? null, [ 'subject' => $subject ]);
  $order->add_order_note('Donation receipt emailed'.($res['messageId']?' ('.$res['messageId'].')':''));
}
```

### Webhook route
```php
register_rest_route('dr/v1', '/brevo-hook', [
  'methods'  => 'POST',
  'callback' => function($req){
    if (!Auth::check($req)) return new WP_Error('forbidden', 'Forbidden', ['status'=>403]);
    $events = json_decode($req->get_body(), true);
    if (!is_array($events)) $events = [$req->get_json_params()];
    foreach ($events as $e) {
      $msgId = $e['message-id'] ?? $e['messageId'] ?? null;
      $status= Events::map($e['event'] ?? $e['eventType'] ?? '');
      if ($msgId && $status) Repo::updateStatusByMessageId($msgId, $status, $e);
    }
    return [ 'ok' => true ];
  },
  'permission_callback' => '__return_true'
]);
```

---

## Hand‑off to Codex
Tasks Codex can pick up directly:
1. Scaffold files from this spec
2. Implement `DB::install()` and run on boot
3. Build `Detector::isDonationOrder()` and `donationTotal()`
4. Implement `ReceiptService::queue()` + `handle()`
5. Implement `BrevoClient::send()` (template or raw HTML)
6. Admin pages: Settings, Templates, Receipts (`WP_List_Table`)
7. REST route for webhook + mapping
8. CSV export for Receipts table
9. Unit tests for mapping + detector

> Done. Drop this folder in `app/mu-plugins/`, run `composer update`, configure Brevo key + webhook, and you’re ready to iterate.

