Compare commits

...

2 Commits

Author SHA1 Message Date
Dwindi Ramadhana
9937da6a7b style: format with pint 2026-06-14 15:47:37 +07:00
Dwindi Ramadhana
c3ce549264 chore: change Android Apk download status to upcoming 2026-06-14 15:47:28 +07:00
45 changed files with 541 additions and 361 deletions

View File

@@ -21,6 +21,7 @@ class AdminAnalyticsController extends Controller
if ($token === '' || $provided === '' || ! hash_equals($token, $provided)) { if ($token === '' || $provided === '' || ! hash_equals($token, $provided)) {
return response()->json(['ok' => false, 'error' => 'unauthorized'], 401); return response()->json(['ok' => false, 'error' => 'unauthorized'], 401);
} }
return null; return null;
} }

View File

@@ -9,9 +9,7 @@ use Illuminate\Http\Request;
class AdminSettingsController extends Controller class AdminSettingsController extends Controller
{ {
public function __construct(private readonly SettingsService $settings) public function __construct(private readonly SettingsService $settings) {}
{
}
private function authorizeAdmin(Request $request): ?JsonResponse private function authorizeAdmin(Request $request): ?JsonResponse
{ {
@@ -20,6 +18,7 @@ class AdminSettingsController extends Controller
if ($token === '' || $provided === '' || ! hash_equals($token, $provided)) { if ($token === '' || $provided === '' || ! hash_equals($token, $provided)) {
return response()->json(['ok' => false, 'error' => 'unauthorized'], 401); return response()->json(['ok' => false, 'error' => 'unauthorized'], 401);
} }
return null; return null;
} }

View File

@@ -15,8 +15,7 @@ class AdminSubscriptionController extends Controller
{ {
public function __construct( public function __construct(
private readonly KeywordQuotaService $keywordQuota private readonly KeywordQuotaService $keywordQuota
) { ) {}
}
private function authorizeAdmin(Request $request): ?JsonResponse private function authorizeAdmin(Request $request): ?JsonResponse
{ {
@@ -25,6 +24,7 @@ class AdminSubscriptionController extends Controller
if ($token === '' || $provided === '' || ! hash_equals($token, $provided)) { if ($token === '' || $provided === '' || ! hash_equals($token, $provided)) {
return response()->json(['ok' => false, 'error' => 'unauthorized'], 401); return response()->json(['ok' => false, 'error' => 'unauthorized'], 401);
} }
return null; return null;
} }
@@ -104,6 +104,7 @@ class AdminSubscriptionController extends Controller
} }
$sub->update(['status' => 'revoked', 'expires_at' => $now]); $sub->update(['status' => 'revoked', 'expires_at' => $now]);
$this->syncUserTier($sub->user_id); $this->syncUserTier($sub->user_id);
return response()->json(['ok' => true, 'revoked' => true]); return response()->json(['ok' => true, 'revoked' => true]);
} }

View File

