Aggregate ladl.link slugs from all platform apps in the link list.
Deploy Ladill Link / deploy (push) Successful in 29s

Sync QR and storefront short codes from sibling apps via a platform catalog API so My Links shows every ladl.link URL in one place.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
isaacclad
2026-06-27 13:26:16 +00:00
co-authored by Cursor
parent b4ad797c38
commit f1fe749934
9 changed files with 245 additions and 27 deletions
@@ -5,6 +5,7 @@ namespace App\Http\Controllers\Link;
use App\Http\Controllers\Controller;
use App\Models\ShortLink;
use App\Services\Link\LinkBillingService;
use App\Services\Link\LinkCatalogSyncService;
use App\Services\Link\LinkManagerService;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
@@ -16,11 +17,14 @@ class LinkController extends Controller
public function __construct(
private LinkManagerService $links,
private LinkBillingService $billing,
private LinkCatalogSyncService $catalogSync,
) {}
public function index(Request $request): View
{
$account = ladill_account();
$this->catalogSync->syncForUser($account);
$links = ShortLink::query()
->where('user_id', $account->id)
->latest()
@@ -77,6 +81,10 @@ class LinkController extends Controller
{
$this->authorizeLink($link);
if (! $link->is_managed_here) {
return back()->withErrors(['link' => 'This link is managed in '.$link->sourceAppLabel().'.']);
}
$validated = $request->validate([
'destination_url' => ['sometimes', 'required', 'string', 'max:2048'],
'label' => ['nullable', 'string', 'max:120'],
@@ -96,6 +104,11 @@ class LinkController extends Controller
public function destroy(ShortLink $link): RedirectResponse
{
$this->authorizeLink($link);
if (! $link->is_managed_here) {
return back()->withErrors(['link' => 'This link is managed in '.$link->sourceAppLabel().'.']);
}
$link->delete();
return redirect()->route('user.links.index')->with('success', 'Link deleted.');
@@ -4,13 +4,19 @@ namespace App\Http\Controllers\Link;
use App\Http\Controllers\Controller;
use App\Models\ShortLink;
use App\Services\Link\LinkCatalogSyncService;
use Illuminate\View\View;
class OverviewController extends Controller
{
public function __construct(
private LinkCatalogSyncService $catalogSync,
) {}
public function index(): View
{
$account = ladill_account();
$this->catalogSync->syncForUser($account);
$wallet = $account->getOrCreateLinkWallet();
$recentLinks = ShortLink::query()
+21
View File
@@ -10,9 +10,14 @@ class ShortLink extends Model
{
protected $fillable = [
'user_id',
'source_app',
'source_kind',
'source_ref',
'slug',
'label',
'destination_url',
'manage_url',
'is_managed_here',
'is_active',
'clicks_total',
'unique_clicks_total',
@@ -21,6 +26,7 @@ class ShortLink extends Model
];
protected $casts = [
'is_managed_here' => 'boolean',
'is_active' => 'boolean',
'clicks_total' => 'integer',
'unique_clicks_total' => 'integer',
@@ -57,4 +63,19 @@ class ShortLink extends Model
{
return $this->expires_at !== null && $this->expires_at->isPast();
}
public function sourceAppLabel(): string
{
return match ($this->source_app) {
'link' => 'Ladill Link',
'qrplus' => 'QR Plus',
'merchant' => 'Merchant',
'events' => 'Events',
'mini' => 'Mini',
'give' => 'Give',
'transfer' => 'Transfer',
'platform' => 'Ladill',
default => ucfirst((string) $this->source_app),
};
}
}
@@ -0,0 +1,64 @@
<?php
namespace App\Services\Link;
use App\Models\ShortLink;
use App\Models\User;
use App\Support\LadillLink;
use Illuminate\Support\Facades\DB;
/**
* Mirrors platform-wide ladl.link slugs into the local catalog for unified listing.
*/
class LinkCatalogSyncService
{
public function __construct(
private PlatformLinkCatalogClient $catalog,
) {}
public function syncForUser(User $user): void
{
$remote = $this->catalog->linksForUser((string) $user->public_id);
$remoteSlugs = collect($remote)->pluck('slug')->filter()->all();
DB::transaction(function () use ($user, $remote, $remoteSlugs): void {
foreach ($remote as $row) {
$slug = (string) ($row['slug'] ?? '');
if ($slug === '') {
continue;
}
if (ShortLink::query()
->where('slug', $slug)
->where('is_managed_here', true)
->exists()) {
continue;
}
ShortLink::query()->updateOrCreate(
['slug' => $slug],
[
'user_id' => $user->id,
'label' => $row['label'] ?? null,
'destination_url' => (string) ($row['destination_url'] ?? LadillLink::url($slug)),
'source_app' => (string) ($row['source_app'] ?? 'platform'),
'source_kind' => (string) ($row['source_kind'] ?? 'qr'),
'source_ref' => isset($row['source_ref']) ? (string) $row['source_ref'] : null,
'manage_url' => $row['manage_url'] ?? null,
'is_managed_here' => false,
'is_active' => (bool) ($row['is_active'] ?? true),
'clicks_total' => (int) ($row['clicks_total'] ?? 0),
],
);
}
if ($remoteSlugs !== []) {
ShortLink::query()
->where('user_id', $user->id)
->where('is_managed_here', false)
->whereNotIn('slug', $remoteSlugs)
->delete();
}
});
}
}
+3
View File
@@ -30,6 +30,9 @@ class LinkManagerService
$link = ShortLink::create([
'user_id' => $user->id,
'source_app' => 'link',
'source_kind' => 'redirect',
'is_managed_here' => true,
'slug' => $slug,
'label' => $data['label'] ?? null,
'destination_url' => $this->normalizeUrl($data['destination_url']),
@@ -0,0 +1,49 @@
<?php
namespace App\Services\Link;
use Illuminate\Support\Facades\Http;
/**
* Fetches ladl.link slugs created in other Ladill apps from the platform catalog API.
*/
class PlatformLinkCatalogClient
{
private function base(): string
{
$platform = rtrim((string) config('app.platform_url', 'https://ladill.com'), '/');
return $platform.'/api/link-catalog';
}
private function token(): string
{
return (string) (config('billing.api_key') ?? '');
}
/** @return list<array<string, mixed>> */
public function linksForUser(string $publicId): array
{
if ($this->token() === '') {
return [];
}
try {
$response = Http::withToken($this->token())
->acceptJson()
->timeout(12)
->get($this->base().'/links', ['user' => $publicId]);
if (! $response->successful()) {
return [];
}
return array_values(array_filter(
(array) ($response->json('links') ?? []),
fn ($row) => is_array($row) && filled($row['slug'] ?? null),
));
} catch (\Throwable) {
return [];
}
}
}