style: format with pint

This commit is contained in:
Dwindi Ramadhana
2026-06-14 15:47:37 +07:00
parent c3ce549264
commit 9937da6a7b
45 changed files with 576 additions and 538 deletions

View File

@@ -18,9 +18,10 @@ class AdminAnalyticsController extends Controller
{ {
$token = (string) config('dewemoji.admin.token', ''); $token = (string) config('dewemoji.admin.token', '');
$provided = trim((string) $request->header('X-Admin-Token', '')); $provided = trim((string) $request->header('X-Admin-Token', ''));
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,7 +15,7 @@ class AdminPricingController extends Controller
{ {
$adminToken = (string) config('dewemoji.admin.token', ''); $adminToken = (string) config('dewemoji.admin.token', '');
$provided = trim((string) $request->header('X-Admin-Token', '')); $provided = trim((string) $request->header('X-Admin-Token', ''));
if ($adminToken === '' || $provided === '' || !hash_equals($adminToken, $provided)) { if ($adminToken === '' || $provided === '' || ! hash_equals($adminToken, $provided)) {
return response()->json(['ok' => false, 'error' => 'unauthorized'], 401); return response()->json(['ok' => false, 'error' => 'unauthorized'], 401);
} }

View File

@@ -9,17 +9,16 @@ 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
{ {
$token = (string) config('dewemoji.admin.token', ''); $token = (string) config('dewemoji.admin.token', '');
$provided = trim((string) $request->header('X-Admin-Token', '')); $provided = trim((string) $request->header('X-Admin-Token', ''));
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;
} }
@@ -46,7 +45,7 @@ class AdminSettingsController extends Controller
} }
$payload = $request->input('settings'); $payload = $request->input('settings');
if (!is_array($payload)) { if (! is_array($payload)) {
return response()->json(['ok' => false, 'error' => 'invalid_payload'], 422); return response()->json(['ok' => false, 'error' => 'invalid_payload'], 422);
} }

View File

@@ -15,16 +15,16 @@ 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
{ {
$token = (string) config('dewemoji.admin.token', ''); $token = (string) config('dewemoji.admin.token', '');
$provided = trim((string) $request->header('X-Admin-Token', '')); $provided = trim((string) $request->header('X-Admin-Token', ''));
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;
} }
@@ -59,7 +59,7 @@ class AdminSubscriptionController extends Controller
} }
$user = $this->resolveUser($request); $user = $this->resolveUser($request);
if (!$user) { if (! $user) {
return response()->json(['ok' => false, 'error' => 'not_found'], 404); return response()->json(['ok' => false, 'error' => 'not_found'], 404);
} }
@@ -99,16 +99,17 @@ class AdminSubscriptionController extends Controller
if ($id > 0) { if ($id > 0) {
$sub = Subscription::find($id); $sub = Subscription::find($id);
if (!$sub) { if (! $sub) {
return response()->json(['ok' => false, 'error' => 'not_found'], 404); return response()->json(['ok' => false, 'error' => 'not_found'], 404);
} }
$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]);
} }
$user = $this->resolveUser($request); $user = $this->resolveUser($request);
if (!$user) { if (! $user) {
return response()->json(['ok' => false, 'error' => 'not_found'], 404); return response()->json(['ok' => false, 'error' => 'not_found'], 404);
} }
@@ -135,7 +136,7 @@ class AdminSubscriptionController extends Controller
private function parseDate(mixed $value): ?Carbon private function parseDate(mixed $value): ?Carbon
{ {
if (!$value) { if (! $value) {
return null; return null;
} }
try { try {
@@ -159,7 +160,7 @@ class AdminSubscriptionController extends Controller
'tier' => $active ? 'personal' : 'free', 'tier' => $active ? 'personal' : 'free',
]); ]);
$this->keywordQuota->enforceForUser($userId, $active ? 'personal' : 'free'); $this->keywordQuota->enforceForUser($userId, $active ? 'personal' : 'free');
if (!$active) { if (! $active) {
UserApiKey::where('user_id', $userId)->update(['revoked_at' => now()]); UserApiKey::where('user_id', $userId)->update(['revoked_at' => now()]);
} }
} }

View File

@@ -12,14 +12,13 @@ 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
{ {
$adminToken = (string) config('dewemoji.admin.token', ''); $adminToken = (string) config('dewemoji.admin.token', '');
$provided = trim((string) $request->header('X-Admin-Token', '')); $provided = trim((string) $request->header('X-Admin-Token', ''));
if ($adminToken === '' || $provided === '' || !hash_equals($adminToken, $provided)) { if ($adminToken === '' || $provided === '' || ! hash_equals($adminToken, $provided)) {
return response()->json(['ok' => false, 'error' => 'unauthorized'], 401); return response()->json(['ok' => false, 'error' => 'unauthorized'], 401);
} }
@@ -71,7 +70,7 @@ class AdminUserController extends Controller
/** @var User|null $user */ /** @var User|null $user */
$user = $query->first(); $user = $query->first();
if (!$user) { if (! $user) {
return response()->json(['ok' => false, 'error' => 'not_found'], 404); return response()->json(['ok' => false, 'error' => 'not_found'], 404);
} }
@@ -100,9 +99,9 @@ class AdminUserController extends Controller
]); ]);
$query = User::query(); $query = User::query();
if (!empty($data['email'])) { if (! empty($data['email'])) {
$query->where('email', $data['email']); $query->where('email', $data['email']);
} elseif (!empty($data['user_id'])) { } elseif (! empty($data['user_id'])) {
$query->where('id', $data['user_id']); $query->where('id', $data['user_id']);
} else { } else {
return response()->json(['ok' => false, 'error' => 'missing_target'], 400); return response()->json(['ok' => false, 'error' => 'missing_target'], 400);
@@ -110,7 +109,7 @@ class AdminUserController extends Controller
/** @var User|null $user */ /** @var User|null $user */
$user = $query->first(); $user = $query->first();
if (!$user) { if (! $user) {
return response()->json(['ok' => false, 'error' => 'not_found'], 404); return response()->json(['ok' => false, 'error' => 'not_found'], 404);
} }

View File

@@ -10,17 +10,16 @@ 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
{ {
$token = (string) config('dewemoji.admin.token', ''); $token = (string) config('dewemoji.admin.token', '');
$provided = trim((string) $request->header('X-Admin-Token', '')); $provided = trim((string) $request->header('X-Admin-Token', ''));
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;
} }
@@ -51,7 +50,7 @@ class AdminWebhookController extends Controller
} }
$item = WebhookEvent::find($id); $item = WebhookEvent::find($id);
if (!$item) { if (! $item) {
return response()->json(['ok' => false, 'error' => 'not_found'], 404); return response()->json(['ok' => false, 'error' => 'not_found'], 404);
} }
@@ -65,7 +64,7 @@ class AdminWebhookController extends Controller
} }
$item = WebhookEvent::find($id); $item = WebhookEvent::find($id);
if (!$item) { if (! $item) {
return response()->json(['ok' => false, 'error' => 'not_found'], 404); return response()->json(['ok' => false, 'error' => 'not_found'], 404);
} }

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 = [
@@ -149,7 +149,7 @@ class EmojiApiController extends Controller
} }
$sourceItem = $itemsBySlug[$slug] ?? null; $sourceItem = $itemsBySlug[$slug] ?? null;
if (!$sourceItem) { if (! $sourceItem) {
continue; continue;
} }
@@ -213,12 +213,12 @@ class EmojiApiController extends Controller
public function search(Request $request): JsonResponse public function search(Request $request): JsonResponse
{ {
$private = filter_var($request->query('private', false), FILTER_VALIDATE_BOOL); $private = filter_var($request->query('private', false), FILTER_VALIDATE_BOOL);
if (!$private) { if (! $private) {
return $this->emojis($request); return $this->emojis($request);
} }
$user = $this->apiKeys->resolveUser($request) ?? $request->user(); $user = $this->apiKeys->resolveUser($request) ?? $request->user();
if (!$user) { if (! $user) {
return response()->json(['ok' => false, 'error' => 'unauthorized'], 401); return response()->json(['ok' => false, 'error' => 'unauthorized'], 401);
} }
$q = trim((string) ($request->query('q', $request->query('query', '')))); $q = trim((string) ($request->query('q', $request->query('query', ''))));
@@ -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;
} }
@@ -431,7 +432,7 @@ class EmojiApiController extends Controller
? $activePath ? $activePath
: (string) config('dewemoji.data_path'); : (string) config('dewemoji.data_path');
if (!is_file($path)) { if (! is_file($path)) {
throw new RuntimeException('Emoji dataset file was not found at: '.$path); throw new RuntimeException('Emoji dataset file was not found at: '.$path);
} }
@@ -441,11 +442,12 @@ class EmojiApiController extends Controller
} }
$decoded = json_decode($raw, true); $decoded = json_decode($raw, true);
if (!is_array($decoded)) { if (! is_array($decoded)) {
throw new RuntimeException('Emoji dataset JSON is invalid.'); throw new RuntimeException('Emoji dataset JSON is invalid.');
} }
self::$dataset = $decoded; self::$dataset = $decoded;
return self::$dataset; return self::$dataset;
} }
@@ -464,11 +466,12 @@ 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, '-');
} }
/** /**
* @param array<int,array<string,mixed>> $items * @param array<int,array<string,mixed>> $items
* @return array<int,array<string,mixed>> * @return array<int,array<string,mixed>>
*/ */
private function filterItems(array $items, string $q, string $category, string $subSlug): array private function filterItems(array $items, string $q, string $category, string $subSlug): array
@@ -506,16 +509,17 @@ class EmojiApiController extends Controller
if ($token === '') { if ($token === '') {
continue; continue;
} }
if (!str_contains($haystack, $token)) { if (! str_contains($haystack, $token)) {
return false; return false;
} }
} }
return true; return true;
})); }));
} }
/** /**
* @param array<string,mixed> $item * @param array<string,mixed> $item
* @return array<string,mixed> * @return array<string,mixed>
*/ */
private function transformItem(array $item, string $tier): array private function transformItem(array $item, string $tier): array
@@ -560,7 +564,7 @@ class EmojiApiController extends Controller
} }
/** /**
* @param array<string,mixed> $item * @param array<string,mixed> $item
* @return array<string,mixed> * @return array<string,mixed>
*/ */
private function transformEmojiDetail(array $item, string $tier): array private function transformEmojiDetail(array $item, string $tier): array
@@ -583,12 +587,12 @@ class EmojiApiController extends Controller
return $text; return $text;
} }
return rtrim(mb_substr($text, 0, $max - 1), " ,.;:-").'…'; return rtrim(mb_substr($text, 0, $max - 1), ' ,.;:-').'…';
} }
/** /**
* @param array<string,mixed> $payload * @param array<string,mixed> $payload
* @param array<string,string> $extraHeaders * @param array<string,string> $extraHeaders
*/ */
private function jsonWithTier(Request $request, array $payload, string $tier, int $status = 200, array $extraHeaders = []): JsonResponse private function jsonWithTier(Request $request, array $payload, string $tier, int $status = 200, array $extraHeaders = []): JsonResponse
{ {

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
{ {
@@ -37,7 +35,7 @@ class PaypalWebhookController extends Controller
'error' => $signatureOk ? null : 'signature_unverified', 'error' => $signatureOk ? null : 'signature_unverified',
]); ]);
if (!$signatureOk) { if (! $signatureOk) {
return response()->json(['ok' => true, 'signature' => 'unverified']); return response()->json(['ok' => true, 'signature' => 'unverified']);
} }

View File

