feat: wire GHL webhook to Resend email templates

- Replace raw Resend text emails with sendMail using Resend templates
- automated-user-registration template: firstName, tempPassword,
  verifyUrl, loginUrl
- license-key-delivery template: firstName, license_key
- Remove requestPasswordReset call (temp password is the one-time
  credential; verification link handles email confirmation)
- Add .catch() to Better Auth email callbacks to prevent unhandled
  rejections from missing templates crashing the server
This commit is contained in:
NeilClack
2026-07-08 15:01:08 -05:00
parent e6ef7d3b36
commit 55a29b9c1a
29 changed files with 26572 additions and 21641 deletions
+22 -18
View File
@@ -58,16 +58,16 @@ State variables declared with `$state('')` are set once on component mount. To k
```svelte
<script lang="ts">
let { record = {} as Record<string, unknown> } = $props();
let { record = {} as Record<string, unknown> } = $props();
let name = $state('');
let freq = $state('');
let name = $state('');
let freq = $state('');
// Sync local state when prop changes
$effect(() => {
name = String(record.name ?? '');
freq = String(record.solfeggioFrequencyId ?? '');
});
// Sync local state when prop changes
$effect(() => {
name = String(record.name ?? '');
freq = String(record.solfeggioFrequencyId ?? '');
});
</script>
```
@@ -164,17 +164,18 @@ For every admin route, write BOTH:
These methods are available on `page` from `vitest/browser`:
| Method | Works? | Example |
|--------|--------|---------|
| `getByRole` | ✅ | `page.getByRole('textbox', { name: 'Name' })` |
| `getByText` | ✅ | `page.getByText('Content')` |
| `getByPlaceholder` | ✅ | `page.getByPlaceholder('Email Address')` |
| `getByAltText` | ✅ | `page.getByAltText('Sign In with Google')` |
| `getByTestId` | ✅ | `page.getByTestId('tiptap-mock')` |
| `getByLabel` | ❌ | Not available in this version |
| `getByDisplayValue` | ❌ | Not available in this version |
| Method | Works? | Example |
| ------------------- | ------ | --------------------------------------------- |
| `getByRole` | ✅ | `page.getByRole('textbox', { name: 'Name' })` |
| `getByText` | ✅ | `page.getByText('Content')` |
| `getByPlaceholder` | ✅ | `page.getByPlaceholder('Email Address')` |
| `getByAltText` | ✅ | `page.getByAltText('Sign In with Google')` |
| `getByTestId` | ✅ | `page.getByTestId('tiptap-mock')` |
| `getByLabel` | ❌ | Not available in this version |
| `getByDisplayValue` | ❌ | Not available in this version |
For label-identified elements, use `getByRole` with `name` option:
- Text inputs → `getByRole('textbox', { name: /^Name$/ })`
- Selects → `getByRole('combobox', { name: /^Frequency$/ })`
- Buttons → `getByRole('button', { name: 'Save' })`
@@ -186,7 +187,9 @@ For label-identified elements, use `getByRole` with `name` option:
Since `bind:value` on selects doesn't work with `$effect`-driven state, and we use `selected` on `<option>` instead, you can check the select's value directly:
```ts
const el = await page.getByRole('combobox', { name: /^Frequency$/ }).element() as HTMLSelectElement;
const el = (await page
.getByRole('combobox', { name: /^Frequency$/ })
.element()) as HTMLSelectElement;
expect(el.value).toBe('2');
```
@@ -251,6 +254,7 @@ Imports of the mocked module must come AFTER `vi.mock()` calls.
### Per-route form components
Each admin route under `admin/[model]/` has its own form component (e.g. `ProclamationForm.svelte`). These components:
- Receive `record`, `fieldErrors`, `formError`, `relationOptionsData`, and model-specific extras (e.g. `categories`, `isSuper`) as props.
- Use `$state` + `$effect` for field values (see "Svelte 5 Runes Gotchas" above).
- Use `selected` on `<option>` for selects.