- scripts/verify-pax8-auth.ts loads .env.local, calls getPax8Client().listCompanies(0,1), logs only counts/status (never token/secret)
- CLAUDE.md External integrations table gains a PAX8 row (PAX8_* prefix)
- INTEGRATIONS.md does not exist at repo root; edit skipped per plan instructions (noted in SUMMARY)
- isPax8Configured(): both PAX8_CLIENT_ID and PAX8_CLIENT_SECRET required
- getPax8Client(): throws exact error naming both env vars when missing;
caches singleton Pax8Client instance
- _resetPax8Client(): test seam to clear the cached singleton
- follows appgate-factory.ts / 10-RESEARCH.md Pattern 2 verbatim
- Six tables: pax8_companies, pax8_products, pax8_subscriptions,
pax8_orders, pax8_order_items, pax8_company_match_review
- Header/line-item design (D-01) with hard FK order_items->orders CASCADE
- Monetary NUMERIC(12,2) + currency CHAR(3) DEFAULT 'USD' (D-04)
- raw_payload JSONB safety net on all five non-review tables (D-03)
- pax8_company_match_review modeled field-for-field on device_link_review
(migration 080), with soft-ref BIGINT[] candidates and hard FK to
pax8_companies + nullable FK to companies(id)
- getToken() JSON-body OAuth2 client-credentials exchange with audience field
(deviates from msgraph-client.ts's form-encoded body per 10-RESEARCH.md Pitfall 3)
- 60s expiry-buffer token cache, reused across calls
- fetchJson<T>() with 429/Retry-After retry copied from msgraph-client.ts
- listCompanies() auth-proof call parsing the {content,page} envelope
- secret never interpolated into any throw/console call
Decision coverage gate requires an explicit D-NN citation inside a
plan's must_haves/truths block, not just body prose. D-04 was already
cited; D-01/D-02/D-03 were only mentioned in read_first/action text.
Populate 10-VALIDATION.md from RESEARCH.md's Validation Architecture
section (was an unfilled template), add RESOLVED/DEFERRED markers to
RESEARCH.md's Open Questions, and align pax8_subscriptions/order_items
product_id to UUID (matching pax8_products.id) to avoid a Phase 11 cast.
Phase directories (01-09.1) are fully preserved in git history at
prior commits; this just clears the working tree so v2.0 roadmapping
starts clean, per gsd-new-milestone's phases.clear step.
Plant two seeds (PAX8 sync + Autotask mapping, general Pulse data
assistant) and one research question (PAX8 company identifier
alternatives to fuzzy name matching), surfaced while scoping a
future PAX8 integration.
- Broaden mimecast retention from 30 days to 18 months rolling
- Re-enable mimecast-sync schedule (was disabled since March 17)
- Full sync triggered: 35,559 messages loaded for last 30 days
- Users list API: LATERAL join on mimecast_messages for emails_sent/received
- User detail API: add emails{d7,d30,d90} field from mimecast
- Engagement page: prefer mimecast email counts in detail panel sub-label
Graph API has 48-72hr reporting lag; mimecast is same-day
- Add getValidCompanyIds() helper mirroring getValidResourceIds()
- Add a new TICKETS validation block that nullifies ticket.company_id when
the referenced company is not present (is_deleted=false) in the Pulse
mirror, instead of letting tickets_company_id_fkey roll back the
bulkUpsert transaction
- Block runs after the existing recordsWithoutCompany filter and before
the existing resource-FK nullification block (correct ordering)
- Belt-and-suspenders on top of Task 1: covers hard-deleted-in-Autotask
companies that Task 1's widening still won't fetch
- Add buildCompaniesFilter() returning id > 0 in lib/utils/sync-helpers.ts
- Route COMPANIES through buildCompaniesFilter on full sync instead of
the generic buildActiveFilter (which applied isActive=true and missed
inactive companies with tickets, causing tickets_company_id_fkey on
weekly-full and full syncs since 2026-05-15)
- hasAppliedFilters stays true (filter is non-empty), so soft-delete of
companies is not triggered
- sync-scheduler.ts: extend sync_type union with 'tickets-reconcile', add a
default schedule entry (disabled, 30 4 * * *), and a dispatch case using
the device-link-reconcile / integration-health dynamic-import pattern.
- migrations/090_ticket_reconcile_schedule.sql: idempotent INSERT (ON CONFLICT
DO NOTHING) so existing installs pick up the row without disturbing the
fresh-DB default-seed path.
- New lib/services/ticket-reconciliation-service.ts: reconcileStaleTickets()
scans tickets where is_deleted=false AND status<>5 AND synced_at older than
7 days (capped at 500), re-fetches each from Autotask, and either upserts
via the webhook SQL pattern or soft-deletes when Autotask returns null.
- Returns { scanned, updated, statusFlippedToComplete, softDeleted, errors }.
- New POST /api/sync/reconcile-tickets — fire-and-forget trigger mirroring
/api/sync/incremental (public per existing middleware allowlist).
In dry-run mode the script makes zero QBO writes, so QboClient should not
be instantiated (which would throw if QBO_CLIENT_ID etc are unset).
Replace eager getQboClient() call with a lazy getClient() wrapper that
constructs QboClient only when the first live POST is about to be made.
- scripts/apply-fh-deposit.ts: parse .FH, validate per-check + total against mapping,
resolve invoices + deposit account from Postgres, post Payments + Deposit to QBO
- Idempotency via sibling .applied.json written after each successful QBO post
- --dry-run flag prints all payloads and makes zero QBO writes
- Check number normalization (strip leading zeros) handles bank-vs-remittance mismatch
- dev/fin/4183_mapping_20260518.json: 11 checks, $18,962.12 total, 2026-05-18 batch
- Add QboPaymentCreatePayload + QboDepositCreatePayload interfaces to lib/types/qbo.ts
- Add createPayment(payload) and createDeposit(payload) public methods to QboClient
- Both methods use existing private this.request<T>() with POST + minorversion=65
- Both methods throw descriptively if QBO returns no Id in response
NOW() returns TIMESTAMPTZ. The pattern
(NOW() AT TIME ZONE 'UTC' AT TIME ZONE $userTz)::date
double-converts: first strips the tz designation (keeping UTC wall-clock as
naive TIMESTAMP), then re-interprets that wall-clock as user-local
(pushing UTC into the user-tz's UTC equivalent). For non-UTC users this
gives the WRONG date — e.g. NY user at 9pm sees "today = tomorrow's UTC
date", so opened-today returns 0.
The column-side pattern ((col AT TIME ZONE 'UTC') AT TIME ZONE $userTz)
is correct because the columns are TIMESTAMP without TZ (stored as UTC) —
only the NOW() side was buggy. Replace with (NOW() AT TIME ZONE $userTz)
everywhere.
Affects: dashboard overview/trends, mobile dashboard/engagement/finance.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The dep-only fix wasn't enough: better-auth's useSession (nanostores) can
re-emit on focus / store refresh, transitioning session.user.id through
undefined and back. Each transition re-fires the effect, which then calls
setTheme(session.user.theme) with the stale cached value and reverts the
user's selection.
Track "have we synced this tab session" with a ref. After the first
successful sync, no subsequent effect fire can revert.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Effect deps included `theme`, causing the bridge to re-fire on every
client-side theme change and call setTheme(session.user.theme). Because
the PUT to /api/me/theme does not refresh the better-auth session,
session.user.theme stays at the pre-change value and clobbers the new
selection — toast says "Theme updated" but UI stays on the prior theme.
Sync only on session identity change (sign-in / sign-out). Matches the
planning intent ("on session load and after sign-in").
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>