# Deploy Checklist — Social Engine v1.0

Run top to bottom. Each box should be ticked before moving on. Estimated total time: 3 hours active + 4-8 weeks async wait for TikTok approval.

---

## Phase 0 — Prerequisites (5 min)

- [ ] Supabase CLI installed & logged in (`supabase login`)
- [ ] Project linked: `supabase link --project-ref jhihwzfikvndybbyaoul`
- [ ] You have a Personal Access Token saved as `SUPABASE_PAT` (already done per memory)
- [ ] Cloudflare Pages account access for `easyhotelrfp.com` and `app.easyhotelrfp.com`

---

## Phase 1 — Database (10 min)

- [ ] Open Supabase Dashboard → SQL Editor (project `jhihwzfikvndybbyaoul`)
- [ ] Paste & run `migrations/66_social_engine_foundation.sql`
- [ ] Verify smoke test NOTICE: `Mig 66 smoke test PASSED — 5 tables, 5 RPCs`
- [ ] Run sanity:
  ```sql
  SELECT count(*) FROM social_accounts;     -- 0
  SELECT count(*) FROM social_posts;         -- 0
  SELECT proname FROM pg_proc WHERE proname IN
    ('fetch_due_posts','mark_target_published','mark_target_failed','attribute_signup_to_post','stamp_paid_attribution');
  -- expect 5 rows
  ```

---

## Phase 2 — Create the 4 business accounts (90 min)

Follow `docs/00-SETUP-PLAYBOOK.md` end-to-end. Tick as you go:

- [ ] Facebook Page `Easy RFP` live (handle `@easyhotelrfp`)
- [ ] Instagram Business account `@easyhotelrfp` linked to FB Page
- [ ] LinkedIn Company Page `linkedin.com/company/easy-hotel-rfp` (domain meta tag deployed)
- [ ] TikTok Business account `@easyhotelrfp` registered with TikTok for Business
- [ ] All 4 cross-linked
- [ ] TikTok Developer App submitted for `video.publish` scope review (async wait begins)

---

## Phase 3 — API tokens (45 min)

Per `docs/00-SETUP-PLAYBOOK.md` §6:

- [ ] Meta Graph API: long-lived Page Access Token obtained
- [ ] LinkedIn API: 60-day access token obtained, Org URN noted
- [ ] TikTok API: access + refresh tokens obtained (UPLOAD scope only at this stage)

Save as Supabase secrets via dashboard or CLI:

```bash
supabase secrets set \
  META_GRAPH_PAGE_TOKEN="EAAxxxxx..." \
  META_FB_PAGE_ID="123456789" \
  META_IG_BUSINESS_ID="178987654321" \
  LINKEDIN_TOKEN="AQUxxxxx..." \
  LINKEDIN_ORG_URN="urn:li:organization:12345" \
  TIKTOK_ACCESS_TOKEN="act.xxxx" \
  TIKTOK_REFRESH_TOKEN="rft.xxxx" \
  PUBLIC_MEDIA_HOST="https://easyhotelrfp.com" \
  REMINDER_EMAIL_TO="contact@easyhotelrfp.com" \
  SOCIAL_TEST_MODE="true"
# RESEND_API_KEY and SUPABASE_SERVICE_ROLE_KEY are already set
```

- [ ] All secrets visible in Dashboard → Project Settings → Edge Functions → Secrets

---

## Phase 4 — Deploy edge functions (10 min)

```bash
cd edge-functions
supabase functions deploy social-publisher           --project-ref jhihwzfikvndybbyaoul
supabase functions deploy social-metrics-pull        --project-ref jhihwzfikvndybbyaoul
supabase functions deploy social-attribute-signup    --project-ref jhihwzfikvndybbyaoul
```

Verify each shows up at:
- `https://jhihwzfikvndybbyaoul.functions.supabase.co/social-publisher`
- `https://jhihwzfikvndybbyaoul.functions.supabase.co/social-metrics-pull`
- `https://jhihwzfikvndybbyaoul.functions.supabase.co/social-attribute-signup`

- [ ] All 3 functions deployed and reachable

---

## Phase 5 — Insert social_accounts rows (5 min)

Run in Supabase SQL Editor (replace external_ids with real values from Phase 3):