@@ -12,8 +12,7 @@ class AdminUserController extends Controller
{ {
public function __construct( public function __construct(
private readonly KeywordQuotaService $keywordQuota private readonly KeywordQuotaService $keywordQuota
) { ) {}
}
private function authorizeAdmin(Request $request): ?JsonResponse private function authorizeAdmin(Request $request): ?JsonResponse
{ {

View File

@@ -10,9 +10,7 @@ use Illuminate\Http\Request;
class AdminWebhookController extends Controller class AdminWebhookController extends Controller
{ {
public function __construct(private readonly PaypalWebhookProcessor $processor) public function __construct(private readonly PaypalWebhookProcessor $processor) {}
{
}
private function authorizeAdmin(Request $request): ?JsonResponse private function authorizeAdmin(Request $request): ?JsonResponse
{ {
@@ -21,6 +19,7 @@ class AdminWebhookController extends Controller
if ($token === '' || $provided === '' || ! hash_equals($token, $provided)) { if ($token === '' || $provided === '' || ! hash_equals($token, $provided)) {
return response()->json(['ok' => false, 'error' => 'unauthorized'], 401); return response()->json(['ok' => false, 'error' => 'unauthorized'], 401);
} }
return null; return null;
} }

View File

@@ -2,9 +2,9 @@
namespace App\Http\Controllers\Api\V1; namespace App\Http\Controllers\Api\V1;
use App\Http\Controllers\Controller;
use App\Services\Auth\ApiKeyService; use App\Services\Auth\ApiKeyService;
use App\Services\System\SettingsService; use App\Services\System\SettingsService;
use App\Http\Controllers\Controller;
use Illuminate\Http\JsonResponse; use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
@@ -13,6 +13,7 @@ use RuntimeException;
class EmojiApiController extends Controller class EmojiApiController extends Controller
{ {
private const TIER_FREE = 'free'; private const TIER_FREE = 'free';
private const TIER_PRO = 'pro'; private const TIER_PRO = 'pro';
/** @var array<string,mixed>|null */ /** @var array<string,mixed>|null */
@@ -20,8 +21,7 @@ class EmojiApiController extends Controller
public function __construct( public function __construct(
private readonly ApiKeyService $apiKeys private readonly ApiKeyService $apiKeys
) { ) {}
}
/** @var array<string,string> */ /** @var array<string,string> */
private const CATEGORY_MAP = [ private const CATEGORY_MAP = [
@@ -413,6 +413,7 @@ class EmojiApiController extends Controller
private function isNotModified(Request $request, string $etag): bool private function isNotModified(Request $request, string $etag): bool
{ {
$ifNoneMatch = trim((string) $request->header('If-None-Match', '')); $ifNoneMatch = trim((string) $request->header('If-None-Match', ''));
return $ifNoneMatch !== '' && $ifNoneMatch === $etag; return $ifNoneMatch !== '' && $ifNoneMatch === $etag;
} }
@@ -446,6 +447,7 @@ class EmojiApiController extends Controller
} }
self::$dataset = $decoded; self::$dataset = $decoded;
return self::$dataset; return self::$dataset;
} }
@@ -464,6 +466,7 @@ class EmojiApiController extends Controller
$value = strtolower(trim($text)); $value = strtolower(trim($text));
$value = str_replace('&', 'and', $value); $value = str_replace('&', 'and', $value);
$value = preg_replace('/[^a-z0-9]+/', '-', $value) ?? ''; $value = preg_replace('/[^a-z0-9]+/', '-', $value) ?? '';
return trim($value, '-'); return trim($value, '-');
} }
@@ -510,6 +513,7 @@ class EmojiApiController extends Controller
return false; return false;
} }
} }
return true; return true;
})); }));
} }
@@ -583,7 +587,7 @@ class EmojiApiController extends Controller
return $text; return $text;
} }
return rtrim(mb_substr($text, 0, $max - 1), " ,.;:-").'…'; return rtrim(mb_substr($text, 0, $max - 1), ' ,.;:-').'…';
} }
/** /**

View File

@@ -9,9 +9,7 @@ use Illuminate\Http\Request;
class ExtensionController extends Controller class ExtensionController extends Controller
{ {
public function __construct(private readonly ExtensionVerificationService $verifier) public function __construct(private readonly ExtensionVerificationService $verifier) {}
{
}
public function verify(Request $request): JsonResponse public function verify(Request $request): JsonResponse
{ {

View File

@@ -10,9 +10,7 @@ use Illuminate\Http\Request;
class PaypalWebhookController extends Controller class PaypalWebhookController extends Controller
{ {
public function __construct(private readonly PaypalWebhookProcessor $processor) public function __construct(private readonly PaypalWebhookProcessor $processor) {}
{
}
public function handle(Request $request): JsonResponse public function handle(Request $request): JsonResponse
{ {

View File

@@ -83,6 +83,7 @@ class SystemController extends Controller
$key = 'dw_metrics_ping'; $key = 'dw_metrics_ping';
Cache::put($key, 'ok', 60); Cache::put($key, 'ok', 60);
$val = Cache::get($key); $val = Cache::get($key);
return $val === 'ok' ? 'ok' : 'degraded'; return $val === 'ok' ? 'ok' : 'degraded';
} catch (\Throwable) { } catch (\Throwable) {
return 'down'; return 'down';

View File

@@ -15,8 +15,7 @@ class UserController extends Controller
{ {
public function __construct( public function __construct(
private readonly ApiKeyService $keys private readonly ApiKeyService $keys
) { ) {}
}
public function register(Request $request): JsonResponse public function register(Request $request): JsonResponse
{ {

View File

@@ -14,8 +14,7 @@ class UserKeywordController extends Controller
public function __construct( public function __construct(
private readonly ApiKeyService $keys, private readonly ApiKeyService $keys,
private readonly KeywordQuotaService $keywordQuota private readonly KeywordQuotaService $keywordQuota
) { ) {}
}
private function ensureUser(Request $request): ?array private function ensureUser(Request $request): ?array
{ {
@@ -186,6 +185,7 @@ class UserKeywordController extends Controller
if (! $existing && $targetActive && $limit !== null && $activeCount >= $limit) { if (! $existing && $targetActive && $limit !== null && $activeCount >= $limit) {
$skipped += 1; $skipped += 1;
continue; continue;
} }

View File

@@ -4,8 +4,8 @@ namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Models\User; use App\Models\User;
use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Auth\Events\Registered; use Illuminate\Auth\Events\Registered;
use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Http\RedirectResponse; use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Auth;

View File

@@ -21,6 +21,7 @@ class BillingPaymentController extends Controller
} }
$provider = strtolower((string) $payment->provider); $provider = strtolower((string) $payment->provider);
return match ($provider) { return match ($provider) {
'paypal' => $this->resumePayPal($payment), 'paypal' => $this->resumePayPal($payment),
'pakasir' => $this->resumePakasir($payment), 'pakasir' => $this->resumePakasir($payment),

View File

@@ -22,8 +22,7 @@ class PakasirController extends Controller
public function __construct( public function __construct(
private readonly SubscriptionTransitionService $subscriptionTransition, private readonly SubscriptionTransitionService $subscriptionTransition,
private readonly KeywordQuotaService $keywordQuota private readonly KeywordQuotaService $keywordQuota
) { ) {}
}
public function createTransaction(Request $request): JsonResponse public function createTransaction(Request $request): JsonResponse
{ {
@@ -94,6 +93,7 @@ class PakasirController extends Controller
'endpoint' => $endpoint, 'endpoint' => $endpoint,
'body' => $res->body(), 'body' => $res->body(),
]); ]);
return response()->json(['error' => 'pakasir_create_failed'], 502); return response()->json(['error' => 'pakasir_create_failed'], 502);
} }
@@ -103,6 +103,7 @@ class PakasirController extends Controller
'endpoint' => $endpoint, 'endpoint' => $endpoint,
'body' => $res->body(), 'body' => $res->body(),
]); ]);
return response()->json(['error' => 'pakasir_invalid_response'], 502); return response()->json(['error' => 'pakasir_invalid_response'], 502);
} }
@@ -112,6 +113,7 @@ class PakasirController extends Controller
'endpoint' => $endpoint, 'endpoint' => $endpoint,
'body' => $body, 'body' => $body,
]); ]);
return response()->json(['error' => 'pakasir_invalid_response'], 502); return response()->json(['error' => 'pakasir_invalid_response'], 502);
} }
@@ -324,6 +326,7 @@ class PakasirController extends Controller
'status' => $status, 'status' => $status,
'payload_status' => $payload['status'] ?? null, 'payload_status' => $payload['status'] ?? null,
]); ]);
return; return;
} }
@@ -338,6 +341,7 @@ class PakasirController extends Controller
)); ));
if ($orderId === '') { if ($orderId === '') {
Log::warning('Pakasir webhook paid event missing order reference', ['payload' => $payload]); Log::warning('Pakasir webhook paid event missing order reference', ['payload' => $payload]);
return; return;
} }
@@ -349,6 +353,7 @@ class PakasirController extends Controller
} }
if (! $order) { if (! $order) {
Log::warning('Pakasir webhook order not found', ['order_id' => $orderId, 'payload' => $payload]); Log::warning('Pakasir webhook order not found', ['order_id' => $orderId, 'payload' => $payload]);
return; return;
} }

View File

@@ -17,7 +17,6 @@ use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Str; use Illuminate\Support\Str;
class PayPalController extends Controller class PayPalController extends Controller
@@ -25,8 +24,7 @@ class PayPalController extends Controller
public function __construct( public function __construct(
private readonly SubscriptionTransitionService $subscriptionTransition, private readonly SubscriptionTransitionService $subscriptionTransition,
private readonly KeywordQuotaService $keywordQuota private readonly KeywordQuotaService $keywordQuota
) { ) {}
}
public function createSubscription(Request $request): RedirectResponse|JsonResponse public function createSubscription(Request $request): RedirectResponse|JsonResponse
{ {
@@ -106,6 +104,7 @@ class PayPalController extends Controller
'body' => $res->body(), 'body' => $res->body(),
]); ]);
} }
return response()->json(['error' => 'paypal_invalid_response'], 502); return response()->json(['error' => 'paypal_invalid_response'], 502);
} }
@@ -163,6 +162,7 @@ class PayPalController extends Controller
} }
$status = (string) $request->query('status', 'success'); $status = (string) $request->query('status', 'success');
return redirect()->route('dashboard.billing', ['status' => $status]); return redirect()->route('dashboard.billing', ['status' => $status]);
} }
@@ -267,6 +267,7 @@ class PayPalController extends Controller
$sub->canceled_at = now(); $sub->canceled_at = now();
$sub->save(); $sub->save();
} }
return true; return true;
} }
@@ -340,6 +341,7 @@ class PayPalController extends Controller
if ($rate <= 0) { if ($rate <= 0) {
return 0; return 0;
} }
return (int) round($plan->amount / $rate); return (int) round($plan->amount / $rate);
} }
@@ -394,6 +396,7 @@ class PayPalController extends Controller
private function billingMode(): string private function billingMode(): string
{ {
$settings = app(SettingsService::class); $settings = app(SettingsService::class);
return (string) ($settings->get('billing_mode', config('dewemoji.billing.mode', 'sandbox')) ?: 'sandbox'); return (string) ($settings->get('billing_mode', config('dewemoji.billing.mode', 'sandbox')) ?: 'sandbox');
} }

View File

@@ -3,16 +3,16 @@
namespace App\Http\Controllers\Dashboard; namespace App\Http\Controllers\Dashboard;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Models\PricingChange; use App\Models\AdminAuditLog;
use App\Models\PricingPlan;
use App\Models\Order; use App\Models\Order;
use App\Models\Payment; use App\Models\Payment;
use App\Models\PricingChange;
use App\Models\PricingPlan;
use App\Models\Subscription; use App\Models\Subscription;
use App\Models\User; use App\Models\User;
use App\Models\UserApiKey; use App\Models\UserApiKey;
use App\Models\UserKeyword; use App\Models\UserKeyword;
use App\Models\WebhookEvent; use App\Models\WebhookEvent;
use App\Models\AdminAuditLog;
use App\Services\Billing\PayPalPlanSyncService; use App\Services\Billing\PayPalPlanSyncService;
use App\Services\Keywords\KeywordQuotaService; use App\Services\Keywords\KeywordQuotaService;
use App\Services\System\SettingsService; use App\Services\System\SettingsService;
@@ -30,9 +30,7 @@ class AdminDashboardController extends Controller
public function __construct( public function __construct(
private readonly SettingsService $settings, private readonly SettingsService $settings,
private readonly KeywordQuotaService $keywordQuota private readonly KeywordQuotaService $keywordQuota
) ) {}
{
}
public function users(Request $request): View public function users(Request $request): View
{ {
@@ -257,6 +255,7 @@ class AdminDashboardController extends Controller
'subscription_id' => $sub->id, 'subscription_id' => $sub->id,
'user_id' => $sub->user_id, 'user_id' => $sub->user_id,
]); ]);
return back()->with('status', 'Subscription revoked.'); return back()->with('status', 'Subscription revoked.');
} }
@@ -565,7 +564,7 @@ class AdminDashboardController extends Controller
public function exportCsv(Request $request, string $type): StreamedResponse public function exportCsv(Request $request, string $type): StreamedResponse
{ {
$type = strtolower($type); $type = strtolower($type);
$filename = "dewemoji-{$type}-export-".now()->format('Ymd_His').".csv"; $filename = "dewemoji-{$type}-export-".now()->format('Ymd_His').'.csv';
return response()->streamDownload(function () use ($type, $request): void { return response()->streamDownload(function () use ($type, $request): void {
$out = fopen('php://output', 'w'); $out = fopen('php://output', 'w');
@@ -692,6 +691,7 @@ class AdminDashboardController extends Controller
private function sanitizeSort(mixed $value, array $allowed, string $fallback): string private function sanitizeSort(mixed $value, array $allowed, string $fallback): string
{ {
$sort = is_string($value) ? $value : ''; $sort = is_string($value) ? $value : '';
return in_array($sort, $allowed, true) ? $sort : $fallback; return in_array($sort, $allowed, true) ? $sort : $fallback;
} }
@@ -706,6 +706,7 @@ class AdminDashboardController extends Controller
private function splitCsv(string $value): array private function splitCsv(string $value): array
{ {
$items = array_filter(array_map('trim', explode(',', $value))); $items = array_filter(array_map('trim', explode(',', $value)));
return array_values($items); return array_values($items);
} }
} }

View File

@@ -10,16 +10,15 @@ use Illuminate\Contracts\Pagination\LengthAwarePaginator;
use Illuminate\Http\RedirectResponse; use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
use Throwable;
use Illuminate\View\View; use Illuminate\View\View;
use Throwable;
class AdminEmojiCatalogController extends Controller class AdminEmojiCatalogController extends Controller
{ {
public function __construct( public function __construct(
private readonly EmojiCatalogService $catalog, private readonly EmojiCatalogService $catalog,
private readonly SettingsService $settings private readonly SettingsService $settings
) { ) {}
}
public function index(Request $request): View public function index(Request $request): View
{ {
@@ -64,6 +63,7 @@ class AdminEmojiCatalogController extends Controller
$emojiId = $this->catalog->saveItem($validated); $emojiId = $this->catalog->saveItem($validated);
} catch (Throwable $e) { } catch (Throwable $e) {
report($e); report($e);
return back()->withInput()->with('error', $e->getMessage() ?: 'Failed to create catalog item.'); return back()->withInput()->with('error', $e->getMessage() ?: 'Failed to create catalog item.');
} }
@@ -85,6 +85,7 @@ class AdminEmojiCatalogController extends Controller
$savedId = $this->catalog->saveItem($validated); $savedId = $this->catalog->saveItem($validated);
} catch (Throwable $e) { } catch (Throwable $e) {
report($e); report($e);
return back() return back()
->withInput() ->withInput()
->with('error', $e->getMessage() ?: 'Failed to save catalog item.'); ->with('error', $e->getMessage() ?: 'Failed to save catalog item.');
@@ -105,6 +106,7 @@ class AdminEmojiCatalogController extends Controller
$this->catalog->deleteItem($emojiId); $this->catalog->deleteItem($emojiId);
} catch (Throwable $e) { } catch (Throwable $e) {
report($e); report($e);
return back()->with('error', $e->getMessage() ?: 'Failed to delete catalog item.'); return back()->with('error', $e->getMessage() ?: 'Failed to delete catalog item.');
} }
@@ -123,6 +125,7 @@ class AdminEmojiCatalogController extends Controller
$result = $this->catalog->importFromDataFile($path); $result = $this->catalog->importFromDataFile($path);
} catch (Throwable $e) { } catch (Throwable $e) {
report($e); report($e);
return back()->with('error', $e->getMessage() ?: 'Failed to import dataset.'); return back()->with('error', $e->getMessage() ?: 'Failed to import dataset.');
} }
@@ -145,6 +148,7 @@ class AdminEmojiCatalogController extends Controller
$result = $this->catalog->publishSnapshot($request->user()?->email); $result = $this->catalog->publishSnapshot($request->user()?->email);
} catch (Throwable $e) { } catch (Throwable $e) {
report($e); report($e);
return back()->with('error', $e->getMessage() ?: 'Failed to publish snapshot.'); return back()->with('error', $e->getMessage() ?: 'Failed to publish snapshot.');
} }
@@ -163,6 +167,7 @@ class AdminEmojiCatalogController extends Controller
$result = $this->catalog->activateSnapshot($validated['snapshot'], $request->user()?->email); $result = $this->catalog->activateSnapshot($validated['snapshot'], $request->user()?->email);
} catch (Throwable $e) { } catch (Throwable $e) {
report($e); report($e);
return back()->with('error', $e->getMessage() ?: 'Failed to activate snapshot.'); return back()->with('error', $e->getMessage() ?: 'Failed to activate snapshot.');
} }

View File

@@ -2,6 +2,7 @@
namespace App\Http\Controllers\Dashboard; namespace App\Http\Controllers\Dashboard;
use App\Http\Controllers\Api\V1\EmojiApiController;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Models\Order; use App\Models\Order;
use App\Models\Payment; use App\Models\Payment;
@@ -10,7 +11,6 @@ use App\Models\User;
use App\Models\UserApiKey; use App\Models\UserApiKey;
use App\Models\UserKeyword; use App\Models\UserKeyword;
use App\Models\WebhookEvent; use App\Models\WebhookEvent;
use App\Http\Controllers\Api\V1\EmojiApiController;
use App\Services\Auth\ApiKeyService; use App\Services\Auth\ApiKeyService;
use App\Services\Keywords\KeywordQuotaService; use App\Services\Keywords\KeywordQuotaService;
use Carbon\Carbon; use Carbon\Carbon;
@@ -27,8 +27,7 @@ class UserDashboardController extends Controller
public function __construct( public function __construct(
private readonly ApiKeyService $keys, private readonly ApiKeyService $keys,
private readonly KeywordQuotaService $keywordQuota private readonly KeywordQuotaService $keywordQuota
) { ) {}
}
public function overview(Request $request): View public function overview(Request $request): View
{ {
@@ -317,6 +316,7 @@ class UserDashboardController extends Controller
if (! $existing && $targetActive && $limit !== null && $activeCount >= $limit) { if (! $existing && $targetActive && $limit !== null && $activeCount >= $limit) {
$skipped += 1; $skipped += 1;
continue; continue;
} }

View File

@@ -3,8 +3,8 @@
namespace App\Http\Controllers; namespace App\Http\Controllers;
use App\Http\Requests\ProfileUpdateRequest; use App\Http\Requests\ProfileUpdateRequest;
use Illuminate\Http\RedirectResponse;
use Illuminate\Contracts\Auth\MustVerifyEmail; use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Redirect; use Illuminate\Support\Facades\Redirect;

View File

@@ -32,7 +32,12 @@ class SiteController extends Controller
private function billingMode(): string private function billingMode(): string
{ {
$settings = app(SettingsService::class); $settings = app(SettingsService::class);
$preferred = (string) ($settings->get('billing_mode', config('dewemoji.billing.mode', 'sandbox')) ?: 'sandbox'); $preferred =
(string) ($settings->get(
'billing_mode',
config('dewemoji.billing.mode', 'sandbox'),
) ?:
'sandbox');
if ($this->paypalConfiguredMode($preferred)) { if ($this->paypalConfiguredMode($preferred)) {
return $preferred; return $preferred;
} }
@@ -50,7 +55,9 @@ class SiteController extends Controller
return view('site.home', [ return view('site.home', [
'initialQuery' => trim((string) $request->query('q', '')), 'initialQuery' => trim((string) $request->query('q', '')),
'initialCategory' => trim((string) $request->query('category', '')), 'initialCategory' => trim((string) $request->query('category', '')),
'initialSubcategory' => trim((string) $request->query('subcategory', '')), 'initialSubcategory' => trim(
(string) $request->query('subcategory', ''),
),
'canonicalPath' => '/', 'canonicalPath' => '/',
'userTier' => $request->user()?->tier, 'userTier' => $request->user()?->tier,
]); ]);
@@ -59,14 +66,20 @@ class SiteController extends Controller
public function browse(Request $request): RedirectResponse|View public function browse(Request $request): RedirectResponse|View
{ {
$cat = strtolower(trim((string) $request->query('cat', 'all'))); $cat = strtolower(trim((string) $request->query('cat', 'all')));
if ($cat !== '' && $cat !== 'all' && array_key_exists($cat, $this->categorySlugMap())) { if (
$cat !== '' &&
$cat !== 'all' &&
array_key_exists($cat, $this->categorySlugMap())
) {
return redirect('/'.$cat, 301); return redirect('/'.$cat, 301);
} }
return view('site.home', [ return view('site.home', [
'initialQuery' => trim((string) $request->query('q', '')), 'initialQuery' => trim((string) $request->query('q', '')),
'initialCategory' => trim((string) $request->query('category', '')), 'initialCategory' => trim((string) $request->query('category', '')),
'initialSubcategory' => trim((string) $request->query('subcategory', '')), 'initialSubcategory' => trim(
(string) $request->query('subcategory', ''),
),
'canonicalPath' => '/browse', 'canonicalPath' => '/browse',
'userTier' => $request->user()?->tier, 'userTier' => $request->user()?->tier,
]); ]);
@@ -96,8 +109,10 @@ class SiteController extends Controller
]); ]);
} }
public function categorySubcategory(string $categorySlug, string $subcategorySlug): View public function categorySubcategory(
{ string $categorySlug,
string $subcategorySlug,
): View {
if ($categorySlug === 'all') { if ($categorySlug === 'all') {
abort(404); abort(404);
} }
@@ -135,7 +150,8 @@ class SiteController extends Controller
$getPlanAmount = function (string $code) use ($plans, $fallback): int { $getPlanAmount = function (string $code) use ($plans, $fallback): int {
$plan = $plans->get($code) ?? $fallback->get($code); $plan = $plans->get($code) ?? $fallback->get($code);
return (int) ($plan['amount'] ?? $plan->amount ?? 0);
return (int) ($plan['amount'] ?? ($plan->amount ?? 0));
}; };
$pricing = [ $pricing = [
@@ -151,7 +167,8 @@ class SiteController extends Controller
]; ];
foreach ($pricing as $key => $row) { foreach ($pricing as $key => $row) {
$pricing[$key]['usd'] = $rate > 0 ? round($row['idr'] / $rate, 2) : 0; $pricing[$key]['usd'] =
$rate > 0 ? round($row['idr'] / $rate, 2) : 0;
} }
$hasActiveLifetime = false; $hasActiveLifetime = false;
@@ -163,7 +180,8 @@ class SiteController extends Controller
->where('plan', 'personal_lifetime') ->where('plan', 'personal_lifetime')
->where('status', 'active') ->where('status', 'active')
->where(function ($query) { ->where(function ($query) {
$query->whereNull('expires_at') $query
->whereNull('expires_at')
->orWhere('expires_at', '>', now()); ->orWhere('expires_at', '>', now());
}) })
->exists(); ->exists();
@@ -171,7 +189,10 @@ class SiteController extends Controller
->where('user_id', $user->id) ->where('user_id', $user->id)
->where('status', 'pending') ->where('status', 'pending')
->exists(); ->exists();
$cooldown = (int) config('dewemoji.billing.pending_cooldown_seconds', 120); $cooldown = (int) config(
'dewemoji.billing.pending_cooldown_seconds',
120,
);
if ($cooldown > 0) { if ($cooldown > 0) {
$latestPending = Payment::query() $latestPending = Payment::query()
->where('user_id', $user->id) ->where('user_id', $user->id)
@@ -179,7 +200,11 @@ class SiteController extends Controller
->orderByDesc('id') ->orderByDesc('id')
->first(); ->first();
if ($latestPending && $latestPending->created_at) { if ($latestPending && $latestPending->created_at) {
$age = max(0, now()->getTimestamp() - $latestPending->created_at->getTimestamp()); $age = max(
0,
now()->getTimestamp() -
$latestPending->created_at->getTimestamp(),
);
$pendingCooldownRemaining = max(0, $cooldown - $age); $pendingCooldownRemaining = max(0, $cooldown - $age);
} }
} }
@@ -191,14 +216,31 @@ class SiteController extends Controller
'pricing' => $pricing, 'pricing' => $pricing,
'payments' => [ 'payments' => [
'qris_url' => (string) config('dewemoji.payments.qris_url', ''), 'qris_url' => (string) config('dewemoji.payments.qris_url', ''),
'paypal_url' => (string) config('dewemoji.payments.paypal_url', ''), 'paypal_url' => (string) config(
'dewemoji.payments.paypal_url',
'',
),
], ],
'pakasirEnabled' => (bool) config('dewemoji.billing.providers.pakasir.enabled', false) 'pakasirEnabled' => (bool) config(
&& (string) config('dewemoji.billing.providers.pakasir.api_base', '') !== '' 'dewemoji.billing.providers.pakasir.enabled',
&& (string) config('dewemoji.billing.providers.pakasir.api_key', '') !== '' false,
&& (string) config('dewemoji.billing.providers.pakasir.project', '') !== '', ) &&
(string) config(
'dewemoji.billing.providers.pakasir.api_base',
'',
) !== '' &&
(string) config(
'dewemoji.billing.providers.pakasir.api_key',
'',
) !== '' &&
(string) config(
'dewemoji.billing.providers.pakasir.project',
'',
) !== '',
'paypalEnabled' => $this->paypalEnabled($this->billingMode()), 'paypalEnabled' => $this->paypalEnabled($this->billingMode()),
'paypalPlans' => $this->paypalPlanAvailability($this->billingMode()), 'paypalPlans' => $this->paypalPlanAvailability(
$this->billingMode(),
),
'hasActiveLifetime' => $hasActiveLifetime, 'hasActiveLifetime' => $hasActiveLifetime,
'hasPendingPayment' => $hasPendingPayment, 'hasPendingPayment' => $hasPendingPayment,
'pendingCooldownRemaining' => $pendingCooldownRemaining, 'pendingCooldownRemaining' => $pendingCooldownRemaining,
@@ -212,22 +254,43 @@ class SiteController extends Controller
} }
$fallback = $mode === 'live' ? 'sandbox' : 'live'; $fallback = $mode === 'live' ? 'sandbox' : 'live';
return $this->paypalConfiguredMode($fallback); return $this->paypalConfiguredMode($fallback);
} }
private function paypalConfiguredMode(string $mode): bool private function paypalConfiguredMode(string $mode): bool
{ {
$enabled = (bool) config('dewemoji.billing.providers.paypal.enabled', false); $enabled = (bool) config(
$clientId = (string) config("dewemoji.billing.providers.paypal.{$mode}.client_id", ''); 'dewemoji.billing.providers.paypal.enabled',
$clientSecret = (string) config("dewemoji.billing.providers.paypal.{$mode}.client_secret", ''); false,
$apiBase = (string) config("dewemoji.billing.providers.paypal.{$mode}.api_base", ''); );
$clientId = (string) config(
"dewemoji.billing.providers.paypal.{$mode}.client_id",
'',
);
$clientSecret = (string) config(
"dewemoji.billing.providers.paypal.{$mode}.client_secret",
'',
);
$apiBase = (string) config(
"dewemoji.billing.providers.paypal.{$mode}.api_base",
'',
);
return $enabled && $clientId !== '' && $clientSecret !== '' && $apiBase !== ''; return $enabled &&
$clientId !== '' &&
$clientSecret !== '' &&
$apiBase !== '';
} }
private function paypalPlanAvailability(string $mode): array private function paypalPlanAvailability(string $mode): array
{ {
$plans = PricingPlan::whereIn('code', ['personal_monthly', 'personal_annual'])->get()->keyBy('code'); $plans = PricingPlan::whereIn('code', [
'personal_monthly',
'personal_annual',
])
->get()
->keyBy('code');
$fromDb = function (string $code) use ($plans, $mode): bool { $fromDb = function (string $code) use ($plans, $mode): bool {
$plan = $plans->get($code); $plan = $plans->get($code);
@@ -235,11 +298,15 @@ class SiteController extends Controller
return false; return false;
} }
$meta = $plan->meta ?? []; $meta = $plan->meta ?? [];
return (string) ($meta['paypal'][$mode]['plan']['id'] ?? '') !== ''; return (string) ($meta['paypal'][$mode]['plan']['id'] ?? '') !== '';
}; };
$fromEnv = function (string $code) use ($mode): bool { $fromEnv = function (string $code) use ($mode): bool {
return (string) config("dewemoji.billing.providers.paypal.plan_ids.{$mode}.{$code}", '') !== ''; return (string) config(
"dewemoji.billing.providers.paypal.plan_ids.{$mode}.{$code}",
'',
) !== '';
}; };
return [ return [
@@ -255,6 +322,7 @@ class SiteController extends Controller
]); ]);
session(['pricing_currency' => $data['currency']]); session(['pricing_currency' => $data['currency']]);
return back(); return back();
} }
@@ -265,21 +333,32 @@ class SiteController extends Controller
public function download(): View public function download(): View
{ {
$downloadBaseUrl = rtrim((string) config('dewemoji.apk_release.public_base_url', ''), '/'); $downloadBaseUrl = rtrim(
$androidEnabled = (bool) config('dewemoji.apk_release.enabled', false) && $downloadBaseUrl !== ''; (string) config('dewemoji.apk_release.public_base_url', ''),
'/',
);
$androidEnabled = false;
return view('site.download', [ return view('site.download', [
'androidEnabled' => $androidEnabled, 'androidEnabled' => $androidEnabled,
'androidVersionJsonUrl' => $androidEnabled ? $downloadBaseUrl.'/version.json' : '', 'androidVersionJsonUrl' => $androidEnabled
'androidLatestApkUrl' => $androidEnabled ? $downloadBaseUrl.'/dewemoji-latest.apk' : '', ? $downloadBaseUrl.'/version.json'
: '',
'androidLatestApkUrl' => $androidEnabled
? $downloadBaseUrl.'/dewemoji-latest.apk'
: '',
]); ]);
} }
public function downloadVersionJson(Request $request): RedirectResponse|JsonResponse public function downloadVersionJson(
{ Request $request,
): RedirectResponse|JsonResponse {
$target = $this->apkReleaseTargetUrl('version_json'); $target = $this->apkReleaseTargetUrl('version_json');
if ($target === '') { if ($target === '') {
return response()->json(['ok' => false, 'error' => 'apk_release_not_configured'], 404); return response()->json(
['ok' => false, 'error' => 'apk_release_not_configured'],
404,
);
} }
return redirect()->away($target, 302, [ return redirect()->away($target, 302, [
@@ -288,11 +367,15 @@ class SiteController extends Controller
]); ]);
} }
public function downloadLatestApk(Request $request): RedirectResponse|JsonResponse public function downloadLatestApk(
{ Request $request,
): RedirectResponse|JsonResponse {
$target = $this->apkReleaseTargetUrl('latest_apk'); $target = $this->apkReleaseTargetUrl('latest_apk');
if ($target === '') { if ($target === '') {
return response()->json(['ok' => false, 'error' => 'apk_release_not_configured'], 404); return response()->json(
['ok' => false, 'error' => 'apk_release_not_configured'],
404,
);
} }
return redirect()->away($target, 302, [ return redirect()->away($target, 302, [
@@ -304,10 +387,15 @@ class SiteController extends Controller
public function assetLinks(): JsonResponse public function assetLinks(): JsonResponse
{ {
$appId = trim((string) config('dewemoji.apk_release.app_id', '')); $appId = trim((string) config('dewemoji.apk_release.app_id', ''));
$rawFingerprints = (array) config('dewemoji.apk_release.assetlinks.fingerprints', []); $rawFingerprints = (array) config(
'dewemoji.apk_release.assetlinks.fingerprints',
[],
);
$fingerprints = []; $fingerprints = [];
foreach ($rawFingerprints as $fingerprint) { foreach ($rawFingerprints as $fingerprint) {
$normalized = $this->normalizeApkCertFingerprint((string) $fingerprint); $normalized = $this->normalizeApkCertFingerprint(
(string) $fingerprint,
);
if ($normalized !== '') { if ($normalized !== '') {
$fingerprints[] = $normalized; $fingerprints[] = $normalized;
} }
@@ -321,7 +409,8 @@ class SiteController extends Controller
]); ]);
} }
return response()->json([ return response()->json(
[
[ [
'relation' => [ 'relation' => [
'delegate_permission/common.handle_all_urls', 'delegate_permission/common.handle_all_urls',
@@ -332,10 +421,13 @@ class SiteController extends Controller
'sha256_cert_fingerprints' => $fingerprints, 'sha256_cert_fingerprints' => $fingerprints,
], ],
], ],
], 200, [ ],
200,
[
'Cache-Control' => 'no-store, no-cache, must-revalidate', 'Cache-Control' => 'no-store, no-cache, must-revalidate',
'Pragma' => 'no-cache', 'Pragma' => 'no-cache',
]); ],
);
} }
public function privacy(): View public function privacy(): View
@@ -350,12 +442,14 @@ class SiteController extends Controller
private function detectPricingCurrency(Request $request): string private function detectPricingCurrency(Request $request): string
{ {
$country = strtoupper((string) ($request->header('CF-IPCountry') $country = strtoupper(
?? $request->header('X-Country-Code') (string) ($request->header('CF-IPCountry') ??
?? $request->header('X-Geo-Country') ($request->header('X-Country-Code') ??
?? $request->header('X-Appengine-Country') ($request->header('X-Geo-Country') ??
?? $request->header('CloudFront-Viewer-Country') ($request->header('X-Appengine-Country') ??
?? '')); ($request->header('CloudFront-Viewer-Country') ??
''))))),
);
return $country === 'ID' ? 'IDR' : 'USD'; return $country === 'ID' ? 'IDR' : 'USD';
} }
@@ -421,7 +515,8 @@ class SiteController extends Controller
->orderByDesc('id') ->orderByDesc('id')
->get(); ->get();
} }
$limitReached = $keywordLimit !== null && $activeKeywordCount >= $keywordLimit; $limitReached =
$keywordLimit !== null && $activeKeywordCount >= $keywordLimit;
return view('site.emoji-detail', [ return view('site.emoji-detail', [
'emoji' => $match, 'emoji' => $match,
@@ -438,28 +533,64 @@ class SiteController extends Controller
public function robotsTxt(): Response public function robotsTxt(): Response
{ {
$base = rtrim(config('app.url', request()->getSchemeAndHttpHost()), '/'); $base = rtrim(
$body = "User-agent: *\nAllow: /\n\nSitemap: ".$base."/sitemap.xml\n"; config('app.url', request()->getSchemeAndHttpHost()),
'/',
);
$body =
"User-agent: *\nAllow: /\n\nSitemap: ".$base."/sitemap.xml\n";
return response($body, 200)->header('Content-Type', 'text/plain; charset=UTF-8'); return response($body, 200)->header(
'Content-Type',
'text/plain; charset=UTF-8',
);
} }
public function sitemapXml(): Response public function sitemapXml(): Response
{ {
$data = $this->loadDataset(); $data = $this->loadDataset();
$items = is_array($data['emojis'] ?? null) ? $data['emojis'] : []; $items = is_array($data['emojis'] ?? null) ? $data['emojis'] : [];
$base = rtrim(config('app.url', request()->getSchemeAndHttpHost()), '/'); $base = rtrim(
config('app.url', request()->getSchemeAndHttpHost()),
'/',
);
$lastUpdatedTs = isset($data['last_updated_ts']) ? (int) $data['last_updated_ts'] : time(); $lastUpdatedTs = isset($data['last_updated_ts'])
$lastUpdated = gmdate('Y-m-d\TH:i:s\Z', $lastUpdatedTs); ? (int) $data['last_updated_ts']
: time();
$lastUpdated = gmdate("Y-m-d\TH:i:s\Z", $lastUpdatedTs);
$urls = [ $urls = [
['loc' => $base.'/', 'priority' => '0.8', 'changefreq' => 'daily'], [
['loc' => $base.'/api-docs', 'priority' => '0.5', 'changefreq' => 'weekly'], 'loc' => $base.'/',
['loc' => $base.'/pricing', 'priority' => '0.7', 'changefreq' => 'weekly'], 'priority' => '0.8',
['loc' => $base.'/privacy', 'priority' => '0.3', 'changefreq' => 'monthly'], 'changefreq' => 'daily',
['loc' => $base.'/terms', 'priority' => '0.3', 'changefreq' => 'monthly'], ],
['loc' => $base.'/support', 'priority' => '0.4', 'changefreq' => 'weekly'], [
'loc' => $base.'/api-docs',
'priority' => '0.5',
'changefreq' => 'weekly',
],
[
'loc' => $base.'/pricing',
'priority' => '0.7',
'changefreq' => 'weekly',
],
[
'loc' => $base.'/privacy',
'priority' => '0.3',
'changefreq' => 'monthly',
],
[
'loc' => $base.'/terms',
'priority' => '0.3',
'changefreq' => 'monthly',
],
[
'loc' => $base.'/support',
'priority' => '0.4',
'changefreq' => 'weekly',
],
]; ];
foreach ($items as $item) { foreach ($items as $item) {
@@ -475,10 +606,15 @@ class SiteController extends Controller
} }
$xml = '<?xml version="1.0" encoding="UTF-8"?>'."\n"; $xml = '<?xml version="1.0" encoding="UTF-8"?>'."\n";
$xml .= '<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">'."\n"; $xml .=
'<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">'.
"\n";
foreach ($urls as $url) { foreach ($urls as $url) {
$xml .= " <url>\n"; $xml .= " <url>\n";
$xml .= ' <loc>'.htmlspecialchars((string) $url['loc'], ENT_XML1)."</loc>\n"; $xml .=
' <loc>'.
htmlspecialchars((string) $url['loc'], ENT_XML1).
"</loc>\n";
$xml .= ' <lastmod>'.$lastUpdated."</lastmod>\n"; $xml .= ' <lastmod>'.$lastUpdated."</lastmod>\n";
$xml .= ' <changefreq>'.$url['changefreq']."</changefreq>\n"; $xml .= ' <changefreq>'.$url['changefreq']."</changefreq>\n";
$xml .= ' <priority>'.$url['priority']."</priority>\n"; $xml .= ' <priority>'.$url['priority']."</priority>\n";
@@ -486,7 +622,10 @@ class SiteController extends Controller
} }
$xml .= '</urlset>'."\n"; $xml .= '</urlset>'."\n";
return response($xml, 200)->header('Content-Type', 'application/xml; charset=UTF-8'); return response($xml, 200)->header(
'Content-Type',
'application/xml; charset=UTF-8',
);
} }
/** /**
@@ -542,8 +681,12 @@ class SiteController extends Controller
return ''; return '';
} }
$base = trim((string) config('dewemoji.apk_release.r2_public_base_url', '')); $base = trim(
$objectKey = trim((string) config("dewemoji.apk_release.r2_keys.{$key}", '')); (string) config('dewemoji.apk_release.r2_public_base_url', ''),
);
$objectKey = trim(
(string) config("dewemoji.apk_release.r2_keys.{$key}", ''),
);
if ($base === '' || $objectKey === '') { if ($base === '' || $objectKey === '') {
return ''; return '';
} }
@@ -581,32 +724,32 @@ class SiteController extends Controller
if ($subcategory === 'family' || str_starts_with($name, 'family:')) { if ($subcategory === 'family' || str_starts_with($name, 'family:')) {
return true; return true;
} }
if (preg_match('~\bwoman: beard\b~i', $name)) { if (preg_match("~\bwoman: beard\b~i", $name)) {
return true; return true;
} }
if (preg_match('~\bmen with bunny ears\b~i', $name)) { if (preg_match("~\bmen with bunny ears\b~i", $name)) {
return true; return true;
} }
if (preg_match('~\bpregnant man\b~i', $name)) { if (preg_match("~\bpregnant man\b~i", $name)) {
return true; return true;
} }
if ($category === 'people & body') { if ($category === 'people & body') {
if (preg_match('~\bmen holding hands\b~i', $name)) { if (preg_match("~\bmen holding hands\b~i", $name)) {
return true; return true;
} }
if (preg_match('~\bwomen holding hands\b~i', $name)) { if (preg_match("~\bwomen holding hands\b~i", $name)) {
return true; return true;
} }
if (preg_match('~kiss:.*\bman,\s*man\b~i', $name)) { if (preg_match("~kiss:.*\bman,\s*man\b~i", $name)) {
return true; return true;
} }
if (preg_match('~kiss:.*\bwoman,\s*woman\b~i', $name)) { if (preg_match("~kiss:.*\bwoman,\s*woman\b~i", $name)) {
return true; return true;
} }
if (preg_match('~couple.*\bman,\s*man\b~i', $name)) { if (preg_match("~couple.*\bman,\s*man\b~i", $name)) {
return true; return true;
} }
if (preg_match('~couple.*\bwoman,\s*woman\b~i', $name)) { if (preg_match("~couple.*\bwoman,\s*woman\b~i", $name)) {
return true; return true;
} }
} }

View File

@@ -2,10 +2,10 @@
namespace App\Models; namespace App\Models;
use Illuminate\Contracts\Auth\MustVerifyEmail as MustVerifyEmailContract;
use Illuminate\Auth\MustVerifyEmail;
use App\Notifications\ResetPasswordNotification; use App\Notifications\ResetPasswordNotification;
use App\Notifications\VerifyEmailNotification; use App\Notifications\VerifyEmailNotification;
use Illuminate\Auth\MustVerifyEmail;
use Illuminate\Contracts\Auth\MustVerifyEmail as MustVerifyEmailContract;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable; use Illuminate\Notifications\Notifiable;
@@ -13,7 +13,7 @@ use Illuminate\Notifications\Notifiable;
class User extends Authenticatable implements MustVerifyEmailContract class User extends Authenticatable implements MustVerifyEmailContract
{ {
/** @use HasFactory<\Database\Factories\UserFactory> */ /** @use HasFactory<\Database\Factories\UserFactory> */
use HasFactory, Notifiable, MustVerifyEmail; use HasFactory, MustVerifyEmail, Notifiable;
/** /**
* The attributes that are mass assignable. * The attributes that are mass assignable.
@@ -58,7 +58,7 @@ class User extends Authenticatable implements MustVerifyEmailContract
public function sendEmailVerificationNotification(): void public function sendEmailVerificationNotification(): void
{ {
$this->notify(new VerifyEmailNotification()); $this->notify(new VerifyEmailNotification);
} }
public function sendPasswordResetNotification($token): void public function sendPasswordResetNotification($token): void

View File

@@ -2,10 +2,10 @@
namespace App\Providers; namespace App\Providers;
use Illuminate\Support\ServiceProvider; use App\Mail\MailketingTransport;
use Illuminate\Support\Facades\Gate; use Illuminate\Support\Facades\Gate;
use Illuminate\Support\Facades\Mail; use Illuminate\Support\Facades\Mail;
use App\Mail\MailketingTransport; use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider class AppServiceProvider extends ServiceProvider
{ {

View File

@@ -2,8 +2,8 @@
namespace App\Providers; namespace App\Providers;
use Native\Desktop\Facades\Window;
use Native\Desktop\Contracts\ProvidesPhpIni; use Native\Desktop\Contracts\ProvidesPhpIni;
use Native\Desktop\Facades\Window;
class NativeAppServiceProvider implements ProvidesPhpIni class NativeAppServiceProvider implements ProvidesPhpIni
{ {

View File

@@ -23,12 +23,14 @@ class PayPalPlanSyncService
$token = $this->getAccessToken($mode); $token = $this->getAccessToken($mode);
if (! $token) { if (! $token) {
Log::warning('PayPal plan sync aborted: missing access token', ['mode' => $mode]); Log::warning('PayPal plan sync aborted: missing access token', ['mode' => $mode]);
return $result; return $result;
} }
$productId = $this->ensureProduct($mode, $token); $productId = $this->ensureProduct($mode, $token);
if (! $productId) { if (! $productId) {
Log::warning('PayPal plan sync aborted: missing product id', ['mode' => $mode]); Log::warning('PayPal plan sync aborted: missing product id', ['mode' => $mode]);
return $result; return $result;
} }
@@ -58,12 +60,14 @@ class PayPalPlanSyncService
$keepIds[] = $currentPlanId; $keepIds[] = $currentPlanId;
} }
$result['skipped']++; $result['skipped']++;
continue; continue;
} }
$newPlanId = $this->createPlan($mode, $token, $productId, $plan->code, $plan->name, $amountUsd, $plan->period); $newPlanId = $this->createPlan($mode, $token, $productId, $plan->code, $plan->name, $amountUsd, $plan->period);
if (! $newPlanId) { if (! $newPlanId) {
$result['skipped']++; $result['skipped']++;
continue; continue;
} }
@@ -105,6 +109,7 @@ class PayPalPlanSyncService
{ {
$rate = (int) config('dewemoji.pricing.usd_rate', 15000); $rate = (int) config('dewemoji.pricing.usd_rate', 15000);
$usd = $rate > 0 ? $idrAmount / $rate : 0; $usd = $rate > 0 ? $idrAmount / $rate : 0;
return number_format(max($usd, 1), 2, '.', ''); return number_format(max($usd, 1), 2, '.', '');
} }
@@ -138,6 +143,7 @@ class PayPalPlanSyncService
if (! $res->successful()) { if (! $res->successful()) {
Log::warning('PayPal auth failed', ['body' => $res->body()]); Log::warning('PayPal auth failed', ['body' => $res->body()]);
return null; return null;
} }
@@ -181,6 +187,7 @@ class PayPalPlanSyncService
'body' => $create->body(), 'body' => $create->body(),
]); ]);
} }
return $createdId; return $createdId;
} }
@@ -188,6 +195,7 @@ class PayPalPlanSyncService
'status' => $create->status(), 'status' => $create->status(),
'body' => $create->body(), 'body' => $create->body(),
]); ]);
return null; return null;
} }
@@ -244,6 +252,7 @@ class PayPalPlanSyncService
'body' => $res->body(), 'body' => $res->body(),
]); ]);
} }
return $planId; return $planId;
} }
@@ -252,6 +261,7 @@ class PayPalPlanSyncService
'status' => $res->status(), 'status' => $res->status(),
'body' => $res->body(), 'body' => $res->body(),
]); ]);
return null; return null;
} }

View File

@@ -14,8 +14,7 @@ class PaypalWebhookProcessor
public function __construct( public function __construct(
private readonly SubscriptionTransitionService $subscriptionTransition, private readonly SubscriptionTransitionService $subscriptionTransition,
private readonly KeywordQuotaService $keywordQuota private readonly KeywordQuotaService $keywordQuota
) { ) {}
}
/** /**
* @param array<string,mixed> $payload * @param array<string,mixed> $payload
@@ -68,6 +67,7 @@ class PaypalWebhookProcessor
$subscriptionId, $subscriptionId,
$planCode $planCode
); );
return; return;
} }

View File

@@ -67,6 +67,7 @@ class SubscriptionTransitionService
$sub->status = 'canceled'; $sub->status = 'canceled';
$sub->canceled_at = now(); $sub->canceled_at = now();
$sub->save(); $sub->save();
continue; continue;
} }
@@ -80,6 +81,7 @@ class SubscriptionTransitionService
'new_plan' => $newPlanCode, 'new_plan' => $newPlanCode,
'old_subscription_id' => $sub->provider_ref, 'old_subscription_id' => $sub->provider_ref,
]); ]);
continue; continue;
} }
} }

View File

@@ -11,8 +11,7 @@ class EmojiCatalogService
{ {
public function __construct( public function __construct(
private readonly SettingsService $settings private readonly SettingsService $settings
) { ) {}
}
/** /**
* @return array<string,mixed>|null * @return array<string,mixed>|null
@@ -203,10 +202,12 @@ class EmojiCatalogService
$emojiId = (int) ($row['emoji_id'] ?? 0); $emojiId = (int) ($row['emoji_id'] ?? 0);
if ($slug !== '' && DB::table('emojis')->where('slug', $slug)->exists()) { if ($slug !== '' && DB::table('emojis')->where('slug', $slug)->exists()) {
$skipped++; $skipped++;
continue; continue;
} }
if ($emojiId > 0 && DB::table('emojis')->where('emoji_id', $emojiId)->exists()) { if ($emojiId > 0 && DB::table('emojis')->where('emoji_id', $emojiId)->exists()) {
$skipped++; $skipped++;
continue; continue;
} }
@@ -509,7 +510,6 @@ class EmojiCatalogService
} }
/** /**
* @param mixed $input
* @return array<int,string> * @return array<int,string>
*/ */
private function normalizeArray(mixed $input): array private function normalizeArray(mixed $input): array

