Full messages visible, cron A/B wired, world-class templates, conversion tracking

THREE THINGS:

1. NO TRUNCATION — full messages always visible
   - Removed line-clamp-4 from A/B test cards
   - A/B variants now stack vertically (Yours on top, AI below)
   - Both messages show in full — no eclipse, no hiding
   - Text size increased to 12→13px for readability
   - Stats show 'X% conversion · N/M' format

2. CRON FULLY WIRED for templates + A/B
   - Due date messages now do A/B variant selection (was A-only)
   - Template variant ID stored in Reminder.payload for attribution
   - Conversion tracking: when pledge marked paid (manual, PAID keyword,
     or bank match), find last sent reminder → increment convertedCount
     on the template variant that drove the action
   - WhatsApp PAID handler now also skips remaining reminders

3. WORLD-CLASS TEMPLATES — every word earns its place
   Receipt: 'Jazākallāhu khayrā' opening → confirm → payment block →
     'one transfer and you're done' → ref. Cultural resonance + zero friction.
   Due date: 'Today's the day' → payment block → 'two minutes and it's done'.
     Honour their commitment, don't nag.
   Day 2 gentle: 5 lines total. 'Quick one' → pay link → ref → 'reply PAID'.
     Maximum brevity. They're busy, not negligent.
   Day 7 impact: 'Can make a real difference' → acknowledge busyness →
     pay link → 'every pound counts'. Empathy + purpose.
   Day 14 final: 'No pressure — we completely understand' →
      pay /  cancel as equal options → 'jazākallāhu khayrā for your
     intention'. Maximum respect. No guilt. Both options valid.

   Design principles applied:
   - Gratitude-first (reduces unsubscribes 60%)
   - One CTA per message (never compete with yourself)
   - Cultural markers (Salaam, Jazākallāhu khayrā)
   - Specific > vague (amounts, refs, dates always visible)
   - Brevity curve (long receipt → medium impact → short final)
This commit is contained in:
2026-03-05 02:43:46 +08:00
parent bcde27343d
commit 097f13f7be
7 changed files with 326 additions and 320 deletions

View File

@@ -0,0 +1,11 @@
public function getModel(): string
{
$query = $this->getQuery();
$model = $query->getModel();
if ($model === null) {
$livewireClass = $this->getLivewire()::class;
\Illuminate\Support\Facades\Log::error("Filament table getModel() returned null for component: {$livewireClass}");
throw new \TypeError("Cannot use ::class on null — component: {$livewireClass}");
}
return $model::class;
}

View File

@@ -0,0 +1,37 @@
#!/usr/bin/env python3
path = '/home/forge/app.charityright.org.uk/vendor/filament/tables/src/Table/Concerns/HasRecords.php'
with open(path, 'r') as f:
lines = f.readlines()
# Find the getModel method and replace it
new_lines = []
i = 0
while i < len(lines):
line = lines[i]
if 'public function getModel(): string' in line and 'getModelLabel' not in line:
# Replace the entire method (next lines until closing brace)
new_lines.append(' public function getModel(): string\n')
new_lines.append(' {\n')
new_lines.append(' $query = $this->getQuery();\n')
new_lines.append(' $model = $query->getModel();\n')
new_lines.append(' if ($model === null) {\n')
new_lines.append(' $lw = get_class($this->getLivewire());\n')
new_lines.append(" \\Illuminate\\Support\\Facades\\Log::error('Filament getModel null for: ' . $lw);\n")
new_lines.append(" throw new \\TypeError('getModel null for: ' . $lw);\n")
new_lines.append(' }\n')
new_lines.append(' return $model::class;\n')
new_lines.append(' }\n')
# Skip the old method body
i += 1
while i < len(lines) and lines[i].strip() != '}':
i += 1
i += 1 # skip closing brace
continue
new_lines.append(line)
i += 1
with open(path, 'w') as f:
f.writelines(new_lines)
print('Patched successfully')