```sql
INSERT INTO social_accounts (network, handle, display_name, external_id, publish_enabled) VALUES
  ('facebook',  'easyhotelrfp', 'Easy RFP', '<META_FB_PAGE_ID>',     false),
  ('instagram', 'easyhotelrfp', 'Easy RFP', '<META_IG_BUSINESS_ID>', false),
  ('linkedin',  'easy-rfp',     'Easy RFP', '<LINKEDIN_ORG_URN>',    false),
  ('tiktok',    'easyhotelrfp', 'Easy RFP', '<TIKTOK_OPEN_ID>',      false);
```

Note `publish_enabled=false` for all. Master kill switch ON until you smoke-test.

- [ ] 4 rows in `social_accounts`

---

## Phase 6 — Schedule pg_cron jobs (5 min)

```sql
-- Publisher: every 5 minutes
SELECT cron.schedule(
  'social-publisher-tick',
  '*/5 * * * *',
  $$
    SELECT net.http_post(
      'https://jhihwzfikvndybbyaoul.functions.supabase.co/social-publisher',
      '{}'::jsonb,
      '{"Content-Type":"application/json","Authorization":"Bearer <SUPABASE_SERVICE_ROLE_KEY>"}'::jsonb
    );
  $$
);

-- Metrics pull: daily at 06:00 UTC (08:00 Tallinn)
SELECT cron.schedule(
  'social-metrics-pull-daily',
  '0 6 * * *',
  $$
    SELECT net.http_post(
      'https://jhihwzfikvndybbyaoul.functions.supabase.co/social-metrics-pull',
      '{}'::jsonb,
      '{"Content-Type":"application/json","Authorization":"Bearer <SUPABASE_SERVICE_ROLE_KEY>"}'::jsonb
    );
  $$
);
```

Verify: `SELECT * FROM cron.job WHERE jobname LIKE 'social-%';`

- [ ] 2 cron jobs scheduled

---

## Phase 7 — Auth hook (5 min)

Wire `social-attribute-signup` to fire on `auth.users` INSERT:

1. Supabase Dashboard → Authentication → Hooks
2. Add Hook: HTTP, on `User Created`
3. Endpoint: `https://jhihwzfikvndybbyaoul.functions.supabase.co/social-attribute-signup/auth-hook`
4. Secret: generate one and save as `SUPABASE_AUTH_HOOK_SECRET` in edge function secrets

- [ ] Hook live; test with a fresh signup, see row in `social_attribution`

Wire Stripe webhook to forward to `/stripe` route of the same function (or call `stamp_paid_attribution` from your existing `/stripe-webhook` function — whichever is cleaner). Existing Easy RFP webhook handler already exists per memory; just add the call.

- [ ] Stripe `customer.subscription.created` triggers `stamp_paid_attribution`

---

## Phase 8 — Deploy admin dashboard (10 min)

In your Cloudflare Pages source folder (`/site/`):

```
/site/app/admin/social/index.html
/site/app/admin/social/calendar.html
/site/app/admin/social/compose.html
/site/app/admin/social/analytics.html
/site/assets/utm-track.js
```

Reference `utm-track.js` from the public site `<head>`:
```html
<script defer src="/assets/utm-track.js?v=1"></script>
```

Inject Supabase env vars into the admin pages (Cloudflare Pages build):
```html
<meta name="sb-url"  content="https://jhihwzfikvndybbyaoul.supabase.co">
<meta name="sb-anon" content="<SUPABASE_ANON_KEY>">
```

Bump cache-buster `?v=` on the new admin pages per the May 3 cache-buster hard rule.

Add `/app/admin/social/*` to `_headers` no-cache list.

Deploy via Wrangler or Pages dashboard.

- [ ] `/app/admin/social/` loads, all 4 KPIs show 0
- [ ] Login gate works (admin email enforced)

---

## Phase 9 — Import the seed CSV (5 min)

1. Open `/app/admin/social/`
2. Click `Import CSV`
3. Pick `content/03-CONTENT-CALENDAR.csv`
4. Wait for "Imported 35 of 35 rows" alert

- [ ] 35 rows in `social_posts`, status `scheduled`
- [ ] Calendar view shows them on their dates

---

## Phase 10 — Smoke test (15 min)

**TEST_MODE smoke test (no real posting):**

1. Confirm `SOCIAL_TEST_MODE=true` in secrets
2. Open Compose, create a post scheduled for 2 minutes from now, fan out to all 4 networks
3. Wait 5 min for cron tick
4. Check edge function logs:
   ```bash
   supabase functions logs social-publisher --tail
   ```