View File

@@ -31,6 +31,7 @@ class KeywordQuotaService
->where('user_id', $userId) ->where('user_id', $userId)
->where('is_active', false) ->where('is_active', false)
->update(['is_active' => true]); ->update(['is_active' => true]);
return; return;
} }
@@ -55,4 +56,3 @@ class KeywordQuotaService
->update(['is_active' => false]); ->update(['is_active' => false]);
} }
} }

View File

@@ -145,7 +145,7 @@ class LiveSqlImportService
$valuesRaw = $matches[3]; $valuesRaw = $matches[3];
$columns = array_map( $columns = array_map(
static fn (string $col): string => trim($col, " `"), static fn (string $col): string => trim($col, ' `'),
explode(',', $columnsRaw) explode(',', $columnsRaw)
); );
@@ -260,20 +260,24 @@ class LiveSqlImportService
if ($escape) { if ($escape) {
$buffer .= $this->unescapeChar($ch); $buffer .= $this->unescapeChar($ch);
$escape = false; $escape = false;
continue; continue;
} }
if ($ch === '\\\\') { if ($ch === '\\\\') {
$escape = true; $escape = true;
continue; continue;
} }
if ($ch === '\'') { if ($ch === '\'') {
$inString = false; $inString = false;
continue; continue;
} }
$buffer .= $ch; $buffer .= $ch;
continue; continue;
} }
@@ -282,12 +286,14 @@ class LiveSqlImportService
$currentRow = []; $currentRow = [];
$buffer = ''; $buffer = '';
$valueIsString = false; $valueIsString = false;
continue; continue;
} }
if ($ch === '\'') { if ($ch === '\'') {
$inString = true; $inString = true;
$valueIsString = true; $valueIsString = true;
continue; continue;
} }
@@ -295,6 +301,7 @@ class LiveSqlImportService
$currentRow[] = $this->convertValue($buffer, $valueIsString); $currentRow[] = $this->convertValue($buffer, $valueIsString);
$buffer = ''; $buffer = '';
$valueIsString = false; $valueIsString = false;
continue; continue;
} }
@@ -306,6 +313,7 @@ class LiveSqlImportService
$buffer = ''; $buffer = '';
$valueIsString = false; $valueIsString = false;
$inRow = false; $inRow = false;
continue; continue;
} }

