Scope Afia assistant to Ladill Transfer.
Deploy Ladill Transfer / deploy (push) Successful in 45s

Replace Mini/QR Plus copy and system prompt with Transfer file-sharing context, suggestions, and dashboard metrics.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
isaacclad
2026-06-08 10:11:27 +00:00
co-authored by Cursor
parent de999851d0
commit b09a72cefc
6 changed files with 82 additions and 57 deletions
@@ -26,7 +26,7 @@ class AccountController extends Controller
}
$balanceMinor = $this->billing->balanceMinor($publicId);
$ledger = $this->billing->serviceLedger($publicId, config('billing.service', 'mini'));
$ledger = $this->billing->serviceLedger($publicId, config('billing.service', 'transfer'));
return [$balanceMinor, $ledger];
}
+37 -21
View File
@@ -3,10 +3,10 @@
namespace App\Http\Controllers\Qr;
use App\Http\Controllers\Controller;
use App\Models\QrCode;
use App\Models\Transfer;
use App\Models\TransferDownloadEvent;
use App\Services\Afia\AfiaService;
use App\Services\Billing\BillingClient;
use App\Support\Qr\QrTypeCatalog;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
@@ -45,19 +45,32 @@ class AfiaController extends Controller
return ['signed_in' => 'no'];
}
$types = QrTypeCatalog::eventTypes();
$codes = $account->qrCodes()->whereIn('type', $types);
$active = Transfer::query()
->where('user_id', $account->id)
->where('status', Transfer::STATUS_ACTIVE);
$storageBytes = (int) (clone $active)->sum('storage_bytes');
$storageGb = round($storageBytes / (1024 * 1024 * 1024), 2);
$transferIds = Transfer::query()
->where('user_id', $account->id)
->pluck('id');
$ctx = [
'signed_in' => 'yes',
'qr_codes_total' => (clone $codes)->count(),
'qr_codes_active' => (clone $codes)->where('is_active', true)->count(),
'scans_total' => (int) (clone $codes)->sum('scans_total'),
'top_code_type' => (clone $codes)
->selectRaw('type, count(*) as total')
->groupBy('type')
->orderByDesc('total')
->value('type') ?: 'none yet',
'active_transfers' => (clone $active)->count(),
'storage_gb' => $storageGb,
'downloads_30d' => TransferDownloadEvent::query()
->whereIn('transfer_id', $transferIds)
->where('downloaded_at', '>=', now()->subDays(30))
->count(),
'expiring_within_7_days' => Transfer::query()
->where('user_id', $account->id)
->where('status', Transfer::STATUS_ACTIVE)
->whereNotNull('expires_at')
->whereBetween('expires_at', [now(), now()->addDays(7)])
->count(),
'storage_price_per_gb_month_ghs' => number_format((float) config('transfer.price_per_gb_month', 1.0), 2),
];
if ($account->public_id) {
@@ -67,18 +80,21 @@ class AfiaController extends Controller
}
}
$recent = $account->qrCodes()
->whereIn('type', $types)
->latest('updated_at')
$recent = Transfer::query()
->where('user_id', $account->id)
->where('status', Transfer::STATUS_ACTIVE)
->with('qrCode:id,short_code')
->latest()
->limit(3)
->get(['type', 'label', 'short_code', 'scans_total']);
->get(['id', 'title', 'qr_code_id', 'download_count', 'expires_at']);
if ($recent->isNotEmpty()) {
$ctx['recent_codes'] = $recent->map(fn (QrCode $code): string => sprintf(
'%s (%s, %d scans)',
$code->label ?: $code->short_code,
QrTypeCatalog::label($code->type),
$code->scans_total,
$ctx['recent_transfers'] = $recent->map(fn (Transfer $transfer): string => sprintf(
'%s (%s, %d downloads%s)',
$transfer->title,
$transfer->qrCode?->short_code ?? 'no QR',
$transfer->download_count,
$transfer->expires_at ? ', expires '.$transfer->expires_at->toDateString() : '',
))->implode('; ');
}
+26 -22
View File
@@ -6,7 +6,7 @@ use Illuminate\Support\Facades\Http;
use RuntimeException;
/**
* Afia Ladill in-app AI assistant scoped to a product (QR Plus).
* Afia Ladill in-app AI assistant scoped to a product.
*/
class AfiaService
{
@@ -88,36 +88,40 @@ class AfiaService
{
$ctx = collect($context)->map(fn ($v, $k) => "- {$k}: {$v}")->implode("\n");
return match ((string) config('afia.product', 'qr')) {
'qr' => $this->qrSystemPrompt($ctx),
default => $this->qrSystemPrompt($ctx),
return match ((string) config('afia.product', 'transfer')) {
'transfer' => $this->transferSystemPrompt($ctx),
default => $this->transferSystemPrompt($ctx),
};
}
private function qrSystemPrompt(string $ctx): string
private function transferSystemPrompt(string $ctx): string
{
$pricePerGb = number_format((float) config('transfer.price_per_gb_month', 1.0), 2);
return <<<PROMPT
You are Afia, the assistant inside Ladill QR Plus (qrplus.ladill.com).
Help users create and manage dynamic QR codes, understand scans and billing, and print codes that work reliably.
You are Afia, the assistant inside Ladill Transfer (transfer.ladill.com).
Help users share files securely with QR codes and links, manage retention, track downloads, and understand storage billing.
Be concise, friendly, and actionable: short numbered steps and name the screen or section to use.
What Ladill QR Plus does and where things live:
- Overview (Dashboard): recent codes, active code count, scans in the last 30 days, wallet balance.
- My Codes: list, create, and edit QR codes. Types: Link (URL), PDF, List of Links, Business profile, WiFi, App download.
- Creating a code: pick a type, set content, customize style (colors, logo, frame), then download PNG/SVG/PDF.
- Dynamic codes: destination can change after printing the printed QR always points to ladill.com/q/{code}.
- Business QR: name, tagline, contact, hours, logo, cover, social links shown as a mobile landing page.
- WiFi QR: guests scan to join; network name and password are encoded in the code.
- PDF QR: hosts a PDF with optional download button on the viewer.
- Billing: each new code debits the Ladill wallet (account portal Wallet). Top up at account.ladill.com/wallet.
- Team: invite teammates to manage codes together (sidebar Team).
- Developers: API tokens for programmatic code creation (sidebar Developers).
What Ladill Transfer does and where things live:
- Overview (Dashboard): active transfers, storage used, downloads in the last 30 days, transfers expiring soon, wallet balance.
- Transfers: list all file shares; create a new transfer (upload one or more files, title, optional message, retention period, optional password).
- After creating a transfer: users get a share link and downloadable QR code; recipients scan ladill.com/q/{code} or open the link.
- Files: browse all stored files across transfers with sizes and download counts.
- Analytics: download trends, top transfers, recent download activity.
- Wallet & Billing: prepaid storage is billed from the Ladill wallet (sidebar Wallet). Top up at account.ladill.com/wallet.
- Team: invite teammates to manage transfers together (sidebar Team).
- Settings: notification preferences for transfer activity.
Pricing:
- Storage retention is prepaid at GHS {$pricePerGb} per GB per month (metered on actual storage; no free tier).
- Wallet must have funds to keep transfers active through their retention period.
Rules:
- Only answer questions about Ladill QR Plus code types, styling, downloads, scans, wallet billing, team, and API.
- If asked about domains, hosting, email, or payments/commerce QR (shop, events, donations), briefly say those live in other Ladill apps and suggest the app launcher.
- Never invent prices, short codes, or wallet balances use the user context below or tell them where to check.
- For print tips: recommend high contrast, adequate quiet zone, test-scan before mass printing.
- Only answer questions about Ladill Transfer uploads, share links, QR codes, passwords, retention/expiry, downloads, storage billing, team, and wallet.
- If asked about payment QRs (Mini), shops (Merchant), events, domains, hosting, or utility QR types, briefly say those live in other Ladill apps and suggest the app launcher.
- Never invent file sizes, download counts, short codes, or wallet balances use the user context below or tell them where to check in the app.
- Public scan URLs always use ladill.com/q/{code} so printed QRs never need reprinting.
Current user context:
{$ctx}
+6 -6
View File
@@ -1,10 +1,10 @@
@php
$afiaGreeting = "Hi, I'm Afia 👋 Ask me about payment QRs — creating one, accepting payments, payouts, or the 5% platform fee";
$afiaGreeting = "Hi, I'm Afia 👋 Ask me about file transfers — uploading, share links, QR codes, retention, or storage billing";
$afiaSuggestions = [
'How do I create a payment QR?',
'How do customers pay me?',
'When do payouts reach my wallet?',
'What is the platform fee?',
'How do I create a new transfer?',
'How do recipients download my files?',
'How does storage billing work?',
'Can I password-protect a transfer?',
];
@endphp
{{-- Afia Ladill AI assistant slide-over. Opened via $dispatch('afia-open'). --}}
@@ -48,7 +48,7 @@
</span>
<div class="leading-tight">
<p class="text-sm font-semibold text-slate-900">Afia</p>
<p class="text-[11px] text-slate-400">Mini assistant</p>
<p class="text-[11px] text-slate-400">Transfer assistant</p>
</div>
</div>
<div class="flex items-center gap-1">
+2 -2
View File
@@ -3,7 +3,7 @@
@php $fmt = fn ($m) => 'GHS '.number_format($m / 100, 2); @endphp
<div class="mx-auto max-w-3xl">
<h1 class="text-xl font-semibold tracking-tight text-slate-900">Wallet</h1>
<p class="mt-0.5 text-sm text-slate-500">Your Ladill wallet funds QR Plus and other Ladill apps.</p>
<p class="mt-0.5 text-sm text-slate-500">Your Ladill wallet funds Transfer storage and other Ladill apps.</p>
<div class="mt-6 rounded-2xl bg-gradient-to-br from-indigo-700 via-indigo-600 to-blue-500 p-6 text-white shadow-sm">
<p class="text-xs font-medium uppercase tracking-wide text-indigo-100">Balance</p>
<p class="mt-1 text-3xl font-semibold">{{ $fmt($balanceMinor) }}</p>
@@ -11,7 +11,7 @@
</div>
<div class="mt-4 grid gap-4 sm:grid-cols-2">
<div class="rounded-2xl border border-slate-200 bg-white p-5">
<p class="text-xs font-medium uppercase tracking-wide text-slate-400">Spent on QR Plus</p>
<p class="text-xs font-medium uppercase tracking-wide text-slate-400">Spent on Transfer</p>
<p class="mt-1.5 text-2xl font-semibold text-slate-900">{{ $fmt($spentMinor) }}</p>
</div>
<div class="rounded-2xl border border-slate-200 bg-white p-5">
+10 -5
View File
@@ -37,18 +37,23 @@ class AfiaTest extends TestCase
public function test_returns_reply_from_llm(): void
{
config(['afia.api_key' => 'sk-test', 'afia.provider' => 'openai', 'afia.model' => 'gpt-4o-mini']);
config([
'afia.api_key' => 'sk-test',
'afia.provider' => 'openai',
'afia.model' => 'gpt-4o-mini',
'afia.product' => 'transfer',
]);
Http::fake([
'api.openai.com/*' => Http::response(['choices' => [['message' => ['content' => 'Create a Business QR under My Codes.']]]]),
'api.openai.com/*' => Http::response(['choices' => [['message' => ['content' => 'Open Transfers → New transfer to upload files.']]]]),
]);
$this->actingAs($this->user())
->postJson('/afia/chat', ['message' => 'How do I create a business QR?'])
->postJson('/afia/chat', ['message' => 'How do I share files?'])
->assertOk()
->assertJson(['reply' => 'Create a Business QR under My Codes.']);
->assertJson(['reply' => 'Open Transfers → New transfer to upload files.']);
Http::assertSent(fn ($r) => str_contains($r->url(), 'openai.com')
&& collect($r['messages'])->first()['role'] === 'system'
&& str_contains(collect($r['messages'])->first()['content'], 'Ladill QR Plus'));
&& str_contains(collect($r['messages'])->first()['content'], 'Ladill Transfer'));
}
}