5. You should see `[TEST_MODE] Would publish ...` lines, no errors
6. Check `social_post_targets` — they should be `published` with `platform_post_id` like `test_facebook_...`

- [ ] TEST_MODE smoke test passes (4 "would-publish" lines, 0 errors)

**Real smoke test (one network at a time):**

1. Flip `SOCIAL_TEST_MODE=false` in secrets
2. Set `social_accounts` row for Facebook only: `publish_enabled=true`
3. Schedule a single FB-only test post for 2 min ahead
4. Wait, verify it appears live on Facebook, `social_post_targets` has real `platform_post_id`
5. Repeat for Instagram, LinkedIn
6. For TikTok, verify the video lands in your Drafts inbox (UPLOAD mode) and you got the Resend reminder email

- [ ] FB live test passes
- [ ] IG live test passes
- [ ] LinkedIn live test passes
- [ ] TikTok upload + reminder email both work

**Attribution smoke test:**

1. Open `https://easyhotelrfp.com/?utm_source=instagram&utm_medium=organic_post&utm_campaign=test&utm_content=post_smoke_001&utm_term=en` in a private window
2. Sign up with a test email
3. Check `social_attribution` — should have a row with `source='instagram'`, `post_slug='post_smoke_001'`
4. (Optional) trigger a test Stripe sub for that user → `paid_at` and `mrr_eur` get stamped

- [ ] Attribution chain end-to-end works

---

## Phase 11 — Production-ready (5 min)

Once all smoke tests pass:

- [ ] Set all 4 `social_accounts.publish_enabled=true` (or just the ones you want to start with)
- [ ] Confirm `SOCIAL_TEST_MODE=false`
- [ ] Verify the next scheduled post in the calendar fires correctly
- [ ] Save a memory entry: "Social Engine v1.0 deployed YYYY-MM-DD"

🎉 Done.

---

## Operating notes

- **Token rotation**: LinkedIn token expires every 60 days. Set a calendar reminder. Refresh flow is in `social-publisher` README footer (TODO v1.1).
- **TikTok approval**: when `video.publish` is approved (4-8 weeks), update `social_accounts.tiktok_mode='direct_post'`. No code change needed.
- **Resend reminder spam**: while TikTok is in UPLOAD mode, you'll get one email per scheduled TikTok post. Manage by reducing TikTok cadence to 2-3 per week until approval comes through.
- **Pause everything**: flip `SOCIAL_TEST_MODE=true` — pauses all 4 networks instantly without unsetting per-account flags.

---

## Audit (run before declaring v1.0 done)

Per memory rule (Audit a cada sprint, May 3):

```sql
-- 1. Schema integrity
SELECT count(*) FROM information_schema.tables WHERE table_schema='public' AND table_name LIKE 'social_%';
-- expect: 5

-- 2. RLS enabled on all
SELECT tablename, rowsecurity FROM pg_tables WHERE schemaname='public' AND tablename LIKE 'social_%';
-- expect: 5 rows, all rowsecurity=true

-- 3. RPC count
SELECT count(*) FROM pg_proc p JOIN pg_namespace n ON n.oid=p.pronamespace
  WHERE n.nspname='public' AND p.proname IN
  ('fetch_due_posts','mark_target_published','mark_target_failed','attribute_signup_to_post','stamp_paid_attribution');
-- expect: 5

-- 4. Coherence guards working
-- This is a NEGATIVE test fixture — canonical truth is **hotels never pay** (planner is the only customer).
-- The CHECK constraints below should REJECT the bad caption pattern.
INSERT INTO social_posts (post_slug,campaign_tag,language,audience,format,caption)
VALUES ('post_999_en_test_bad','test','en','organizer','image','€45 launches later, hotels pay');
-- expect: ERROR (3 CHECK constraints fail)

-- 5. View accessible
SELECT * FROM v_social_post_roi LIMIT 1;
SELECT * FROM v_social_channel_roi LIMIT 1;
-- expect: empty rowset, no permission error
```

Frase obrigatória ao terminar:
> Audit Sprint Social Engine v1.0: 5 tabelas + 5 RPCs + 2 views + RLS = PASS. Coherence guards = PASS. TEST_MODE smoke = PASS. Live smoke FB/IG/LI = PASS. TikTok UPLOAD + reminder = PASS. Attribution chain end-to-end = PASS.
