Update pricing UX, billing flows, and API rules
This commit is contained in:
@@ -3,12 +3,25 @@
|
||||
use App\Http\Controllers\Api\V1\EmojiApiController;
|
||||
use App\Http\Controllers\Api\V1\LicenseController;
|
||||
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\AdminSettingsController;
|
||||
use App\Http\Controllers\Api\V1\AdminSubscriptionController;
|
||||
use App\Http\Controllers\Api\V1\AdminAnalyticsController;
|
||||
use App\Http\Controllers\Api\V1\AdminWebhookController;
|
||||
use App\Http\Controllers\Api\V1\PaypalWebhookController;
|
||||
use App\Http\Controllers\Api\V1\ExtensionController;
|
||||
use App\Http\Controllers\Api\V1\UserController;
|
||||
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 Illuminate\Support\Facades\Route;
|
||||
|
||||
Route::options('/v1/{any}', function () {
|
||||
$headers = [
|
||||
'Access-Control-Allow-Methods' => 'GET, POST, OPTIONS',
|
||||
'Access-Control-Allow-Headers' => 'Content-Type, Authorization, X-License-Key, X-Account-Id, X-Dewemoji-Frontend',
|
||||
'Access-Control-Allow-Methods' => 'GET, POST, PUT, DELETE, OPTIONS',
|
||||
'Access-Control-Allow-Headers' => 'Content-Type, Authorization, X-License-Key, X-Api-Key, X-Admin-Token, X-Account-Id, X-Dewemoji-Frontend, X-Extension-Token',
|
||||
'Vary' => 'Origin',
|
||||
];
|
||||
$origin = request()->headers->get('Origin', '');
|
||||
@@ -20,16 +33,56 @@ Route::options('/v1/{any}', function () {
|
||||
return response('', 204, $headers);
|
||||
})->where('any', '.*');
|
||||
|
||||
Route::post('/paypal/webhook', [PayPalController::class, 'webhook']);
|
||||
Route::post('/webhooks/pakasir', [PakasirController::class, 'webhook']);
|
||||
|
||||
Route::prefix('v1')->group(function () {
|
||||
Route::get('/categories', [EmojiApiController::class, 'categories']);
|
||||
Route::get('/emojis', [EmojiApiController::class, 'emojis']);
|
||||
Route::get('/search', [EmojiApiController::class, 'search']);
|
||||
Route::get('/emoji', [EmojiApiController::class, 'emoji']);
|
||||
Route::get('/emoji/{slug}', [EmojiApiController::class, 'emoji']);
|
||||
Route::get('/pricing', [PricingController::class, 'index']);
|
||||
Route::post('/extension/verify', [ExtensionController::class, 'verify']);
|
||||
Route::get('/extension/search', [ExtensionController::class, 'search']);
|
||||
|
||||
Route::post('/license/verify', [LicenseController::class, 'verify']);
|
||||
Route::post('/license/activate', [LicenseController::class, 'activate']);
|
||||
Route::post('/license/deactivate', [LicenseController::class, 'deactivate']);
|
||||
|
||||
Route::post('/user/register', [UserController::class, 'register']);
|
||||
Route::post('/user/login', [UserController::class, 'login']);
|
||||
Route::post('/user/logout', [UserController::class, 'logout']);
|
||||
Route::get('/user/apikeys', [UserController::class, 'listApiKeys']);
|
||||
Route::post('/user/apikeys', [UserController::class, 'createApiKey']);
|
||||
Route::delete('/user/apikeys/{key}', [UserController::class, 'revokeApiKey']);
|
||||
|
||||
Route::get('/keywords', [UserKeywordController::class, 'index']);
|
||||
Route::post('/keywords', [UserKeywordController::class, 'store']);
|
||||
Route::put('/keywords/{id}', [UserKeywordController::class, 'update']);
|
||||
Route::delete('/keywords/{id}', [UserKeywordController::class, 'destroy']);
|
||||
Route::post('/keywords/import', [UserKeywordController::class, 'import']);
|
||||
Route::get('/keywords/export', [UserKeywordController::class, 'export']);
|
||||
|
||||
Route::post('/admin/user/tier', [AdminUserController::class, 'setTier']);
|
||||
Route::get('/admin/users', [AdminUserController::class, 'index']);
|
||||
Route::get('/admin/user', [AdminUserController::class, 'show']);
|
||||
Route::get('/admin/pricing', [AdminPricingController::class, 'index']);
|
||||
Route::post('/admin/pricing', [AdminPricingController::class, 'update']);
|
||||
Route::get('/admin/pricing/changes', [AdminPricingController::class, 'changes']);
|
||||
Route::post('/admin/pricing/reset', [AdminPricingController::class, 'reset']);
|
||||
Route::get('/admin/settings', [AdminSettingsController::class, 'index']);
|
||||
Route::post('/admin/settings', [AdminSettingsController::class, 'update']);
|
||||
Route::get('/admin/subscriptions', [AdminSubscriptionController::class, 'index']);
|
||||
Route::post('/admin/subscription/grant', [AdminSubscriptionController::class, 'grant']);
|
||||
Route::post('/admin/subscription/revoke', [AdminSubscriptionController::class, 'revoke']);
|
||||
Route::get('/admin/analytics', [AdminAnalyticsController::class, 'overview']);
|
||||
Route::get('/admin/webhooks', [AdminWebhookController::class, 'index']);
|
||||
Route::get('/admin/webhooks/{id}', [AdminWebhookController::class, 'show']);
|
||||
Route::post('/admin/webhooks/{id}/replay', [AdminWebhookController::class, 'replay']);
|
||||
|
||||
Route::post('/paypal/webhook', [PaypalWebhookController::class, 'handle']);
|
||||
|
||||
Route::get('/health', [SystemController::class, 'health']);
|
||||
Route::get('/metrics-lite', [SystemController::class, 'metricsLite']);
|
||||
Route::get('/metrics', [SystemController::class, 'metrics']);
|
||||
|
||||
59
app/routes/auth.php
Normal file
59
app/routes/auth.php
Normal file
@@ -0,0 +1,59 @@
|
||||
<?php
|
||||
|
||||
use App\Http\Controllers\Auth\AuthenticatedSessionController;
|
||||
use App\Http\Controllers\Auth\ConfirmablePasswordController;
|
||||
use App\Http\Controllers\Auth\EmailVerificationNotificationController;
|
||||
use App\Http\Controllers\Auth\EmailVerificationPromptController;
|
||||
use App\Http\Controllers\Auth\NewPasswordController;
|
||||
use App\Http\Controllers\Auth\PasswordController;
|
||||
use App\Http\Controllers\Auth\PasswordResetLinkController;
|
||||
use App\Http\Controllers\Auth\RegisteredUserController;
|
||||
use App\Http\Controllers\Auth\VerifyEmailController;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
|
||||
Route::middleware('guest')->group(function () {
|
||||
Route::get('register', [RegisteredUserController::class, 'create'])
|
||||
->name('register');
|
||||
|
||||
Route::post('register', [RegisteredUserController::class, 'store']);
|
||||
|
||||
Route::get('login', [AuthenticatedSessionController::class, 'create'])
|
||||
->name('login');
|
||||
|
||||
Route::post('login', [AuthenticatedSessionController::class, 'store']);
|
||||
|
||||
Route::get('forgot-password', [PasswordResetLinkController::class, 'create'])
|
||||
->name('password.request');
|
||||
|
||||
Route::post('forgot-password', [PasswordResetLinkController::class, 'store'])
|
||||
->name('password.email');
|
||||
|
||||
Route::get('reset-password/{token}', [NewPasswordController::class, 'create'])
|
||||
->name('password.reset');
|
||||
|
||||
Route::post('reset-password', [NewPasswordController::class, 'store'])
|
||||
->name('password.store');
|
||||
});
|
||||
|
||||
Route::middleware('auth')->group(function () {
|
||||
Route::get('verify-email', EmailVerificationPromptController::class)
|
||||
->name('verification.notice');
|
||||
|
||||
Route::get('verify-email/{id}/{hash}', VerifyEmailController::class)
|
||||
->middleware(['signed', 'throttle:6,1'])
|
||||
->name('verification.verify');
|
||||
|
||||
Route::post('email/verification-notification', [EmailVerificationNotificationController::class, 'store'])
|
||||
->middleware('throttle:6,1')
|
||||
->name('verification.send');
|
||||
|
||||
Route::get('confirm-password', [ConfirmablePasswordController::class, 'show'])
|
||||
->name('password.confirm');
|
||||
|
||||
Route::post('confirm-password', [ConfirmablePasswordController::class, 'store']);
|
||||
|
||||
Route::put('password', [PasswordController::class, 'update'])->name('password.update');
|
||||
|
||||
Route::post('logout', [AuthenticatedSessionController::class, 'destroy'])
|
||||
->name('logout');
|
||||
});
|
||||
@@ -2,7 +2,13 @@
|
||||
|
||||
use Illuminate\Foundation\Inspiring;
|
||||
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\WebhookEvent;
|
||||
use Illuminate\Support\Facades\Mail;
|
||||
use App\Mail\TestMailketing;
|
||||
|
||||
Artisan::command('inspire', function () {
|
||||
$this->comment(Inspiring::quote());
|
||||
@@ -16,3 +22,80 @@ Artisan::command('dewemoji:import-live-sql {path : Absolute path to dewemojiAPI_
|
||||
$importer = app(LiveSqlImportService::class);
|
||||
$importer->import($path, $truncate, $batch, $this->output);
|
||||
})->purpose('Import live SQL dump into the current database');
|
||||
|
||||
Artisan::command('dewemoji:webhooks:process {--limit=100 : Max events to process} {--status=pending,received : Comma-separated statuses}', function () {
|
||||
$limit = (int) $this->option('limit');
|
||||
$statuses = array_filter(array_map('trim', explode(',', (string) $this->option('status'))));
|
||||
|
||||
if (empty($statuses)) {
|
||||
$this->error('No statuses provided.');
|
||||
return 1;
|
||||
}
|
||||
|
||||
$processor = app(PaypalWebhookProcessor::class);
|
||||
|
||||
$events = WebhookEvent::query()
|
||||
->whereIn('status', $statuses)
|
||||
->orderBy('id')
|
||||
->limit($limit)
|
||||
->get();
|
||||
|
||||
$processed = 0;
|
||||
$failed = 0;
|
||||
|
||||
foreach ($events as $event) {
|
||||
try {
|
||||
if ($event->provider === 'paypal') {
|
||||
$processor->process((string) ($event->event_type ?? ''), $event->payload ?? []);
|
||||
}
|
||||
$event->update([
|
||||
'status' => 'processed',
|
||||
'processed_at' => now(),
|
||||
'error' => null,
|
||||
]);
|
||||
$processed++;
|
||||
} catch (\Throwable $e) {
|
||||
$event->update([
|
||||
'status' => 'error',
|
||||
'processed_at' => now(),
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
$failed++;
|
||||
}
|
||||
}
|
||||
|
||||
$this->info("Processed {$processed} events, failed {$failed}.");
|
||||
return 0;
|
||||
})->purpose('Process pending webhook events');
|
||||
|
||||
Schedule::command('dewemoji:webhooks:process --limit=200')->everyMinute()->withoutOverlapping();
|
||||
|
||||
Artisan::command('paypal:sync-plans {--mode=both : sandbox|live|both}', function () {
|
||||
$mode = (string) $this->option('mode');
|
||||
$service = app(PayPalPlanSyncService::class);
|
||||
|
||||
$runMode = match ($mode) {
|
||||
'sandbox' => ['sandbox'],
|
||||
'live' => ['live'],
|
||||
default => ['sandbox', 'live'],
|
||||
};
|
||||
|
||||
foreach ($runMode as $env) {
|
||||
$this->info("Syncing PayPal plans: {$env}");
|
||||
$result = $service->sync($env);
|
||||
$this->line("Created: {$result['created']} · Updated: {$result['updated']} · Deactivated: {$result['deactivated']} · Skipped: {$result['skipped']}");
|
||||
}
|
||||
})->purpose('Create/rotate PayPal plans based on pricing plans');
|
||||
|
||||
Artisan::command('mailketing:test {email : Recipient email address}', function () {
|
||||
$email = (string) $this->argument('email');
|
||||
|
||||
try {
|
||||
Mail::to($email)->send(new TestMailketing());
|
||||
$this->info("Mailketing test email sent to {$email}.");
|
||||
return 0;
|
||||
} catch (\Throwable $e) {
|
||||
$this->error('Mailketing test failed: '.$e->getMessage());
|
||||
return 1;
|
||||
}
|
||||
})->purpose('Send a Mailketing API test email');
|
||||
|
||||
56
app/routes/dashboard.php
Normal file
56
app/routes/dashboard.php
Normal file
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
use App\Http\Controllers\Dashboard\AdminDashboardController;
|
||||
use App\Http\Controllers\Dashboard\UserDashboardController;
|
||||
use App\Http\Controllers\ProfileController;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
|
||||
Route::middleware('auth')->prefix('dashboard')->name('dashboard.')->group(function () {
|
||||
Route::middleware('verified')->group(function () {
|
||||
Route::get('/', [UserDashboardController::class, 'overview'])->name('overview');
|
||||
Route::get('/keywords', [UserDashboardController::class, 'keywords'])->name('keywords');
|
||||
Route::get('/keywords/search', [UserDashboardController::class, 'keywordSearch'])->name('keywords.search');
|
||||
Route::post('/keywords', [UserDashboardController::class, 'storeKeyword'])->name('keywords.store');
|
||||
Route::put('/keywords/{keyword}', [UserDashboardController::class, 'updateKeyword'])->name('keywords.update');
|
||||
Route::delete('/keywords/{keyword}', [UserDashboardController::class, 'deleteKeyword'])->name('keywords.delete');
|
||||
Route::post('/keywords/import', [UserDashboardController::class, 'importKeywords'])->name('keywords.import');
|
||||
Route::get('/keywords/export', [UserDashboardController::class, 'exportKeywords'])->name('keywords.export');
|
||||
|
||||
Route::get('/api-keys', [UserDashboardController::class, 'apiKeys'])->name('api-keys');
|
||||
Route::post('/api-keys', [UserDashboardController::class, 'createApiKey'])->name('api-keys.create');
|
||||
Route::post('/api-keys/{key}/revoke', [UserDashboardController::class, 'revokeApiKey'])->name('api-keys.revoke');
|
||||
|
||||
Route::get('/billing', [UserDashboardController::class, 'billing'])->name('billing');
|
||||
Route::get('/preferences', [UserDashboardController::class, 'preferences'])->name('preferences');
|
||||
|
||||
Route::middleware('can:admin')->prefix('admin')->name('admin.')->group(function () {
|
||||
Route::get('/users', [AdminDashboardController::class, 'users'])->name('users');
|
||||
Route::get('/users/{user}', [AdminDashboardController::class, 'userDetail'])->name('users.show');
|
||||
Route::post('/users/tier', [AdminDashboardController::class, 'updateUserTier'])->name('users.tier');
|
||||
Route::post('/users/create', [AdminDashboardController::class, 'createUser'])->name('users.create');
|
||||
Route::delete('/users/{user}', [AdminDashboardController::class, 'deleteUser'])->name('users.delete');
|
||||
|
||||
Route::get('/subscriptions', [AdminDashboardController::class, 'subscriptions'])->name('subscriptions');
|
||||
Route::get('/subscriptions/{subscription}', [AdminDashboardController::class, 'subscriptionDetail'])->name('subscriptions.show');
|
||||
Route::post('/subscriptions/grant', [AdminDashboardController::class, 'grantSubscription'])->name('subscriptions.grant');
|
||||
Route::post('/subscriptions/revoke', [AdminDashboardController::class, 'revokeSubscription'])->name('subscriptions.revoke');
|
||||
|
||||
Route::get('/pricing', [AdminDashboardController::class, 'pricing'])->name('pricing');
|
||||
Route::post('/pricing/update', [AdminDashboardController::class, 'updatePricing'])->name('pricing.update');
|
||||
Route::post('/pricing/reset', [AdminDashboardController::class, 'resetPricing'])->name('pricing.reset');
|
||||
Route::post('/pricing/snapshot', [AdminDashboardController::class, 'createPricingSnapshot'])->name('pricing.snapshot');
|
||||
Route::post('/pricing/paypal-sync', [AdminDashboardController::class, 'syncPaypalPlans'])->name('pricing.paypal_sync');
|
||||
|
||||
Route::get('/webhooks', [AdminDashboardController::class, 'webhooks'])->name('webhooks');
|
||||
Route::post('/webhooks/{id}/replay', [AdminDashboardController::class, 'replayWebhook'])->name('webhooks.replay');
|
||||
Route::post('/webhooks/replay-failed', [AdminDashboardController::class, 'replayFailedWebhooks'])->name('webhooks.replay_failed');
|
||||
Route::get('/webhooks/{event}', [AdminDashboardController::class, 'webhookDetail'])->name('webhooks.show');
|
||||
|
||||
Route::get('/settings', [AdminDashboardController::class, 'settings'])->name('settings');
|
||||
Route::post('/settings/update', [AdminDashboardController::class, 'updateSettings'])->name('settings.update');
|
||||
|
||||
Route::get('/export/{type}', [AdminDashboardController::class, 'exportCsv'])->name('export');
|
||||
Route::get('/audit-logs', [AdminDashboardController::class, 'auditLogs'])->name('audit_logs');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,9 @@
|
||||
<?php
|
||||
|
||||
use App\Http\Controllers\ProfileController;
|
||||
use App\Http\Controllers\Web\SiteController;
|
||||
use App\Http\Controllers\Billing\PayPalController;
|
||||
use App\Http\Controllers\Billing\PakasirController;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
|
||||
Route::get('/', [SiteController::class, 'home'])->name('home');
|
||||
@@ -11,14 +14,37 @@ Route::get('/api-docs', [SiteController::class, 'apiDocs'])->name('api-docs');
|
||||
Route::get('/emoji/{slug}', [SiteController::class, 'emojiDetail'])->name('emoji-detail');
|
||||
|
||||
Route::get('/pricing', [SiteController::class, 'pricing'])->name('pricing');
|
||||
Route::post('/pricing/currency', [SiteController::class, 'setPricingCurrency'])->name('pricing.currency');
|
||||
Route::get('/support', [SiteController::class, 'support'])->name('support');
|
||||
Route::get('/privacy', [SiteController::class, 'privacy'])->name('privacy');
|
||||
Route::get('/terms', [SiteController::class, 'terms'])->name('terms');
|
||||
Route::get('/profile', function () {
|
||||
return redirect()->route('profile.edit');
|
||||
})->middleware('auth');
|
||||
|
||||
Route::get('/{categorySlug}', [SiteController::class, 'category'])
|
||||
->where('categorySlug', 'all|smileys|people|animals|food|travel|activities|objects|symbols|flags')
|
||||
->name('category');
|
||||
Route::get('/{categorySlug}/{subcategorySlug}', [SiteController::class, 'categorySubcategory'])
|
||||
->where('categorySlug', 'all|smileys|people|animals|food|travel|activities|objects|symbols|flags')
|
||||
->where('subcategorySlug', '[a-z0-9\-]+')
|
||||
->where('subcategorySlug', '[a-z0-9\\-]+')
|
||||
->name('category-subcategory');
|
||||
|
||||
Route::middleware('auth')->group(function () {
|
||||
Route::post('/billing/paypal/create', [PayPalController::class, 'createSubscription'])
|
||||
->middleware('verified')
|
||||
->name('billing.paypal.create');
|
||||
Route::get('/billing/paypal/return', [PayPalController::class, 'return'])->name('billing.paypal.return');
|
||||
Route::post('/billing/pakasir/create', [PakasirController::class, 'createTransaction'])
|
||||
->middleware('verified')
|
||||
->name('billing.pakasir.create');
|
||||
Route::post('/billing/pakasir/cancel', [PakasirController::class, 'cancelPending'])
|
||||
->middleware('verified')
|
||||
->name('billing.pakasir.cancel');
|
||||
|
||||
Route::get('/dashboard/profile', [ProfileController::class, 'edit'])->name('profile.edit');
|
||||
Route::patch('/dashboard/profile', [ProfileController::class, 'update'])->name('profile.update');
|
||||
Route::delete('/dashboard/profile', [ProfileController::class, 'destroy'])->name('profile.destroy');
|
||||
});
|
||||
|
||||
require __DIR__.'/auth.php';
|
||||
|
||||
Reference in New Issue
Block a user