View File

@@ -8,6 +8,7 @@ use Illuminate\Support\Facades\Cache;
class SettingsService class SettingsService
{ {
private const CACHE_KEY = 'dw_settings_all'; private const CACHE_KEY = 'dw_settings_all';
private const CACHE_TTL = 30; private const CACHE_TTL = 30;
/** /**
@@ -20,6 +21,7 @@ class SettingsService
foreach (Setting::all(['key', 'value']) as $setting) { foreach (Setting::all(['key', 'value']) as $setting) {
$out[$setting->key] = $setting->value; $out[$setting->key] = $setting->value;
} }
return $out; return $out;
}); });
} }
@@ -27,6 +29,7 @@ class SettingsService
public function get(string $key, mixed $default = null): mixed public function get(string $key, mixed $default = null): mixed
{ {
$all = $this->all(); $all = $this->all();
return array_key_exists($key, $all) ? $all[$key] : $default; return array_key_exists($key, $all) ? $all[$key] : $default;
} }

View File

@@ -29,4 +29,3 @@ return new class extends Migration
Schema::dropIfExists('licenses'); Schema::dropIfExists('licenses');
} }
}; };

View File

@@ -28,4 +28,3 @@ return new class extends Migration
Schema::dropIfExists('license_activations'); Schema::dropIfExists('license_activations');
} }
}; };

View File

@@ -26,4 +26,3 @@ return new class extends Migration
Schema::dropIfExists('usage_logs'); Schema::dropIfExists('usage_logs');
} }
}; };

View File

@@ -4,7 +4,8 @@ use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint; use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema; use Illuminate\Support\Facades\Schema;
return new class extends Migration { return new class extends Migration
{
public function up(): void public function up(): void
{ {
Schema::create('admin_audit_logs', function (Blueprint $table): void { Schema::create('admin_audit_logs', function (Blueprint $table): void {

View File

@@ -22,4 +22,3 @@ return new class extends Migration
}); });
} }
}; };

View File

@@ -2,7 +2,6 @@
namespace Database\Seeders; namespace Database\Seeders;
use App\Models\User;
use Illuminate\Database\Console\Seeds\WithoutModelEvents; use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use Illuminate\Database\Seeder; use Illuminate\Database\Seeder;

View File

@@ -1,19 +1,19 @@
<?php <?php
use App\Http\Controllers\Api\V1\EmojiApiController; use App\Http\Controllers\Api\V1\AdminAnalyticsController;
use App\Http\Controllers\Api\V1\SystemController;
use App\Http\Controllers\Api\V1\AdminUserController;
use App\Http\Controllers\Api\V1\AdminPricingController; use App\Http\Controllers\Api\V1\AdminPricingController;
use App\Http\Controllers\Api\V1\AdminSettingsController; use App\Http\Controllers\Api\V1\AdminSettingsController;
use App\Http\Controllers\Api\V1\AdminSubscriptionController; use App\Http\Controllers\Api\V1\AdminSubscriptionController;
use App\Http\Controllers\Api\V1\AdminAnalyticsController; use App\Http\Controllers\Api\V1\AdminUserController;
use App\Http\Controllers\Api\V1\AdminWebhookController; use App\Http\Controllers\Api\V1\AdminWebhookController;
use App\Http\Controllers\Api\V1\EmojiApiController;
use App\Http\Controllers\Api\V1\ExtensionController; use App\Http\Controllers\Api\V1\ExtensionController;
use App\Http\Controllers\Api\V1\PricingController;
use App\Http\Controllers\Api\V1\SystemController;
use App\Http\Controllers\Api\V1\UserController; use App\Http\Controllers\Api\V1\UserController;
use App\Http\Controllers\Api\V1\UserKeywordController; use App\Http\Controllers\Api\V1\UserKeywordController;
use App\Http\Controllers\Api\V1\PricingController;
use App\Http\Controllers\Billing\PayPalController;
use App\Http\Controllers\Billing\PakasirController; use App\Http\Controllers\Billing\PakasirController;
use App\Http\Controllers\Billing\PayPalController;
use Illuminate\Support\Facades\Route; use Illuminate\Support\Facades\Route;
Route::options('/v1/{any}', function () { Route::options('/v1/{any}', function () {

View File

@@ -1,17 +1,17 @@
<?php <?php
use Illuminate\Foundation\Inspiring; use App\Mail\TestMailketing;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\Schedule;
use App\Services\LiveSqlImportService;
use App\Services\Billing\PaypalWebhookProcessor;
use App\Services\Billing\PayPalPlanSyncService;
use App\Models\Order; use App\Models\Order;
use App\Models\Payment; use App\Models\Payment;
use App\Models\Subscription; use App\Models\Subscription;
use App\Models\WebhookEvent; use App\Models\WebhookEvent;
use App\Services\Billing\PayPalPlanSyncService;
use App\Services\Billing\PaypalWebhookProcessor;
use App\Services\LiveSqlImportService;
use Illuminate\Foundation\Inspiring;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\Mail; use Illuminate\Support\Facades\Mail;
use App\Mail\TestMailketing; use Illuminate\Support\Facades\Schedule;
Artisan::command('inspire', function () { Artisan::command('inspire', function () {
$this->comment(Inspiring::quote()); $this->comment(Inspiring::quote());
@@ -32,6 +32,7 @@ Artisan::command('dewemoji:webhooks:process {--limit=100 : Max events to process
if (empty($statuses)) { if (empty($statuses)) {
$this->error('No statuses provided.'); $this->error('No statuses provided.');
return 1; return 1;
} }
@@ -68,6 +69,7 @@ Artisan::command('dewemoji:webhooks:process {--limit=100 : Max events to process
} }
$this->info("Processed {$processed} events, failed {$failed}."); $this->info("Processed {$processed} events, failed {$failed}.");
return 0; return 0;
})->purpose('Process pending webhook events'); })->purpose('Process pending webhook events');
@@ -94,11 +96,13 @@ Artisan::command('mailketing:test {email : Recipient email address}', function (
$email = (string) $this->argument('email'); $email = (string) $this->argument('email');
try { try {
Mail::to($email)->send(new TestMailketing()); Mail::to($email)->send(new TestMailketing);
$this->info("Mailketing test email sent to {$email}."); $this->info("Mailketing test email sent to {$email}.");
return 0; return 0;
} catch (\Throwable $e) { } catch (\Throwable $e) {
$this->error('Mailketing test failed: '.$e->getMessage()); $this->error('Mailketing test failed: '.$e->getMessage());
return 1; return 1;
} }
})->purpose('Send a Mailketing API test email'); })->purpose('Send a Mailketing API test email');
@@ -117,5 +121,6 @@ Artisan::command('dewemoji:normalize-statuses', function () {
->update(['status' => 'canceled']); ->update(['status' => 'canceled']);
$this->info("Normalized statuses: subscriptions={$subs}, orders={$orders}, payments={$payments}"); $this->info("Normalized statuses: subscriptions={$subs}, orders={$orders}, payments={$payments}");
return 0; return 0;
})->purpose('Normalize legacy cancelled status spelling to canceled'); })->purpose('Normalize legacy cancelled status spelling to canceled');

View File

@@ -3,7 +3,6 @@
use App\Http\Controllers\Dashboard\AdminDashboardController; use App\Http\Controllers\Dashboard\AdminDashboardController;
use App\Http\Controllers\Dashboard\AdminEmojiCatalogController; use App\Http\Controllers\Dashboard\AdminEmojiCatalogController;
use App\Http\Controllers\Dashboard\UserDashboardController; use App\Http\Controllers\Dashboard\UserDashboardController;
use App\Http\Controllers\ProfileController;
use Illuminate\Support\Facades\Route; use Illuminate\Support\Facades\Route;
Route::middleware('auth')->prefix('dashboard')->name('dashboard.')->group(function () { Route::middleware('auth')->prefix('dashboard')->name('dashboard.')->group(function () {

View File

@@ -1,10 +1,10 @@
<?php <?php
use App\Http\Controllers\Billing\BillingPaymentController;
use App\Http\Controllers\Billing\PakasirController;
use App\Http\Controllers\Billing\PayPalController;
use App\Http\Controllers\ProfileController; use App\Http\Controllers\ProfileController;
use App\Http\Controllers\Web\SiteController; use App\Http\Controllers\Web\SiteController;
use App\Http\Controllers\Billing\PayPalController;
use App\Http\Controllers\Billing\PakasirController;
use App\Http\Controllers\Billing\BillingPaymentController;
use Illuminate\Support\Facades\Route; use Illuminate\Support\Facades\Route;
Route::get('/', [SiteController::class, 'home'])->name('home'); Route::get('/', [SiteController::class, 'home'])->name('home');