@@ -21,7 +21,7 @@ class SystemController extends Controller
public function metricsLite(Request $request): JsonResponse public function metricsLite(Request $request): JsonResponse
{ {
if (!$this->metricsEnabled()) { if (! $this->metricsEnabled()) {
return $this->response($request, [ return $this->response($request, [
'ok' => false, 'ok' => false,
'error' => 'metrics_disabled', 'error' => 'metrics_disabled',
@@ -39,14 +39,14 @@ class SystemController extends Controller
public function metrics(Request $request): JsonResponse public function metrics(Request $request): JsonResponse
{ {
if (!$this->metricsEnabled()) { if (! $this->metricsEnabled()) {
return $this->response($request, [ return $this->response($request, [
'ok' => false, 'ok' => false,
'error' => 'metrics_disabled', 'error' => 'metrics_disabled',
], 404); ], 404);
} }
if (!$this->canAccessMetrics($request)) { if (! $this->canAccessMetrics($request)) {
return $this->response($request, [ return $this->response($request, [
'ok' => false, 'ok' => false,
'error' => 'forbidden', 'error' => 'forbidden',
@@ -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';
@@ -111,7 +112,7 @@ class SystemController extends Controller
} }
/** /**
* @param array<string,mixed> $payload * @param array<string,mixed> $payload
*/ */
private function response(Request $request, array $payload, int $status = 200): JsonResponse private function response(Request $request, array $payload, int $status = 200): JsonResponse
{ {

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
{ {
@@ -57,7 +56,7 @@ class UserController extends Controller
/** @var User|null $user */ /** @var User|null $user */
$user = User::where('email', $data['email'])->first(); $user = User::where('email', $data['email'])->first();
if (!$user || !Hash::check($data['password'], $user->password)) { if (! $user || ! Hash::check($data['password'], $user->password)) {
return response()->json([ return response()->json([
'ok' => false, 'ok' => false,
'error' => 'invalid_credentials', 'error' => 'invalid_credentials',
@@ -96,7 +95,7 @@ class UserController extends Controller
public function listApiKeys(Request $request): JsonResponse public function listApiKeys(Request $request): JsonResponse
{ {
$user = $this->keys->resolveUser($request); $user = $this->keys->resolveUser($request);
if (!$user) { if (! $user) {
return response()->json(['ok' => false, 'error' => 'unauthorized'], 401); return response()->json(['ok' => false, 'error' => 'unauthorized'], 401);
} }
@@ -121,7 +120,7 @@ class UserController extends Controller
public function createApiKey(Request $request): JsonResponse public function createApiKey(Request $request): JsonResponse
{ {
$user = $this->keys->resolveUser($request); $user = $this->keys->resolveUser($request);
if (!$user) { if (! $user) {
return response()->json(['ok' => false, 'error' => 'unauthorized'], 401); return response()->json(['ok' => false, 'error' => 'unauthorized'], 401);
} }
if ((string) $user->tier !== 'personal') { if ((string) $user->tier !== 'personal') {
@@ -149,7 +148,7 @@ class UserController extends Controller
public function revokeApiKey(Request $request, string $key): JsonResponse public function revokeApiKey(Request $request, string $key): JsonResponse
{ {
$user = $this->keys->resolveUser($request); $user = $this->keys->resolveUser($request);
if (!$user) { if (! $user) {
return response()->json(['ok' => false, 'error' => 'unauthorized'], 401); return response()->json(['ok' => false, 'error' => 'unauthorized'], 401);
} }

View File

@@ -14,13 +14,12 @@ 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
{ {
$user = $this->keys->resolveUser($request); $user = $this->keys->resolveUser($request);
if (!$user) { if (! $user) {
return ['error' => 'unauthorized', 'status' => 401]; return ['error' => 'unauthorized', 'status' => 401];
} }
@@ -30,7 +29,7 @@ class UserKeywordController extends Controller
public function index(Request $request): JsonResponse public function index(Request $request): JsonResponse
{ {
$check = $this->ensureUser($request); $check = $this->ensureUser($request);
if (!isset($check['user'])) { if (! isset($check['user'])) {
return response()->json(['ok' => false, 'error' => $check['error']], $check['status']); return response()->json(['ok' => false, 'error' => $check['error']], $check['status']);
} }
@@ -45,7 +44,7 @@ class UserKeywordController extends Controller
public function store(Request $request): JsonResponse public function store(Request $request): JsonResponse
{ {
$check = $this->ensureUser($request); $check = $this->ensureUser($request);
if (!isset($check['user'])) { if (! isset($check['user'])) {
return response()->json(['ok' => false, 'error' => $check['error']], $check['status']); return response()->json(['ok' => false, 'error' => $check['error']], $check['status']);
} }
@@ -64,7 +63,7 @@ class UserKeywordController extends Controller
$targetActive = $existing ? (bool) $existing->is_active : true; $targetActive = $existing ? (bool) $existing->is_active : true;
$limit = $this->keywordLimitFor($user); $limit = $this->keywordLimitFor($user);
if ($limit !== null) { if ($limit !== null) {
if (!$existing && $targetActive) { if (! $existing && $targetActive) {
$activeCount = $this->keywordQuota->activeCount((int) $user->id); $activeCount = $this->keywordQuota->activeCount((int) $user->id);
if ($activeCount >= $limit) { if ($activeCount >= $limit) {
return response()->json(['ok' => false, 'error' => 'free_active_limit_reached', 'limit' => $limit], 403); return response()->json(['ok' => false, 'error' => 'free_active_limit_reached', 'limit' => $limit], 403);
@@ -93,7 +92,7 @@ class UserKeywordController extends Controller
public function destroy(Request $request, int $id): JsonResponse public function destroy(Request $request, int $id): JsonResponse
{ {
$check = $this->ensureUser($request); $check = $this->ensureUser($request);
if (!isset($check['user'])) { if (! isset($check['user'])) {
return response()->json(['ok' => false, 'error' => $check['error']], $check['status']); return response()->json(['ok' => false, 'error' => $check['error']], $check['status']);
} }
@@ -107,7 +106,7 @@ class UserKeywordController extends Controller
public function update(Request $request, int $id): JsonResponse public function update(Request $request, int $id): JsonResponse
{ {
$check = $this->ensureUser($request); $check = $this->ensureUser($request);
if (!isset($check['user'])) { if (! isset($check['user'])) {
return response()->json(['ok' => false, 'error' => $check['error']], $check['status']); return response()->json(['ok' => false, 'error' => $check['error']], $check['status']);
} }
@@ -121,7 +120,7 @@ class UserKeywordController extends Controller
->where('id', $id) ->where('id', $id)
->first(); ->first();
if (!$item) { if (! $item) {
return response()->json(['ok' => false, 'error' => 'not_found'], 404); return response()->json(['ok' => false, 'error' => 'not_found'], 404);
} }
@@ -134,11 +133,11 @@ class UserKeywordController extends Controller
$item->delete(); $item->delete();
$item = $duplicate; $item = $duplicate;
} else { } else {
$item->update([ $item->update([
'emoji_slug' => $data['emoji_slug'], 'emoji_slug' => $data['emoji_slug'],
'keyword' => $data['keyword'], 'keyword' => $data['keyword'],
'lang' => $data['lang'] ?? 'und', 'lang' => $data['lang'] ?? 'und',
]); ]);
} }
return response()->json(['ok' => true, 'item' => $item]); return response()->json(['ok' => true, 'item' => $item]);
@@ -147,7 +146,7 @@ class UserKeywordController extends Controller
public function export(Request $request): JsonResponse public function export(Request $request): JsonResponse
{ {
$check = $this->ensureUser($request); $check = $this->ensureUser($request);
if (!isset($check['user'])) { if (! isset($check['user'])) {
return response()->json(['ok' => false, 'error' => $check['error']], $check['status']); return response()->json(['ok' => false, 'error' => $check['error']], $check['status']);
} }
@@ -161,7 +160,7 @@ class UserKeywordController extends Controller
public function import(Request $request): JsonResponse public function import(Request $request): JsonResponse
{ {
$check = $this->ensureUser($request); $check = $this->ensureUser($request);
if (!isset($check['user'])) { if (! isset($check['user'])) {
return response()->json(['ok' => false, 'error' => $check['error']], $check['status']); return response()->json(['ok' => false, 'error' => $check['error']], $check['status']);
} }
@@ -184,8 +183,9 @@ class UserKeywordController extends Controller
->first(); ->first();
$targetActive = filter_var($row['is_active'] ?? true, FILTER_VALIDATE_BOOL); $targetActive = filter_var($row['is_active'] ?? true, FILTER_VALIDATE_BOOL);
if (!$existing && $targetActive && $limit !== null && $activeCount >= $limit) { if (! $existing && $targetActive && $limit !== null && $activeCount >= $limit) {
$skipped += 1; $skipped += 1;
continue; continue;
} }
@@ -214,7 +214,7 @@ class UserKeywordController extends Controller
public function toggleActive(Request $request, int $id): JsonResponse public function toggleActive(Request $request, int $id): JsonResponse
{ {
$check = $this->ensureUser($request); $check = $this->ensureUser($request);
if (!isset($check['user'])) { if (! isset($check['user'])) {
return response()->json(['ok' => false, 'error' => $check['error']], $check['status']); return response()->json(['ok' => false, 'error' => $check['error']], $check['status']);
} }
@@ -224,13 +224,13 @@ class UserKeywordController extends Controller
$user = $check['user']; $user = $check['user'];
$item = UserKeyword::where('user_id', $user->id)->where('id', $id)->first(); $item = UserKeyword::where('user_id', $user->id)->where('id', $id)->first();
if (!$item) { if (! $item) {
return response()->json(['ok' => false, 'error' => 'not_found'], 404); return response()->json(['ok' => false, 'error' => 'not_found'], 404);
} }
if ((bool) $data['is_active'] && ($limit = $this->keywordLimitFor($user))) { if ((bool) $data['is_active'] && ($limit = $this->keywordLimitFor($user))) {
$activeCount = $this->keywordQuota->activeCount((int) $user->id); $activeCount = $this->keywordQuota->activeCount((int) $user->id);
if (!$item->is_active && $activeCount >= $limit) { if (! $item->is_active && $activeCount >= $limit) {
return response()->json(['ok' => false, 'error' => 'free_active_limit_reached', 'limit' => $limit], 403); return response()->json(['ok' => false, 'error' => 'free_active_limit_reached', 'limit' => $limit], 403);
} }
} }

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

@@ -12,7 +12,7 @@ class BillingPaymentController extends Controller
public function resume(Request $request, Payment $payment): JsonResponse public function resume(Request $request, Payment $payment): JsonResponse
{ {
$user = $request->user(); $user = $request->user();
if (!$user || (int) $payment->user_id !== (int) $user->id) { if (! $user || (int) $payment->user_id !== (int) $user->id) {
abort(403); abort(403);
} }
@@ -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),
@@ -34,7 +35,7 @@ class BillingPaymentController extends Controller
$links = is_array($raw['links'] ?? null) ? $raw['links'] : []; $links = is_array($raw['links'] ?? null) ? $raw['links'] : [];
$approveUrl = null; $approveUrl = null;
foreach ($links as $link) { foreach ($links as $link) {
if (!is_array($link)) { if (! is_array($link)) {
continue; continue;
} }
if ((string) ($link['rel'] ?? '') === 'approve') { if ((string) ($link['rel'] ?? '') === 'approve') {

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
{ {
@@ -32,7 +31,7 @@ class PakasirController extends Controller
]); ]);
$user = $request->user(); $user = $request->user();
if (!$user) { if (! $user) {
return response()->json(['error' => 'auth_required'], 401); return response()->json(['error' => 'auth_required'], 401);
} }
@@ -51,7 +50,7 @@ class PakasirController extends Controller
$project = (string) ($config['project'] ?? ''); $project = (string) ($config['project'] ?? '');
$timeout = (int) ($config['timeout'] ?? 10); $timeout = (int) ($config['timeout'] ?? 10);
if (!$enabled || $apiBase === '' || $apiKey === '' || $project === '') { if (! $enabled || $apiBase === '' || $apiKey === '' || $project === '') {
return response()->json(['error' => 'pakasir_not_configured'], 422); return response()->json(['error' => 'pakasir_not_configured'], 422);
} }
@@ -84,25 +83,27 @@ class PakasirController extends Controller
$endpoint = $apiBase.'/api/transactioncreate/qris'; $endpoint = $apiBase.'/api/transactioncreate/qris';
// Pakasir expects form payloads; keep JSON as fallback for provider-side variations. // Pakasir expects form payloads; keep JSON as fallback for provider-side variations.
$res = Http::asForm()->timeout($timeout)->post($endpoint, $payload); $res = Http::asForm()->timeout($timeout)->post($endpoint, $payload);
if (!$res->successful()) { if (! $res->successful()) {
$res = Http::timeout($timeout)->post($endpoint, $payload); $res = Http::timeout($timeout)->post($endpoint, $payload);
} }
if (!$res->successful()) { if (! $res->successful()) {
Log::warning('Pakasir create transaction failed', [ Log::warning('Pakasir create transaction failed', [
'status' => $res->status(), 'status' => $res->status(),
'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);
} }
$body = $res->json(); $body = $res->json();
if (!is_array($body)) { if (! is_array($body)) {
Log::warning('Pakasir create transaction invalid response', [ Log::warning('Pakasir create transaction invalid response', [
'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);
} }
@@ -146,7 +148,7 @@ class PakasirController extends Controller
} }
/** /**
* @param array<string,mixed> $body * @param array<string,mixed> $body
* @return array{0:string,1:string,2:string,3:int} * @return array{0:string,1:string,2:string,3:int}
*/ */
private function extractPakasirPayload(array $body, int $amountFallback): array private function extractPakasirPayload(array $body, int $amountFallback): array
@@ -210,7 +212,7 @@ class PakasirController extends Controller
->where('status', 'pending') ->where('status', 'pending')
->orderByDesc('id') ->orderByDesc('id')
->first(); ->first();
if (!$pending || !$pending->created_at) { if (! $pending || ! $pending->created_at) {
return null; return null;
} }
@@ -232,7 +234,7 @@ class PakasirController extends Controller
public function cancelPending(Request $request): JsonResponse public function cancelPending(Request $request): JsonResponse
{ {
$user = $request->user(); $user = $request->user();
if (!$user) { if (! $user) {
return response()->json(['error' => 'auth_required'], 401); return response()->json(['error' => 'auth_required'], 401);
} }
@@ -247,7 +249,7 @@ class PakasirController extends Controller
} }
$order = $orderQuery->orderByDesc('id')->first(); $order = $orderQuery->orderByDesc('id')->first();
if (!$order) { if (! $order) {
return response()->json(['ok' => true, 'canceled' => false, 'cancelled' => false]); return response()->json(['ok' => true, 'canceled' => false, 'cancelled' => false]);
} }
@@ -264,7 +266,7 @@ class PakasirController extends Controller
public function paymentStatus(Request $request): JsonResponse public function paymentStatus(Request $request): JsonResponse
{ {
$user = $request->user(); $user = $request->user();
if (!$user) { if (! $user) {
return response()->json(['error' => 'auth_required'], 401); return response()->json(['error' => 'auth_required'], 401);
} }
@@ -275,7 +277,7 @@ class PakasirController extends Controller
} }
$order = $query->orderByDesc('id')->first(); $order = $query->orderByDesc('id')->first();
if (!$order) { if (! $order) {
return response()->json(['ok' => true, 'found' => false, 'status' => null, 'paid' => false]); return response()->json(['ok' => true, 'found' => false, 'status' => null, 'paid' => false]);
} }
@@ -319,11 +321,12 @@ class PakasirController extends Controller
} }
$status = strtolower((string) ($data['status'] ?? $payload['status'] ?? '')); $status = strtolower((string) ($data['status'] ?? $payload['status'] ?? ''));
if (!in_array($status, ['paid', 'success', 'settlement', 'completed'], true)) { if (! in_array($status, ['paid', 'success', 'settlement', 'completed'], true)) {
Log::info('Pakasir webhook ignored: status not paid', [ Log::info('Pakasir webhook ignored: status not paid', [
'status' => $status, 'status' => $status,
'payload_status' => $payload['status'] ?? null, 'payload_status' => $payload['status'] ?? null,
]); ]);
return; return;
} }
@@ -338,17 +341,19 @@ 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;
} }
$order = Order::where('provider', 'pakasir')->where('provider_ref', $orderId)->first(); $order = Order::where('provider', 'pakasir')->where('provider_ref', $orderId)->first();
if (!$order) { if (! $order) {
if (preg_match('/^DW-(\d+)-/', $orderId, $matches) === 1) { if (preg_match('/^DW-(\d+)-/', $orderId, $matches) === 1) {
$order = Order::where('provider', 'pakasir')->where('id', (int) $matches[1])->first(); $order = Order::where('provider', 'pakasir')->where('id', (int) $matches[1])->first();
} }
} }
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;
} }
@@ -356,7 +361,7 @@ class PakasirController extends Controller
Payment::where('order_id', $order->id)->update(['status' => 'paid']); Payment::where('order_id', $order->id)->update(['status' => 'paid']);
$user = User::find($order->user_id); $user = User::find($order->user_id);
if (!$user) { if (! $user) {
return; return;
} }
@@ -400,7 +405,7 @@ class PakasirController extends Controller
$defaults = collect(config('dewemoji.pricing.defaults', []))->keyBy('code'); $defaults = collect(config('dewemoji.pricing.defaults', []))->keyBy('code');
$fallback = $defaults->get($planCode); $fallback = $defaults->get($planCode);
if (!$fallback) { if (! $fallback) {
return 0; return 0;
} }

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
{ {
@@ -35,7 +33,7 @@ class PayPalController extends Controller
]); ]);
$user = $request->user(); $user = $request->user();
if (!$user) { if (! $user) {
return response()->json(['error' => 'auth_required'], 401); return response()->json(['error' => 'auth_required'], 401);
} }
@@ -48,7 +46,7 @@ class PayPalController extends Controller
} }
$mode = $this->resolvePaypalMode($this->billingMode()); $mode = $this->resolvePaypalMode($this->billingMode());
if (!$this->paypalConfigured($mode)) { if (! $this->paypalConfigured($mode)) {
return response()->json(['error' => 'paypal_not_configured'], 422); return response()->json(['error' => 'paypal_not_configured'], 422);
} }
@@ -57,12 +55,12 @@ class PayPalController extends Controller
} }
$planId = $this->resolvePlanId($data['plan_code'], $mode); $planId = $this->resolvePlanId($data['plan_code'], $mode);
if (!$planId) { if (! $planId) {
return response()->json(['error' => 'paypal_plan_missing'], 422); return response()->json(['error' => 'paypal_plan_missing'], 422);
} }
$token = $this->getAccessToken($mode); $token = $this->getAccessToken($mode);
if (!$token) { if (! $token) {
return response()->json(['error' => 'paypal_auth_failed'], 502); return response()->json(['error' => 'paypal_auth_failed'], 502);
} }
@@ -94,8 +92,8 @@ class PayPalController extends Controller
$subscriptionId = $body['id'] ?? null; $subscriptionId = $body['id'] ?? null;
$approveUrl = collect($body['links'] ?? [])->firstWhere('rel', 'approve')['href'] ?? null; $approveUrl = collect($body['links'] ?? [])->firstWhere('rel', 'approve')['href'] ?? null;
if (!$subscriptionId || !$approveUrl) { if (! $subscriptionId || ! $approveUrl) {
if (!$res->ok()) { if (! $res->ok()) {
Log::warning('PayPal create subscription failed', [ Log::warning('PayPal create subscription failed', [
'status' => $res->status(), 'status' => $res->status(),
'body' => $res->body(), 'body' => $res->body(),
@@ -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);
} }
@@ -156,13 +155,14 @@ class PayPalController extends Controller
$token = trim((string) $request->query('token', '')); $token = trim((string) $request->query('token', ''));
if ($token !== '') { if ($token !== '') {
$captured = $this->captureLifetimeOrder($token, $request->user()?->id); $captured = $this->captureLifetimeOrder($token, $request->user()?->id);
if (!$captured) { if (! $captured) {
return redirect()->route('dashboard.billing', ['status' => 'error']); return redirect()->route('dashboard.billing', ['status' => 'error']);
} }
} }
} }
$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]);
} }
@@ -177,7 +177,7 @@ class PayPalController extends Controller
if ($webhookId) { if ($webhookId) {
$verified = $this->verifySignature($mode, $webhookId, $payload, $request); $verified = $this->verifySignature($mode, $webhookId, $payload, $request);
if (!$verified) { if (! $verified) {
return response()->json(['error' => 'invalid_signature'], 401); return response()->json(['error' => 'invalid_signature'], 401);
} }
} }
@@ -223,14 +223,14 @@ class PayPalController extends Controller
'provider' => 'paypal', 'provider' => 'paypal',
'provider_ref' => $subscriptionId, 'provider_ref' => $subscriptionId,
]); ]);
if (!$sub->user_id) { if (! $sub->user_id) {
$order = Order::where('provider', 'paypal')->where('provider_ref', $subscriptionId)->first(); $order = Order::where('provider', 'paypal')->where('provider_ref', $subscriptionId)->first();
if ($order) { if ($order) {
$sub->user_id = $order->user_id; $sub->user_id = $order->user_id;
$resolvedPlan = (string) ($order->plan_code ?: $resolvedPlan); $resolvedPlan = (string) ($order->plan_code ?: $resolvedPlan);
} }
} }
if (!empty($sub->plan)) { if (! empty($sub->plan)) {
$resolvedPlan = (string) $sub->plan; $resolvedPlan = (string) $sub->plan;
} }
$sub->plan = $resolvedPlan; $sub->plan = $resolvedPlan;
@@ -267,6 +267,7 @@ class PayPalController extends Controller
$sub->canceled_at = now(); $sub->canceled_at = now();
$sub->save(); $sub->save();
} }
return true; return true;
} }
@@ -311,7 +312,7 @@ class PayPalController extends Controller
->where('status', 'pending') ->where('status', 'pending')
->orderByDesc('id') ->orderByDesc('id')
->first(); ->first();
if (!$pending || !$pending->created_at) { if (! $pending || ! $pending->created_at) {
return null; return null;
} }
@@ -333,20 +334,21 @@ class PayPalController extends Controller
private function resolvePlanAmountUsd(string $planCode): int private function resolvePlanAmountUsd(string $planCode): int
{ {
$plan = PricingPlan::where('code', $planCode)->first(); $plan = PricingPlan::where('code', $planCode)->first();
if (!$plan) { if (! $plan) {
return 0; return 0;
} }
$rate = (int) config('dewemoji.pricing.usd_rate', 15000); $rate = (int) config('dewemoji.pricing.usd_rate', 15000);
if ($rate <= 0) { if ($rate <= 0) {
return 0; return 0;
} }
return (int) round($plan->amount / $rate); return (int) round($plan->amount / $rate);
} }
private function resolvePlanAmountUsdValue(string $planCode): string private function resolvePlanAmountUsdValue(string $planCode): string
{ {
$plan = PricingPlan::where('code', $planCode)->first(); $plan = PricingPlan::where('code', $planCode)->first();
if (!$plan) { if (! $plan) {
return '0.00'; return '0.00';
} }
$rate = (int) config('dewemoji.pricing.usd_rate', 15000); $rate = (int) config('dewemoji.pricing.usd_rate', 15000);
@@ -363,7 +365,7 @@ class PayPalController extends Controller
$clientSecret = config("dewemoji.billing.providers.paypal.{$mode}.client_secret"); $clientSecret = config("dewemoji.billing.providers.paypal.{$mode}.client_secret");
$apiBase = config("dewemoji.billing.providers.paypal.{$mode}.api_base"); $apiBase = config("dewemoji.billing.providers.paypal.{$mode}.api_base");
if (!$clientId || !$clientSecret || !$apiBase) { if (! $clientId || ! $clientSecret || ! $apiBase) {
return null; return null;
} }
@@ -374,7 +376,7 @@ class PayPalController extends Controller
'grant_type' => 'client_credentials', 'grant_type' => 'client_credentials',
]); ]);
if (!$res->successful()) { if (! $res->successful()) {
return null; return null;
} }
@@ -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');
} }
@@ -414,7 +417,7 @@ class PayPalController extends Controller
private function verifySignature(string $mode, string $webhookId, array $payload, Request $request): bool private function verifySignature(string $mode, string $webhookId, array $payload, Request $request): bool
{ {
$token = $this->getAccessToken($mode); $token = $this->getAccessToken($mode);
if (!$token) { if (! $token) {
return false; return false;
} }
@@ -443,12 +446,12 @@ class PayPalController extends Controller
private function createLifetimeOrder(Request $request, string $mode): JsonResponse private function createLifetimeOrder(Request $request, string $mode): JsonResponse
{ {
$user = $request->user(); $user = $request->user();
if (!$user) { if (! $user) {
return response()->json(['error' => 'auth_required'], 401); return response()->json(['error' => 'auth_required'], 401);
} }
$token = $this->getAccessToken($mode); $token = $this->getAccessToken($mode);
if (!$token) { if (! $token) {
return response()->json(['error' => 'paypal_auth_failed'], 502); return response()->json(['error' => 'paypal_auth_failed'], 502);
} }
@@ -487,7 +490,7 @@ class PayPalController extends Controller
$body = $res->json(); $body = $res->json();
$orderId = (string) ($body['id'] ?? ''); $orderId = (string) ($body['id'] ?? '');
$approveUrl = collect($body['links'] ?? [])->firstWhere('rel', 'approve')['href'] ?? null; $approveUrl = collect($body['links'] ?? [])->firstWhere('rel', 'approve')['href'] ?? null;
if ($orderId === '' || !$approveUrl) { if ($orderId === '' || ! $approveUrl) {
Log::warning('PayPal create lifetime order failed', [ Log::warning('PayPal create lifetime order failed', [
'status' => $res->status(), 'status' => $res->status(),
'body' => $res->body(), 'body' => $res->body(),
@@ -529,7 +532,7 @@ class PayPalController extends Controller
{ {
$mode = $this->resolvePaypalMode($this->billingMode()); $mode = $this->resolvePaypalMode($this->billingMode());
$token = $this->getAccessToken($mode); $token = $this->getAccessToken($mode);
if (!$token) { if (! $token) {
return false; return false;
} }
@@ -537,7 +540,7 @@ class PayPalController extends Controller
->where('provider_ref', $orderId) ->where('provider_ref', $orderId)
->where('type', 'one_time') ->where('type', 'one_time')
->first(); ->first();
if (!$order) { if (! $order) {
return false; return false;
} }
if ($userId !== null && (int) $order->user_id !== $userId) { if ($userId !== null && (int) $order->user_id !== $userId) {
@@ -581,7 +584,7 @@ class PayPalController extends Controller
->where('provider_ref', $orderId) ->where('provider_ref', $orderId)
->where('type', 'one_time') ->where('type', 'one_time')
->first(); ->first();
if (!$order) { if (! $order) {
return false; return false;
} }
@@ -594,7 +597,7 @@ class PayPalController extends Controller
->first(); ->first();
if ($payment) { if ($payment) {
$payment->status = 'paid'; $payment->status = 'paid';
if (!empty($rawPayload)) { if (! empty($rawPayload)) {
$payment->raw_payload = $rawPayload; $payment->raw_payload = $rawPayload;
} }
$payment->save(); $payment->save();

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
{ {
@@ -206,7 +204,7 @@ class AdminDashboardController extends Controller
]); ]);
$user = $this->resolveUser($data['user_id'] ?? null, $data['email'] ?? null); $user = $this->resolveUser($data['user_id'] ?? null, $data['email'] ?? null);
if (!$user) { if (! $user) {
return back()->withErrors(['user' => 'User not found.']); return back()->withErrors(['user' => 'User not found.']);
} }
@@ -246,9 +244,9 @@ class AdminDashboardController extends Controller
'email' => 'nullable|email|max:255', 'email' => 'nullable|email|max:255',
]); ]);
if (!empty($data['subscription_id'])) { if (! empty($data['subscription_id'])) {
$sub = Subscription::find($data['subscription_id']); $sub = Subscription::find($data['subscription_id']);
if (!$sub) { if (! $sub) {
return back()->withErrors(['subscription' => 'Subscription not found.']); return back()->withErrors(['subscription' => 'Subscription not found.']);
} }
$sub->update(['status' => 'revoked', 'expires_at' => now()]); $sub->update(['status' => 'revoked', 'expires_at' => now()]);
@@ -257,11 +255,12 @@ 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.');
} }
$user = $this->resolveUser($data['user_id'] ?? null, $data['email'] ?? null); $user = $this->resolveUser($data['user_id'] ?? null, $data['email'] ?? null);
if (!$user) { if (! $user) {
return back()->withErrors(['user' => 'User not found.']); return back()->withErrors(['user' => 'User not found.']);
} }
@@ -447,7 +446,7 @@ class AdminDashboardController extends Controller
public function replayWebhook(int $id): RedirectResponse public function replayWebhook(int $id): RedirectResponse
{ {
$event = WebhookEvent::find($id); $event = WebhookEvent::find($id);
if (!$event) { if (! $event) {
return back()->withErrors(['webhook' => 'Webhook not found.']); return back()->withErrors(['webhook' => 'Webhook not found.']);
} }
@@ -557,7 +556,7 @@ class AdminDashboardController extends Controller
'tier' => $active ? 'personal' : 'free', 'tier' => $active ? 'personal' : 'free',
]); ]);
$this->keywordQuota->enforceForUser($userId, $active ? 'personal' : 'free'); $this->keywordQuota->enforceForUser($userId, $active ? 'personal' : 'free');
if (!$active) { if (! $active) {
UserApiKey::where('user_id', $userId)->update(['revoked_at' => now()]); UserApiKey::where('user_id', $userId)->update(['revoked_at' => now()]);
} }
} }
@@ -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');
@@ -676,7 +675,7 @@ class AdminDashboardController extends Controller
private function logAdminAction(string $action, array $payload = []): void private function logAdminAction(string $action, array $payload = []): void
{ {
if (!Schema::hasTable('admin_audit_logs')) { if (! Schema::hasTable('admin_audit_logs')) {
return; return;
} }
$user = auth()->user(); $user = auth()->user();
@@ -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.');
} }
@@ -172,12 +177,12 @@ class AdminEmojiCatalogController extends Controller
} }
/** /**
* @param array<string,mixed> $payload * @param array<string,mixed> $payload
*/ */
private function logAdminAction(Request $request, string $action, array $payload): void private function logAdminAction(Request $request, string $action, array $payload): void
{ {
$admin = $request->user(); $admin = $request->user();
if (!$admin) { if (! $admin) {
return; return;
} }

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
{ {
@@ -171,7 +170,7 @@ class UserDashboardController extends Controller
public function storeKeyword(Request $request): RedirectResponse|JsonResponse public function storeKeyword(Request $request): RedirectResponse|JsonResponse
{ {
$user = $request->user(); $user = $request->user();
if (!$user) { if (! $user) {
abort(403); abort(403);
} }
@@ -188,7 +187,7 @@ class UserDashboardController extends Controller
$targetActive = $existing ? (bool) $existing->is_active : true; $targetActive = $existing ? (bool) $existing->is_active : true;
if ($limit = $this->keywordLimitFor($user)) { if ($limit = $this->keywordLimitFor($user)) {
if (!$existing && $targetActive) { if (! $existing && $targetActive) {
$activeCount = $this->keywordQuota->activeCount((int) $user->id); $activeCount = $this->keywordQuota->activeCount((int) $user->id);
if ($activeCount >= $limit) { if ($activeCount >= $limit) {
return $this->rejectKeywordLimit($request, $limit, 'free_active_limit_reached'); return $this->rejectKeywordLimit($request, $limit, 'free_active_limit_reached');
@@ -221,7 +220,7 @@ class UserDashboardController extends Controller
public function updateKeyword(Request $request, UserKeyword $keyword): RedirectResponse|JsonResponse public function updateKeyword(Request $request, UserKeyword $keyword): RedirectResponse|JsonResponse
{ {
$user = $request->user(); $user = $request->user();
if (!$user) { if (! $user) {
abort(403); abort(403);
} }
@@ -261,7 +260,7 @@ class UserDashboardController extends Controller
public function deleteKeyword(Request $request, UserKeyword $keyword): RedirectResponse|JsonResponse public function deleteKeyword(Request $request, UserKeyword $keyword): RedirectResponse|JsonResponse
{ {
$user = $request->user(); $user = $request->user();
if (!$user) { if (! $user) {
abort(403); abort(403);
} }
@@ -281,7 +280,7 @@ class UserDashboardController extends Controller
public function importKeywords(Request $request): RedirectResponse public function importKeywords(Request $request): RedirectResponse
{ {
$user = $request->user(); $user = $request->user();
if (!$user) { if (! $user) {
abort(403); abort(403);
} }
@@ -291,7 +290,7 @@ class UserDashboardController extends Controller
} }
$items = json_decode((string) $payload, true); $items = json_decode((string) $payload, true);
if (!is_array($items)) { if (! is_array($items)) {
return back()->withErrors(['payload' => 'Invalid JSON payload.']); return back()->withErrors(['payload' => 'Invalid JSON payload.']);
} }
@@ -300,7 +299,7 @@ class UserDashboardController extends Controller
$limit = $this->keywordLimitFor($user); $limit = $this->keywordLimitFor($user);
$activeCount = $this->keywordQuota->activeCount((int) $user->id); $activeCount = $this->keywordQuota->activeCount((int) $user->id);
foreach ($items as $row) { foreach ($items as $row) {
if (!is_array($row)) { if (! is_array($row)) {
continue; continue;
} }
$emojiSlug = trim((string) ($row['emoji_slug'] ?? '')); $emojiSlug = trim((string) ($row['emoji_slug'] ?? ''));
@@ -315,8 +314,9 @@ class UserDashboardController extends Controller
->first(); ->first();
$targetActive = filter_var($row['is_active'] ?? true, FILTER_VALIDATE_BOOL); $targetActive = filter_var($row['is_active'] ?? true, FILTER_VALIDATE_BOOL);
if (!$existing && $targetActive && $limit !== null && $activeCount >= $limit) { if (! $existing && $targetActive && $limit !== null && $activeCount >= $limit) {
$skipped += 1; $skipped += 1;
continue; continue;
} }
@@ -350,7 +350,7 @@ class UserDashboardController extends Controller
public function exportKeywords(Request $request): BinaryFileResponse public function exportKeywords(Request $request): BinaryFileResponse
{ {
$user = $request->user(); $user = $request->user();
if (!$user) { if (! $user) {
abort(403); abort(403);
} }
@@ -386,7 +386,7 @@ class UserDashboardController extends Controller
public function createApiKey(Request $request): RedirectResponse public function createApiKey(Request $request): RedirectResponse
{ {
$user = $request->user(); $user = $request->user();
if (!$user) { if (! $user) {
abort(403); abort(403);
} }
if ((string) $user->tier !== 'personal') { if ((string) $user->tier !== 'personal') {
@@ -405,7 +405,7 @@ class UserDashboardController extends Controller
public function revokeApiKey(Request $request, UserApiKey $key): RedirectResponse public function revokeApiKey(Request $request, UserApiKey $key): RedirectResponse
{ {
$user = $request->user(); $user = $request->user();
if (!$user || $key->user_id !== $user->id) { if (! $user || $key->user_id !== $user->id) {
abort(403); abort(403);
} }
@@ -447,7 +447,7 @@ class UserDashboardController extends Controller
public function toggleKeywordActive(Request $request, UserKeyword $keyword): RedirectResponse|JsonResponse public function toggleKeywordActive(Request $request, UserKeyword $keyword): RedirectResponse|JsonResponse
{ {
$user = $request->user(); $user = $request->user();
if (!$user || $keyword->user_id !== $user->id) { if (! $user || $keyword->user_id !== $user->id) {
abort(403); abort(403);
} }
@@ -458,7 +458,7 @@ class UserDashboardController extends Controller
$target = (bool) $data['is_active']; $target = (bool) $data['is_active'];
if ($target && ($limit = $this->keywordLimitFor($user))) { if ($target && ($limit = $this->keywordLimitFor($user))) {
$activeCount = $this->keywordQuota->activeCount((int) $user->id); $activeCount = $this->keywordQuota->activeCount((int) $user->id);
if (!$keyword->is_active && $activeCount >= $limit) { if (! $keyword->is_active && $activeCount >= $limit) {
return $this->rejectKeywordLimit($request, $limit, 'free_active_limit_reached'); return $this->rejectKeywordLimit($request, $limit, 'free_active_limit_reached');
} }
} }
@@ -483,7 +483,7 @@ class UserDashboardController extends Controller
private function keywordLimitFor(?User $user): ?int private function keywordLimitFor(?User $user): ?int
{ {
if (!$user) { if (! $user) {
return null; return null;
} }
if ((string) $user->tier === 'personal') { if ((string) $user->tier === 'personal') {

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

@@ -18,15 +18,15 @@ class SiteController extends Controller
{ {
/** @var array<string,string> */ /** @var array<string,string> */
private const CATEGORY_TO_SLUG = [ private const CATEGORY_TO_SLUG = [
"Smileys & Emotion" => "smileys", 'Smileys & Emotion' => 'smileys',
"People & Body" => "people", 'People & Body' => 'people',
"Animals & Nature" => "animals", 'Animals & Nature' => 'animals',
"Food & Drink" => "food", 'Food & Drink' => 'food',
"Travel & Places" => "travel", 'Travel & Places' => 'travel',
"Activities" => "activities", 'Activities' => 'activities',
"Objects" => "objects", 'Objects' => 'objects',
"Symbols" => "symbols", 'Symbols' => 'symbols',
"Flags" => "flags", 'Flags' => 'flags',
]; ];
private function billingMode(): string private function billingMode(): string
@@ -34,15 +34,15 @@ class SiteController extends Controller
$settings = app(SettingsService::class); $settings = app(SettingsService::class);
$preferred = $preferred =
(string) ($settings->get( (string) ($settings->get(
"billing_mode", 'billing_mode',
config("dewemoji.billing.mode", "sandbox"), config('dewemoji.billing.mode', 'sandbox'),
) ?: ) ?:
"sandbox"); 'sandbox');
if ($this->paypalConfiguredMode($preferred)) { if ($this->paypalConfiguredMode($preferred)) {
return $preferred; return $preferred;
} }
$fallback = $preferred === "live" ? "sandbox" : "live"; $fallback = $preferred === 'live' ? 'sandbox' : 'live';
if ($this->paypalConfiguredMode($fallback)) { if ($this->paypalConfiguredMode($fallback)) {
return $fallback; return $fallback;
} }
@@ -52,60 +52,60 @@ class SiteController extends Controller
public function home(Request $request): View public function home(Request $request): View
{ {
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( 'initialSubcategory' => trim(
(string) $request->query("subcategory", ""), (string) $request->query('subcategory', ''),
), ),
"canonicalPath" => "/", 'canonicalPath' => '/',
"userTier" => $request->user()?->tier, 'userTier' => $request->user()?->tier,
]); ]);
} }
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 ( if (
$cat !== "" && $cat !== '' &&
$cat !== "all" && $cat !== 'all' &&
array_key_exists($cat, $this->categorySlugMap()) 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( 'initialSubcategory' => trim(
(string) $request->query("subcategory", ""), (string) $request->query('subcategory', ''),
), ),
"canonicalPath" => "/browse", 'canonicalPath' => '/browse',
"userTier" => $request->user()?->tier, 'userTier' => $request->user()?->tier,
]); ]);
} }
public function category(string $categorySlug): View public function category(string $categorySlug): View
{ {
if ($categorySlug === "all") { if ($categorySlug === 'all') {
return view("site.home", [ return view('site.home', [
"initialQuery" => "", 'initialQuery' => '',
"initialCategory" => "", 'initialCategory' => '',
"initialSubcategory" => "", 'initialSubcategory' => '',
"canonicalPath" => "/", 'canonicalPath' => '/',
"userTier" => request()->user()?->tier, 'userTier' => request()->user()?->tier,
]); ]);
} }
$categoryLabel = $this->categorySlugMap()[$categorySlug] ?? ""; $categoryLabel = $this->categorySlugMap()[$categorySlug] ?? '';
abort_if($categoryLabel === "", 404); abort_if($categoryLabel === '', 404);
return view("site.home", [ return view('site.home', [
"initialQuery" => "", 'initialQuery' => '',
"initialCategory" => $categoryLabel, 'initialCategory' => $categoryLabel,
"initialSubcategory" => "", 'initialSubcategory' => '',
"canonicalPath" => "/" . $categorySlug, 'canonicalPath' => '/'.$categorySlug,
"userTier" => request()->user()?->tier, 'userTier' => request()->user()?->tier,
]); ]);
} }
@@ -113,61 +113,62 @@ class SiteController extends Controller
string $categorySlug, string $categorySlug,
string $subcategorySlug, string $subcategorySlug,
): View { ): View {
if ($categorySlug === "all") { if ($categorySlug === 'all') {
abort(404); abort(404);
} }
$categoryLabel = $this->categorySlugMap()[$categorySlug] ?? ""; $categoryLabel = $this->categorySlugMap()[$categorySlug] ?? '';
abort_if($categoryLabel === "", 404); abort_if($categoryLabel === '', 404);
return view("site.home", [ return view('site.home', [
"initialQuery" => "", 'initialQuery' => '',
"initialCategory" => $categoryLabel, 'initialCategory' => $categoryLabel,
"initialSubcategory" => $subcategorySlug, 'initialSubcategory' => $subcategorySlug,
"canonicalPath" => "/" . $categorySlug . "/" . $subcategorySlug, 'canonicalPath' => '/'.$categorySlug.'/'.$subcategorySlug,
"userTier" => request()->user()?->tier, 'userTier' => request()->user()?->tier,
]); ]);
} }
public function apiDocs(): View public function apiDocs(): View
{ {
return view("site.api-docs"); return view('site.api-docs');
} }
public function pricing(): View public function pricing(): View
{ {
$user = request()->user(); $user = request()->user();
$currencyPref = strtoupper((string) session("pricing_currency", "")); $currencyPref = strtoupper((string) session('pricing_currency', ''));
if (!in_array($currencyPref, ["IDR", "USD"], true)) { if (! in_array($currencyPref, ['IDR', 'USD'], true)) {
$currencyPref = $this->detectPricingCurrency(request()); $currencyPref = $this->detectPricingCurrency(request());
session(["pricing_currency" => $currencyPref]); session(['pricing_currency' => $currencyPref]);
} }
$rate = (int) config("dewemoji.pricing.usd_rate", 15000); $rate = (int) config('dewemoji.pricing.usd_rate', 15000);
$plans = PricingPlan::where("status", "active")->get()->keyBy("code"); $plans = PricingPlan::where('status', 'active')->get()->keyBy('code');
$defaults = config("dewemoji.pricing.defaults", []); $defaults = config('dewemoji.pricing.defaults', []);
$fallback = collect($defaults)->keyBy("code"); $fallback = collect($defaults)->keyBy('code');
$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 = [
"personal_monthly" => [ 'personal_monthly' => [
"idr" => $getPlanAmount("personal_monthly"), 'idr' => $getPlanAmount('personal_monthly'),
], ],
"personal_annual" => [ 'personal_annual' => [
"idr" => $getPlanAmount("personal_annual"), 'idr' => $getPlanAmount('personal_annual'),
], ],
"personal_lifetime" => [ 'personal_lifetime' => [
"idr" => $getPlanAmount("personal_lifetime"), 'idr' => $getPlanAmount('personal_lifetime'),
], ],
]; ];
foreach ($pricing as $key => $row) { foreach ($pricing as $key => $row) {
$pricing[$key]["usd"] = $pricing[$key]['usd'] =
$rate > 0 ? round($row["idr"] / $rate, 2) : 0; $rate > 0 ? round($row['idr'] / $rate, 2) : 0;
} }
$hasActiveLifetime = false; $hasActiveLifetime = false;
@@ -175,28 +176,28 @@ class SiteController extends Controller
$pendingCooldownRemaining = 0; $pendingCooldownRemaining = 0;
if ($user) { if ($user) {
$hasActiveLifetime = Subscription::query() $hasActiveLifetime = Subscription::query()
->where("user_id", $user->id) ->where('user_id', $user->id)
->where("plan", "personal_lifetime") ->where('plan', 'personal_lifetime')
->where("status", "active") ->where('status', 'active')
->where(function ($query) { ->where(function ($query) {
$query $query
->whereNull("expires_at") ->whereNull('expires_at')
->orWhere("expires_at", ">", now()); ->orWhere('expires_at', '>', now());
}) })
->exists(); ->exists();
$hasPendingPayment = Payment::query() $hasPendingPayment = Payment::query()
->where("user_id", $user->id) ->where('user_id', $user->id)
->where("status", "pending") ->where('status', 'pending')
->exists(); ->exists();
$cooldown = (int) config( $cooldown = (int) config(
"dewemoji.billing.pending_cooldown_seconds", 'dewemoji.billing.pending_cooldown_seconds',
120, 120,
); );
if ($cooldown > 0) { if ($cooldown > 0) {
$latestPending = Payment::query() $latestPending = Payment::query()
->where("user_id", $user->id) ->where('user_id', $user->id)
->where("status", "pending") ->where('status', 'pending')
->orderByDesc("id") ->orderByDesc('id')
->first(); ->first();
if ($latestPending && $latestPending->created_at) { if ($latestPending && $latestPending->created_at) {
$age = max( $age = max(
@@ -209,41 +210,40 @@ class SiteController extends Controller
} }
} }
return view("site.pricing", [ return view('site.pricing', [
"currencyPref" => $currencyPref, 'currencyPref' => $currencyPref,
"usdRate" => $rate, 'usdRate' => $rate,
"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( 'paypal_url' => (string) config(
"dewemoji.payments.paypal_url", 'dewemoji.payments.paypal_url',
"", '',
), ),
], ],
"pakasirEnabled" => 'pakasirEnabled' => (bool) config(
(bool) config( 'dewemoji.billing.providers.pakasir.enabled',
"dewemoji.billing.providers.pakasir.enabled", false,
false, ) &&
) &&
(string) config( (string) config(
"dewemoji.billing.providers.pakasir.api_base", 'dewemoji.billing.providers.pakasir.api_base',
"", '',
) !== "" && ) !== '' &&
(string) config( (string) config(
"dewemoji.billing.providers.pakasir.api_key", 'dewemoji.billing.providers.pakasir.api_key',
"", '',
) !== "" && ) !== '' &&
(string) config( (string) config(
"dewemoji.billing.providers.pakasir.project", 'dewemoji.billing.providers.pakasir.project',
"", '',
) !== "", ) !== '',
"paypalEnabled" => $this->paypalEnabled($this->billingMode()), 'paypalEnabled' => $this->paypalEnabled($this->billingMode()),
"paypalPlans" => $this->paypalPlanAvailability( 'paypalPlans' => $this->paypalPlanAvailability(
$this->billingMode(), $this->billingMode(),
), ),
"hasActiveLifetime" => $hasActiveLifetime, 'hasActiveLifetime' => $hasActiveLifetime,
"hasPendingPayment" => $hasPendingPayment, 'hasPendingPayment' => $hasPendingPayment,
"pendingCooldownRemaining" => $pendingCooldownRemaining, 'pendingCooldownRemaining' => $pendingCooldownRemaining,
]); ]);
} }
@@ -253,141 +253,142 @@ class SiteController extends Controller
return true; return true;
} }
$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( $enabled = (bool) config(
"dewemoji.billing.providers.paypal.enabled", 'dewemoji.billing.providers.paypal.enabled',
false, false,
); );
$clientId = (string) config( $clientId = (string) config(
"dewemoji.billing.providers.paypal.{$mode}.client_id", "dewemoji.billing.providers.paypal.{$mode}.client_id",
"", '',
); );
$clientSecret = (string) config( $clientSecret = (string) config(
"dewemoji.billing.providers.paypal.{$mode}.client_secret", "dewemoji.billing.providers.paypal.{$mode}.client_secret",
"", '',
); );
$apiBase = (string) config( $apiBase = (string) config(
"dewemoji.billing.providers.paypal.{$mode}.api_base", "dewemoji.billing.providers.paypal.{$mode}.api_base",
"", '',
); );
return $enabled && return $enabled &&
$clientId !== "" && $clientId !== '' &&
$clientSecret !== "" && $clientSecret !== '' &&
$apiBase !== ""; $apiBase !== '';
} }
private function paypalPlanAvailability(string $mode): array private function paypalPlanAvailability(string $mode): array
{ {
$plans = PricingPlan::whereIn("code", [ $plans = PricingPlan::whereIn('code', [
"personal_monthly", 'personal_monthly',
"personal_annual", 'personal_annual',
]) ])
->get() ->get()
->keyBy("code"); ->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);
if (!$plan) { if (! $plan) {
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( return (string) config(
"dewemoji.billing.providers.paypal.plan_ids.{$mode}.{$code}", "dewemoji.billing.providers.paypal.plan_ids.{$mode}.{$code}",
"", '',
) !== ""; ) !== '';
}; };
return [ return [
"personal_monthly" => 'personal_monthly' => $fromDb('personal_monthly') || $fromEnv('personal_monthly'),
$fromDb("personal_monthly") || $fromEnv("personal_monthly"), 'personal_annual' => $fromDb('personal_annual') || $fromEnv('personal_annual'),
"personal_annual" =>
$fromDb("personal_annual") || $fromEnv("personal_annual"),
]; ];
} }
public function setPricingCurrency(Request $request): RedirectResponse public function setPricingCurrency(Request $request): RedirectResponse
{ {
$data = $request->validate([ $data = $request->validate([
"currency" => "required|string|in:IDR,USD", 'currency' => 'required|string|in:IDR,USD',
]); ]);
session(["pricing_currency" => $data["currency"]]); session(['pricing_currency' => $data['currency']]);
return back(); return back();
} }
public function support(): View public function support(): View
{ {
return view("site.support"); return view('site.support');
} }
public function download(): View public function download(): View
{ {
$downloadBaseUrl = rtrim( $downloadBaseUrl = rtrim(
(string) config("dewemoji.apk_release.public_base_url", ""), (string) config('dewemoji.apk_release.public_base_url', ''),
"/", '/',
); );
$androidEnabled = false; $androidEnabled = false;
return view("site.download", [ return view('site.download', [
"androidEnabled" => $androidEnabled, 'androidEnabled' => $androidEnabled,
"androidVersionJsonUrl" => $androidEnabled 'androidVersionJsonUrl' => $androidEnabled
? $downloadBaseUrl . "/version.json" ? $downloadBaseUrl.'/version.json'
: "", : '',
"androidLatestApkUrl" => $androidEnabled 'androidLatestApkUrl' => $androidEnabled
? $downloadBaseUrl . "/dewemoji-latest.apk" ? $downloadBaseUrl.'/dewemoji-latest.apk'
: "", : '',
]); ]);
} }
public function downloadVersionJson( public function downloadVersionJson(
Request $request, Request $request,
): RedirectResponse|JsonResponse { ): RedirectResponse|JsonResponse {
$target = $this->apkReleaseTargetUrl("version_json"); $target = $this->apkReleaseTargetUrl('version_json');
if ($target === "") { if ($target === '') {
return response()->json( return response()->json(
["ok" => false, "error" => "apk_release_not_configured"], ['ok' => false, 'error' => 'apk_release_not_configured'],
404, 404,
); );
} }
return redirect()->away($target, 302, [ return redirect()->away($target, 302, [
"Cache-Control" => "no-store, no-cache, must-revalidate", 'Cache-Control' => 'no-store, no-cache, must-revalidate',
"Pragma" => "no-cache", 'Pragma' => 'no-cache',
]); ]);
} }
public function downloadLatestApk( public function downloadLatestApk(
Request $request, Request $request,
): RedirectResponse|JsonResponse { ): RedirectResponse|JsonResponse {
$target = $this->apkReleaseTargetUrl("latest_apk"); $target = $this->apkReleaseTargetUrl('latest_apk');
if ($target === "") { if ($target === '') {
return response()->json( return response()->json(
["ok" => false, "error" => "apk_release_not_configured"], ['ok' => false, 'error' => 'apk_release_not_configured'],
404, 404,
); );
} }
return redirect()->away($target, 302, [ return redirect()->away($target, 302, [
"Cache-Control" => "no-store, no-cache, must-revalidate", 'Cache-Control' => 'no-store, no-cache, must-revalidate',
"Pragma" => "no-cache", 'Pragma' => 'no-cache',
]); ]);
} }
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( $rawFingerprints = (array) config(
"dewemoji.apk_release.assetlinks.fingerprints", 'dewemoji.apk_release.assetlinks.fingerprints',
[], [],
); );
$fingerprints = []; $fingerprints = [];
@@ -395,235 +396,235 @@ class SiteController extends Controller
$normalized = $this->normalizeApkCertFingerprint( $normalized = $this->normalizeApkCertFingerprint(
(string) $fingerprint, (string) $fingerprint,
); );
if ($normalized !== "") { if ($normalized !== '') {
$fingerprints[] = $normalized; $fingerprints[] = $normalized;
} }
} }
$fingerprints = array_values(array_unique($fingerprints)); $fingerprints = array_values(array_unique($fingerprints));
if ($appId === "" || $fingerprints === []) { if ($appId === '' || $fingerprints === []) {
return response()->json([], 200, [ return response()->json([], 200, [
"Cache-Control" => "no-store, no-cache, must-revalidate", 'Cache-Control' => 'no-store, no-cache, must-revalidate',
"Pragma" => "no-cache", 'Pragma' => 'no-cache',
]); ]);
} }
return response()->json( return response()->json(
[ [
[ [
"relation" => [ 'relation' => [
"delegate_permission/common.handle_all_urls", 'delegate_permission/common.handle_all_urls',
], ],
"target" => [ 'target' => [
"namespace" => "android_app", 'namespace' => 'android_app',
"package_name" => $appId, 'package_name' => $appId,
"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
{ {
return view("site.privacy"); return view('site.privacy');
} }
public function terms(): View public function terms(): View
{ {
return view("site.terms"); return view('site.terms');
} }
private function detectPricingCurrency(Request $request): string private function detectPricingCurrency(Request $request): string
{ {
$country = strtoupper( $country = strtoupper(
(string) ($request->header("CF-IPCountry") ?? (string) ($request->header('CF-IPCountry') ??
($request->header("X-Country-Code") ?? ($request->header('X-Country-Code') ??
($request->header("X-Geo-Country") ?? ($request->header('X-Geo-Country') ??
($request->header("X-Appengine-Country") ?? ($request->header('X-Appengine-Country') ??
($request->header("CloudFront-Viewer-Country") ?? ($request->header('CloudFront-Viewer-Country') ??
""))))), ''))))),
); );
return $country === "ID" ? "IDR" : "USD"; return $country === 'ID' ? 'IDR' : 'USD';
} }
public function emojiDetail(string $slug): View|Response public function emojiDetail(string $slug): View|Response
{ {
$dataPath = $this->datasetPath(); $dataPath = $this->datasetPath();
if (!is_file($dataPath)) { if (! is_file($dataPath)) {
abort(500, "Emoji dataset file not found."); abort(500, 'Emoji dataset file not found.');
} }
$raw = file_get_contents($dataPath); $raw = file_get_contents($dataPath);
if ($raw === false) { if ($raw === false) {
abort(500, "Emoji dataset file could not be read."); abort(500, 'Emoji dataset file could not be read.');
} }
$decoded = json_decode($raw, true); $decoded = json_decode($raw, true);
if (!is_array($decoded)) { if (! is_array($decoded)) {
abort(500, "Emoji dataset JSON is invalid."); abort(500, 'Emoji dataset JSON is invalid.');
} }
$items = $decoded["emojis"] ?? []; $items = $decoded['emojis'] ?? [];
$match = null; $match = null;
$byEmoji = []; $byEmoji = [];
foreach ($items as $item) { foreach ($items as $item) {
$char = (string) ($item["emoji"] ?? ""); $char = (string) ($item['emoji'] ?? '');
if ($char !== "" && !isset($byEmoji[$char])) { if ($char !== '' && ! isset($byEmoji[$char])) {
$byEmoji[$char] = $item; $byEmoji[$char] = $item;
} }
if (($item["slug"] ?? "") === $slug) { if (($item['slug'] ?? '') === $slug) {
$match = $item; $match = $item;
} }
} }
if (!$match) { if (! $match) {
abort(404); abort(404);
} }
$relatedDetails = []; $relatedDetails = [];
foreach (array_slice($match["related"] ?? [], 0, 8) as $relatedEmoji) { foreach (array_slice($match['related'] ?? [], 0, 8) as $relatedEmoji) {
$relatedEmoji = (string) $relatedEmoji; $relatedEmoji = (string) $relatedEmoji;
$ref = $byEmoji[$relatedEmoji] ?? null; $ref = $byEmoji[$relatedEmoji] ?? null;
$relatedDetails[] = [ $relatedDetails[] = [
"emoji" => $relatedEmoji, 'emoji' => $relatedEmoji,
"slug" => (string) ($ref["slug"] ?? ""), 'slug' => (string) ($ref['slug'] ?? ''),
"name" => (string) ($ref["name"] ?? $relatedEmoji), 'name' => (string) ($ref['name'] ?? $relatedEmoji),
]; ];
} }
$user = request()->user(); $user = request()->user();
$canManageKeywords = (bool) $user; $canManageKeywords = (bool) $user;
$isPersonal = $user && (string) $user->tier === "personal"; $isPersonal = $user && (string) $user->tier === 'personal';
$freeLimit = (int) config("dewemoji.pagination.free_max_limit", 20); $freeLimit = (int) config('dewemoji.pagination.free_max_limit', 20);
$keywordLimit = $isPersonal ? null : $freeLimit; $keywordLimit = $isPersonal ? null : $freeLimit;
$userKeywords = []; $userKeywords = [];
$activeKeywordCount = 0; $activeKeywordCount = 0;
if ($canManageKeywords) { if ($canManageKeywords) {
$activeKeywordCount = UserKeyword::where("user_id", $user->id) $activeKeywordCount = UserKeyword::where('user_id', $user->id)
->where("is_active", true) ->where('is_active', true)
->count(); ->count();
$userKeywords = UserKeyword::where("user_id", $user->id) $userKeywords = UserKeyword::where('user_id', $user->id)
->where("emoji_slug", $slug) ->where('emoji_slug', $slug)
->orderByDesc("id") ->orderByDesc('id')
->get(); ->get();
} }
$limitReached = $limitReached =
$keywordLimit !== null && $activeKeywordCount >= $keywordLimit; $keywordLimit !== null && $activeKeywordCount >= $keywordLimit;
return view("site.emoji-detail", [ return view('site.emoji-detail', [
"emoji" => $match, 'emoji' => $match,
"relatedDetails" => $relatedDetails, 'relatedDetails' => $relatedDetails,
"canonicalPath" => "/emoji/" . $slug, 'canonicalPath' => '/emoji/'.$slug,
"userKeywords" => $userKeywords, 'userKeywords' => $userKeywords,
"canManageKeywords" => $canManageKeywords, 'canManageKeywords' => $canManageKeywords,
"keywordLimit" => $keywordLimit, 'keywordLimit' => $keywordLimit,
"limitReached" => $limitReached, 'limitReached' => $limitReached,
"activeKeywordCount" => $activeKeywordCount, 'activeKeywordCount' => $activeKeywordCount,
"userTier" => $user?->tier, 'userTier' => $user?->tier,
]); ]);
} }
public function robotsTxt(): Response public function robotsTxt(): Response
{ {
$base = rtrim( $base = rtrim(
config("app.url", request()->getSchemeAndHttpHost()), config('app.url', request()->getSchemeAndHttpHost()),
"/", '/',
); );
$body = $body =
"User-agent: *\nAllow: /\n\nSitemap: " . $base . "/sitemap.xml\n"; "User-agent: *\nAllow: /\n\nSitemap: ".$base."/sitemap.xml\n";
return response($body, 200)->header( return response($body, 200)->header(
"Content-Type", 'Content-Type',
"text/plain; charset=UTF-8", '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( $base = rtrim(
config("app.url", request()->getSchemeAndHttpHost()), config('app.url', request()->getSchemeAndHttpHost()),
"/", '/',
); );
$lastUpdatedTs = isset($data["last_updated_ts"]) $lastUpdatedTs = isset($data['last_updated_ts'])
? (int) $data["last_updated_ts"] ? (int) $data['last_updated_ts']
: time(); : time();
$lastUpdated = gmdate("Y-m-d\TH:i:s\Z", $lastUpdatedTs); $lastUpdated = gmdate("Y-m-d\TH:i:s\Z", $lastUpdatedTs);
$urls = [ $urls = [
[ [
"loc" => $base . "/", 'loc' => $base.'/',
"priority" => "0.8", 'priority' => '0.8',
"changefreq" => "daily", 'changefreq' => 'daily',
], ],
[ [
"loc" => $base . "/api-docs", 'loc' => $base.'/api-docs',
"priority" => "0.5", 'priority' => '0.5',
"changefreq" => "weekly", 'changefreq' => 'weekly',
], ],
[ [
"loc" => $base . "/pricing", 'loc' => $base.'/pricing',
"priority" => "0.7", 'priority' => '0.7',
"changefreq" => "weekly", 'changefreq' => 'weekly',
], ],
[ [
"loc" => $base . "/privacy", 'loc' => $base.'/privacy',
"priority" => "0.3", 'priority' => '0.3',
"changefreq" => "monthly", 'changefreq' => 'monthly',
], ],
[ [
"loc" => $base . "/terms", 'loc' => $base.'/terms',
"priority" => "0.3", 'priority' => '0.3',
"changefreq" => "monthly", 'changefreq' => 'monthly',
], ],
[ [
"loc" => $base . "/support", 'loc' => $base.'/support',
"priority" => "0.4", 'priority' => '0.4',
"changefreq" => "weekly", 'changefreq' => 'weekly',
], ],
]; ];
foreach ($items as $item) { foreach ($items as $item) {
$slug = trim((string) ($item["slug"] ?? "")); $slug = trim((string) ($item['slug'] ?? ''));
if ($slug === "" || $this->shouldHideForSitemap($item)) { if ($slug === '' || $this->shouldHideForSitemap($item)) {
continue; continue;
} }
$urls[] = [ $urls[] = [
"loc" => $base . "/emoji/" . $slug, 'loc' => $base.'/emoji/'.$slug,
"priority" => "0.6", 'priority' => '0.6',
"changefreq" => "weekly", 'changefreq' => 'weekly',
]; ];
} }
$xml = '<?xml version="1.0" encoding="UTF-8"?>' . "\n"; $xml = '<?xml version="1.0" encoding="UTF-8"?>'."\n";
$xml .= $xml .=
'<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">' . '<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">'.
"\n"; "\n";
foreach ($urls as $url) { foreach ($urls as $url) {
$xml .= " <url>\n"; $xml .= " <url>\n";
$xml .= $xml .=
" <loc>" . ' <loc>'.
htmlspecialchars((string) $url["loc"], ENT_XML1) . htmlspecialchars((string) $url['loc'], ENT_XML1).
"</loc>\n"; "</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";
$xml .= " </url>\n"; $xml .= " </url>\n";
} }
$xml .= "</urlset>" . "\n"; $xml .= '</urlset>'."\n";
return response($xml, 200)->header( return response($xml, 200)->header(
"Content-Type", 'Content-Type',
"application/xml; charset=UTF-8", 'application/xml; charset=UTF-8',
); );
} }
@@ -646,18 +647,18 @@ class SiteController extends Controller
private function loadDataset(): array private function loadDataset(): array
{ {
$dataPath = $this->datasetPath(); $dataPath = $this->datasetPath();
if (!is_file($dataPath)) { if (! is_file($dataPath)) {
return ["emojis" => []]; return ['emojis' => []];
} }
$raw = file_get_contents($dataPath); $raw = file_get_contents($dataPath);
if ($raw === false) { if ($raw === false) {
return ["emojis" => []]; return ['emojis' => []];
} }
$decoded = json_decode($raw, true); $decoded = json_decode($raw, true);
if (!is_array($decoded)) { if (! is_array($decoded)) {
return ["emojis" => []]; return ['emojis' => []];
} }
return $decoded; return $decoded;
@@ -666,61 +667,61 @@ class SiteController extends Controller
private function datasetPath(): string private function datasetPath(): string
{ {
$settings = app(SettingsService::class); $settings = app(SettingsService::class);
$activePath = (string) $settings->get("emoji_dataset_active_path", ""); $activePath = (string) $settings->get('emoji_dataset_active_path', '');
if ($activePath !== "" && is_file($activePath)) { if ($activePath !== '' && is_file($activePath)) {
return $activePath; return $activePath;
} }
return (string) config("dewemoji.data_path"); return (string) config('dewemoji.data_path');
} }
private function apkReleaseTargetUrl(string $key): string private function apkReleaseTargetUrl(string $key): string
{ {
if (!(bool) config("dewemoji.apk_release.enabled", false)) { if (! (bool) config('dewemoji.apk_release.enabled', false)) {
return ""; return '';
} }
$base = trim( $base = trim(
(string) config("dewemoji.apk_release.r2_public_base_url", ""), (string) config('dewemoji.apk_release.r2_public_base_url', ''),
); );
$objectKey = trim( $objectKey = trim(
(string) config("dewemoji.apk_release.r2_keys.{$key}", ""), (string) config("dewemoji.apk_release.r2_keys.{$key}", ''),
); );
if ($base === "" || $objectKey === "") { if ($base === '' || $objectKey === '') {
return ""; return '';
} }
return rtrim($base, "/") . "/" . ltrim($objectKey, "/"); return rtrim($base, '/').'/'.ltrim($objectKey, '/');
} }
private function normalizeApkCertFingerprint(string $value): string private function normalizeApkCertFingerprint(string $value): string
{ {
$clean = strtoupper(trim($value)); $clean = strtoupper(trim($value));
if ($clean === "") { if ($clean === '') {
return ""; return '';
} }
if (preg_match('/^[0-9A-F]{64}$/', $clean) === 1) { if (preg_match('/^[0-9A-F]{64}$/', $clean) === 1) {
return implode(":", str_split($clean, 2)); return implode(':', str_split($clean, 2));
} }
if (preg_match('/^[0-9A-F]{2}(?::[0-9A-F]{2}){31}$/', $clean) === 1) { if (preg_match('/^[0-9A-F]{2}(?::[0-9A-F]{2}){31}$/', $clean) === 1) {
return $clean; return $clean;
} }
return ""; return '';
} }
/** /**
* @param array<string,mixed> $emoji * @param array<string,mixed> $emoji
*/ */
private function shouldHideForSitemap(array $emoji): bool private function shouldHideForSitemap(array $emoji): bool
{ {
$name = strtolower(trim((string) ($emoji["name"] ?? ""))); $name = strtolower(trim((string) ($emoji['name'] ?? '')));
$category = strtolower(trim((string) ($emoji["category"] ?? ""))); $category = strtolower(trim((string) ($emoji['category'] ?? '')));
$subcategory = strtolower(trim((string) ($emoji["subcategory"] ?? ""))); $subcategory = strtolower(trim((string) ($emoji['subcategory'] ?? '')));
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)) {
@@ -732,7 +733,7 @@ class SiteController extends Controller
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;
} }

View File

@@ -10,7 +10,7 @@ use Symfony\Component\HttpFoundation\Response;
class CanonicalPathMiddleware class CanonicalPathMiddleware
{ {
/** /**
* @param Closure(Request): Response $next * @param Closure(Request): Response $next
*/ */
public function handle(Request $request, Closure $next): Response public function handle(Request $request, Closure $next): Response
{ {

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

@@ -40,7 +40,7 @@ class ApiKeyService
->whereNull('revoked_at') ->whereNull('revoked_at')
->first(); ->first();
if (!$record) { if (! $record) {
return null; return null;
} }
@@ -48,7 +48,7 @@ class ApiKeyService
$record->save(); $record->save();
$user = $record->user; $user = $record->user;
if (!$user || (string) $user->tier !== 'personal') { if (! $user || (string) $user->tier !== 'personal') {
return null; return null;
} }

View File

@@ -21,14 +21,16 @@ 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, '.', '');
} }
@@ -125,7 +130,7 @@ class PayPalPlanSyncService
$clientSecret = config("dewemoji.billing.providers.paypal.{$mode}.client_secret"); $clientSecret = config("dewemoji.billing.providers.paypal.{$mode}.client_secret");
$apiBase = config("dewemoji.billing.providers.paypal.{$mode}.api_base"); $apiBase = config("dewemoji.billing.providers.paypal.{$mode}.api_base");
if (!$clientId || !$clientSecret || !$apiBase) { if (! $clientId || ! $clientSecret || ! $apiBase) {
return null; return null;
} }
@@ -136,8 +141,9 @@ class PayPalPlanSyncService
'grant_type' => 'client_credentials', 'grant_type' => 'client_credentials',
]); ]);
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;
} }
@@ -175,12 +181,13 @@ class PayPalPlanSyncService
->post(rtrim($apiBase, '/').'/v1/catalogs/products', $payload); ->post(rtrim($apiBase, '/').'/v1/catalogs/products', $payload);
$createdId = $create->json('id'); $createdId = $create->json('id');
if ($createdId) { if ($createdId) {
if (!$create->successful()) { if (! $create->successful()) {
Log::warning('PayPal product create returned non-OK but provided id', [ Log::warning('PayPal product create returned non-OK but provided id', [
'status' => $create->status(), 'status' => $create->status(),
'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;
} }
@@ -237,13 +245,14 @@ class PayPalPlanSyncService
$planId = $res->json('id'); $planId = $res->json('id');
if ($planId) { if ($planId) {
if (!$res->successful()) { if (! $res->successful()) {
Log::warning('PayPal plan create returned non-OK but provided id', [ Log::warning('PayPal plan create returned non-OK but provided id', [
'code' => $code, 'code' => $code,
'status' => $res->status(), 'status' => $res->status(),
'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;
} }
@@ -262,7 +272,7 @@ class PayPalPlanSyncService
->timeout((int) config('dewemoji.billing.providers.paypal.timeout', 10)) ->timeout((int) config('dewemoji.billing.providers.paypal.timeout', 10))
->get(rtrim($apiBase, '/').'/v1/billing/plans/'.$planId); ->get(rtrim($apiBase, '/').'/v1/billing/plans/'.$planId);
if (!$res->successful()) { if (! $res->successful()) {
return null; return null;
} }
@@ -290,12 +300,12 @@ class PayPalPlanSyncService
'page' => 1, 'page' => 1,
]); ]);
if (!$res->successful()) { if (! $res->successful()) {
return 0; return 0;
} }
$items = $res->json('plans') ?? []; $items = $res->json('plans') ?? [];
if (!is_array($items)) { if (! is_array($items)) {
return 0; return 0;
} }
@@ -303,7 +313,7 @@ class PayPalPlanSyncService
foreach ($items as $plan) { foreach ($items as $plan) {
$id = $plan['id'] ?? null; $id = $plan['id'] ?? null;
$status = strtoupper((string) ($plan['status'] ?? '')); $status = strtoupper((string) ($plan['status'] ?? ''));
if (!$id || in_array($id, $keepIds, true)) { if (! $id || in_array($id, $keepIds, true)) {
continue; continue;
} }
if ($status !== 'ACTIVE') { if ($status !== 'ACTIVE') {

View File

@@ -14,16 +14,15 @@ 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
*/ */
public function process(string $eventType, array $payload): void public function process(string $eventType, array $payload): void
{ {
$resource = $payload['resource'] ?? []; $resource = $payload['resource'] ?? [];
if (!is_array($resource)) { if (! is_array($resource)) {
return; return;
} }
@@ -35,7 +34,7 @@ class PaypalWebhookProcessor
} }
$user = User::where('email', $email)->first(); $user = User::where('email', $email)->first();
if (!$user) { if (! $user) {
throw new \RuntimeException('User not found for webhook email.'); throw new \RuntimeException('User not found for webhook email.');
} }
@@ -68,6 +67,7 @@ class PaypalWebhookProcessor
$subscriptionId, $subscriptionId,
$planCode $planCode
); );
return; return;
} }
@@ -99,7 +99,7 @@ class PaypalWebhookProcessor
'tier' => $active ? 'personal' : 'free', 'tier' => $active ? 'personal' : 'free',
]); ]);
$this->keywordQuota->enforceForUser($userId, $active ? 'personal' : 'free'); $this->keywordQuota->enforceForUser($userId, $active ? 'personal' : 'free');
if (!$active) { if (! $active) {
UserApiKey::where('user_id', $userId)->update(['revoked_at' => now()]); UserApiKey::where('user_id', $userId)->update(['revoked_at' => now()]);
} }
} }

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;
} }
@@ -74,12 +75,13 @@ class SubscriptionTransitionService
$isRecurring = in_array((string) $sub->plan, ['personal_monthly', 'personal_annual'], true); $isRecurring = in_array((string) $sub->plan, ['personal_monthly', 'personal_annual'], true);
if ($sub->provider === 'paypal' && $isRecurring && $sub->provider_ref) { if ($sub->provider === 'paypal' && $isRecurring && $sub->provider_ref) {
$cancelled = $this->cancelPaypalSubscription($mode, (string) $sub->provider_ref); $cancelled = $this->cancelPaypalSubscription($mode, (string) $sub->provider_ref);
if (!$cancelled) { if (! $cancelled) {
Log::warning('Could not cancel previous PayPal subscription during plan transition', [ Log::warning('Could not cancel previous PayPal subscription during plan transition', [
'user_id' => $userId, 'user_id' => $userId,
'new_plan' => $newPlanCode, 'new_plan' => $newPlanCode,
'old_subscription_id' => $sub->provider_ref, 'old_subscription_id' => $sub->provider_ref,
]); ]);
continue; continue;
} }
} }
@@ -93,7 +95,7 @@ class SubscriptionTransitionService
private function cancelPaypalSubscription(string $mode, string $subscriptionId): bool private function cancelPaypalSubscription(string $mode, string $subscriptionId): bool
{ {
$token = $this->getAccessToken($mode); $token = $this->getAccessToken($mode);
if (!$token) { if (! $token) {
return false; return false;
} }
@@ -118,7 +120,7 @@ class SubscriptionTransitionService
$clientSecret = config("dewemoji.billing.providers.paypal.{$mode}.client_secret"); $clientSecret = config("dewemoji.billing.providers.paypal.{$mode}.client_secret");
$apiBase = config("dewemoji.billing.providers.paypal.{$mode}.api_base"); $apiBase = config("dewemoji.billing.providers.paypal.{$mode}.api_base");
if (!$clientId || !$clientSecret || !$apiBase) { if (! $clientId || ! $clientSecret || ! $apiBase) {
return null; return null;
} }
@@ -129,7 +131,7 @@ class SubscriptionTransitionService
'grant_type' => 'client_credentials', 'grant_type' => 'client_credentials',
]); ]);
if (!$res->successful()) { if (! $res->successful()) {
return null; return null;
} }

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
@@ -20,7 +19,7 @@ class EmojiCatalogService
public function findItem(int $emojiId): ?array public function findItem(int $emojiId): ?array
{ {
$base = DB::table('emojis')->where('emoji_id', $emojiId)->first(); $base = DB::table('emojis')->where('emoji_id', $emojiId)->first();
if (!$base) { if (! $base) {
return null; return null;
} }
@@ -33,7 +32,7 @@ class EmojiCatalogService
} }
/** /**
* @param array<string,mixed> $input * @param array<string,mixed> $input
*/ */
public function saveItem(array $input): int public function saveItem(array $input): int
{ {
@@ -151,7 +150,7 @@ class EmojiCatalogService
public function deleteItem(int $emojiId): void public function deleteItem(int $emojiId): void
{ {
$emoji = DB::table('emojis')->where('emoji_id', $emojiId)->first(); $emoji = DB::table('emojis')->where('emoji_id', $emojiId)->first();
if (!$emoji) { if (! $emoji) {
return; return;
} }
@@ -176,7 +175,7 @@ class EmojiCatalogService
*/ */
public function importFromDataFile(string $path): array public function importFromDataFile(string $path): array
{ {
if (!is_file($path)) { if (! is_file($path)) {
throw new RuntimeException('Dataset file not found: '.$path); throw new RuntimeException('Dataset file not found: '.$path);
} }
@@ -186,7 +185,7 @@ class EmojiCatalogService
} }
$decoded = json_decode($raw, true); $decoded = json_decode($raw, true);
if (!is_array($decoded) || !is_array($decoded['emojis'] ?? null)) { if (! is_array($decoded) || ! is_array($decoded['emojis'] ?? null)) {
throw new RuntimeException('Invalid emoji dataset format.'); throw new RuntimeException('Invalid emoji dataset format.');
} }
@@ -195,7 +194,7 @@ class EmojiCatalogService
$imported = 0; $imported = 0;
$skipped = 0; $skipped = 0;
foreach ($rows as $row) { foreach ($rows as $row) {
if (!is_array($row)) { if (! is_array($row)) {
continue; continue;
} }
$total++; $total++;
@@ -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;
} }
@@ -222,7 +223,7 @@ class EmojiCatalogService
} }
/** /**
* @param array<string,mixed> $input * @param array<string,mixed> $input
* @return array<string,mixed> * @return array<string,mixed>
*/ */
public function normalizeItemPayload(array $input): array public function normalizeItemPayload(array $input): array
@@ -371,7 +372,7 @@ class EmojiCatalogService
]; ];
$snapshotDir = $this->snapshotDirectory(); $snapshotDir = $this->snapshotDirectory();
if (!is_dir($snapshotDir) && !mkdir($snapshotDir, 0775, true) && !is_dir($snapshotDir)) { if (! is_dir($snapshotDir) && ! mkdir($snapshotDir, 0775, true) && ! is_dir($snapshotDir)) {
throw new RuntimeException('Could not create snapshot directory.'); throw new RuntimeException('Could not create snapshot directory.');
} }
@@ -405,7 +406,7 @@ class EmojiCatalogService
public function listSnapshots(): array public function listSnapshots(): array
{ {
$dir = $this->snapshotDirectory(); $dir = $this->snapshotDirectory();
if (!is_dir($dir)) { if (! is_dir($dir)) {
return []; return [];
} }
@@ -413,11 +414,11 @@ class EmojiCatalogService
$files = glob($dir.'/emojis-*.json') ?: []; $files = glob($dir.'/emojis-*.json') ?: [];
$items = []; $items = [];
foreach ($files as $file) { foreach ($files as $file) {
if (!is_file($file)) { if (! is_file($file)) {
continue; continue;
} }
$name = basename($file); $name = basename($file);
if (!preg_match('/^emojis-(\d{14})\.json$/', $name, $m)) { if (! preg_match('/^emojis-(\d{14})\.json$/', $name, $m)) {
continue; continue;
} }
$items[] = [ $items[] = [
@@ -438,12 +439,12 @@ class EmojiCatalogService
public function activateSnapshot(string $filename, ?string $updatedBy = null): array public function activateSnapshot(string $filename, ?string $updatedBy = null): array
{ {
if (!preg_match('/^emojis-(\d{14})\.json$/', $filename, $m)) { if (! preg_match('/^emojis-(\d{14})\.json$/', $filename, $m)) {
throw new RuntimeException('Invalid snapshot filename.'); throw new RuntimeException('Invalid snapshot filename.');
} }
$fullPath = $this->snapshotDirectory().'/'.$filename; $fullPath = $this->snapshotDirectory().'/'.$filename;
if (!is_file($fullPath)) { if (! is_file($fullPath)) {
throw new RuntimeException('Snapshot file not found.'); throw new RuntimeException('Snapshot file not found.');
} }
@@ -461,7 +462,7 @@ class EmojiCatalogService
} }
/** /**
* @param array<string,mixed> $base * @param array<string,mixed> $base
* @return array<string,mixed> * @return array<string,mixed>
*/ */
private function hydrateEditorItem(array $base): array private function hydrateEditorItem(array $base): array
@@ -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
@@ -517,7 +517,7 @@ class EmojiCatalogService
if (is_string($input)) { if (is_string($input)) {
$input = preg_split('/\r\n|\r|\n|,/', $input) ?: []; $input = preg_split('/\r\n|\r|\n|,/', $input) ?: [];
} }
if (!is_array($input)) { if (! is_array($input)) {
return []; return [];
} }
@@ -547,13 +547,13 @@ class EmojiCatalogService
} }
/** /**
* @param array<int,string> $keywordsEn * @param array<int,string> $keywordsEn
* @param array<int,string> $keywordsId * @param array<int,string> $keywordsId
* @param array<int,string> $aliases * @param array<int,string> $aliases
* @param array<int,string> $shortcodes * @param array<int,string> $shortcodes
* @param array<int,string> $altShortcodes * @param array<int,string> $altShortcodes
* @param array<int,string> $intentTags * @param array<int,string> $intentTags
* @param array<int,string> $codepoints * @param array<int,string> $codepoints
* @return array<int,string> * @return array<int,string>
*/ */
private function buildSearchTokens( private function buildSearchTokens(

View File

@@ -8,7 +8,7 @@ use Illuminate\Support\Facades\Http;
class ExtensionVerificationService class ExtensionVerificationService
{ {
/** /**
* @param array<string> $expectedExtensionIds * @param array<string> $expectedExtensionIds
*/ */
public function verifyToken(string $token, array $expectedExtensionIds): bool public function verifyToken(string $token, array $expectedExtensionIds): bool
{ {
@@ -17,7 +17,7 @@ class ExtensionVerificationService
} }
$config = config('dewemoji.extension_verification', []); $config = config('dewemoji.extension_verification', []);
if (!(bool) ($config['enabled'] ?? true)) { if (! (bool) ($config['enabled'] ?? true)) {
return true; return true;
} }
@@ -36,12 +36,12 @@ class ExtensionVerificationService
'Authorization' => 'key='.$serverKey, 'Authorization' => 'key='.$serverKey,
])->get($url); ])->get($url);
if (!$response->ok()) { if (! $response->ok()) {
return false; return false;
} }
$data = $response->json(); $data = $response->json();
if (!is_array($data)) { if (! is_array($data)) {
return false; return false;
} }

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

@@ -49,7 +49,7 @@ class LiveSqlImportService
public function import(string $path, bool $truncate, int $batchSize, ?OutputInterface $output = null): void public function import(string $path, bool $truncate, int $batchSize, ?OutputInterface $output = null): void
{ {
if (!is_file($path)) { if (! is_file($path)) {
throw new \RuntimeException("SQL file not found: {$path}"); throw new \RuntimeException("SQL file not found: {$path}");
} }
@@ -64,14 +64,14 @@ class LiveSqlImportService
$totalStatements = 0; $totalStatements = 0;
$totalRows = 0; $totalRows = 0;
while (!$file->eof()) { while (! $file->eof()) {
$line = $file->fgets(); $line = $file->fgets();
if ($line === false) { if ($line === false) {
break; break;
} }
if ($statement === '') { if ($statement === '') {
if (!Str::startsWith(ltrim($line), 'INSERT INTO')) { if (! Str::startsWith(ltrim($line), 'INSERT INTO')) {
continue; continue;
} }
} }
@@ -121,7 +121,7 @@ class LiveSqlImportService
]; ];
foreach ($targets as $table) { foreach ($targets as $table) {
if (!Schema::hasTable($table)) { if (! Schema::hasTable($table)) {
continue; continue;
} }
DB::table($table)->truncate(); DB::table($table)->truncate();
@@ -136,7 +136,7 @@ class LiveSqlImportService
$statement = trim($statement); $statement = trim($statement);
$pattern = '/^INSERT INTO `([^`]+)` \\(([^)]+)\\) VALUES\\s*(.+);$/s'; $pattern = '/^INSERT INTO `([^`]+)` \\(([^)]+)\\) VALUES\\s*(.+);$/s';
if (!preg_match($pattern, $statement, $matches)) { if (! preg_match($pattern, $statement, $matches)) {
return [0, 'unknown']; return [0, 'unknown'];
} }
@@ -145,13 +145,13 @@ 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)
); );
$targetTable = $this->tableRename[$table] ?? $table; $targetTable = $this->tableRename[$table] ?? $table;
if (!Schema::hasTable($targetTable)) { if (! Schema::hasTable($targetTable)) {
return [0, $targetTable]; return [0, $targetTable];
} }
@@ -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;
} }
@@ -321,7 +329,7 @@ class LiveSqlImportService
{ {
$value = trim($buffer); $value = trim($buffer);
if (!$valueIsString) { if (! $valueIsString) {
if ($value === '' || strtoupper($value) === 'NULL') { if ($value === '' || strtoupper($value) === 'NULL') {
return null; return null;
} }
@@ -352,7 +360,7 @@ class LiveSqlImportService
return null; return null;
} }
if (!is_string($value)) { if (! is_string($value)) {
return json_encode($value); return json_encode($value);
} }

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,11 +29,12 @@ 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;
} }
/** /**
* @param array<string,mixed> $values * @param array<string,mixed> $values
*/ */
public function setMany(array $values, ?string $updatedBy = null): void public function setMany(array $values, ?string $updatedBy = null): void
{ {

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

@@ -9,7 +9,7 @@ return new class extends Migration
{ {
public function up(): void public function up(): void
{ {
if (!Schema::hasTable('emojis')) { if (! Schema::hasTable('emojis')) {
return; return;
} }
@@ -21,7 +21,7 @@ return new class extends Migration
->pluck('slug') ->pluck('slug')
->all(); ->all();
if (!empty($duplicates)) { if (! empty($duplicates)) {
throw new \RuntimeException( throw new \RuntimeException(
'Cannot add unique index on emojis.slug because duplicate slugs exist: '.implode(', ', $duplicates) 'Cannot add unique index on emojis.slug because duplicate slugs exist: '.implode(', ', $duplicates)
); );
@@ -34,7 +34,7 @@ return new class extends Migration
public function down(): void public function down(): void
{ {
if (!Schema::hasTable('emojis')) { if (! Schema::hasTable('emojis')) {
return; return;
} }

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');