Ezekiel999 commited on
Commit
10dc6f2
·
verified ·
1 Parent(s): 20c0764

Deploy EventFlow to HF Spaces with Docker

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .editorconfig +18 -0
  2. .env.example +65 -0
  3. .gitattributes +11 -35
  4. .github/workflows/deploy.yml +51 -0
  5. .gitignore +24 -0
  6. Dockerfile +56 -0
  7. README.md +59 -10
  8. app/Console/Commands/ExpirePendingOrders.php +29 -0
  9. app/Http/Controllers/Admin/CategoryController.php +39 -0
  10. app/Http/Controllers/Admin/DashboardController.php +29 -0
  11. app/Http/Controllers/Admin/EventController.php +31 -0
  12. app/Http/Controllers/Admin/ReportController.php +41 -0
  13. app/Http/Controllers/Admin/UserController.php +42 -0
  14. app/Http/Controllers/Auth/AuthenticatedSessionController.php +47 -0
  15. app/Http/Controllers/Auth/ConfirmablePasswordController.php +40 -0
  16. app/Http/Controllers/Auth/EmailVerificationNotificationController.php +24 -0
  17. app/Http/Controllers/Auth/EmailVerificationPromptController.php +21 -0
  18. app/Http/Controllers/Auth/NewPasswordController.php +62 -0
  19. app/Http/Controllers/Auth/PasswordController.php +29 -0
  20. app/Http/Controllers/Auth/PasswordResetLinkController.php +44 -0
  21. app/Http/Controllers/Auth/RegisteredUserController.php +50 -0
  22. app/Http/Controllers/Auth/VerifyEmailController.php +27 -0
  23. app/Http/Controllers/CheckoutController.php +83 -0
  24. app/Http/Controllers/Controller.php +8 -0
  25. app/Http/Controllers/DashboardController.php +30 -0
  26. app/Http/Controllers/EventController.php +74 -0
  27. app/Http/Controllers/HomeController.php +28 -0
  28. app/Http/Controllers/MyTicketsController.php +42 -0
  29. app/Http/Controllers/OrdersController.php +73 -0
  30. app/Http/Controllers/Organizer/DashboardController.php +34 -0
  31. app/Http/Controllers/Organizer/EventController.php +123 -0
  32. app/Http/Controllers/Organizer/OrderController.php +71 -0
  33. app/Http/Controllers/Organizer/ScannerController.php +31 -0
  34. app/Http/Controllers/Organizer/TicketTierController.php +74 -0
  35. app/Http/Controllers/ProfileController.php +60 -0
  36. app/Http/Middleware/RoleMiddleware.php +23 -0
  37. app/Http/Requests/Auth/LoginRequest.php +85 -0
  38. app/Http/Requests/ProfileUpdateRequest.php +30 -0
  39. app/Models/Attendee.php +57 -0
  40. app/Models/Category.php +27 -0
  41. app/Models/Event.php +100 -0
  42. app/Models/Order.php +70 -0
  43. app/Models/OrderItem.php +35 -0
  44. app/Models/PromoCode.php +43 -0
  45. app/Models/TicketTier.php +53 -0
  46. app/Models/User.php +47 -0
  47. app/Providers/AppServiceProvider.php +24 -0
  48. app/Services/OrderService.php +154 -0
  49. app/Services/TicketService.php +77 -0
  50. artisan +18 -0
.editorconfig ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ root = true
2
+
3
+ [*]
4
+ charset = utf-8
5
+ end_of_line = lf
6
+ indent_size = 4
7
+ indent_style = space
8
+ insert_final_newline = true
9
+ trim_trailing_whitespace = true
10
+
11
+ [*.md]
12
+ trim_trailing_whitespace = false
13
+
14
+ [*.{yml,yaml}]
15
+ indent_size = 2
16
+
17
+ [compose.yaml]
18
+ indent_size = 4
.env.example ADDED
@@ -0,0 +1,65 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ APP_NAME=Laravel
2
+ APP_ENV=local
3
+ APP_KEY=
4
+ APP_DEBUG=true
5
+ APP_URL=http://localhost
6
+
7
+ APP_LOCALE=en
8
+ APP_FALLBACK_LOCALE=en
9
+ APP_FAKER_LOCALE=en_US
10
+
11
+ APP_MAINTENANCE_DRIVER=file
12
+ # APP_MAINTENANCE_STORE=database
13
+
14
+ # PHP_CLI_SERVER_WORKERS=4
15
+
16
+ BCRYPT_ROUNDS=12
17
+
18
+ LOG_CHANNEL=stack
19
+ LOG_STACK=single
20
+ LOG_DEPRECATIONS_CHANNEL=null
21
+ LOG_LEVEL=debug
22
+
23
+ DB_CONNECTION=sqlite
24
+ # DB_HOST=127.0.0.1
25
+ # DB_PORT=3306
26
+ # DB_DATABASE=laravel
27
+ # DB_USERNAME=root
28
+ # DB_PASSWORD=
29
+
30
+ SESSION_DRIVER=database
31
+ SESSION_LIFETIME=120
32
+ SESSION_ENCRYPT=false
33
+ SESSION_PATH=/
34
+ SESSION_DOMAIN=null
35
+
36
+ BROADCAST_CONNECTION=log
37
+ FILESYSTEM_DISK=local
38
+ QUEUE_CONNECTION=database
39
+
40
+ CACHE_STORE=database
41
+ # CACHE_PREFIX=
42
+
43
+ MEMCACHED_HOST=127.0.0.1
44
+
45
+ REDIS_CLIENT=phpredis
46
+ REDIS_HOST=127.0.0.1
47
+ REDIS_PASSWORD=null
48
+ REDIS_PORT=6379
49
+
50
+ MAIL_MAILER=log
51
+ MAIL_SCHEME=null
52
+ MAIL_HOST=127.0.0.1
53
+ MAIL_PORT=2525
54
+ MAIL_USERNAME=null
55
+ MAIL_PASSWORD=null
56
+ MAIL_FROM_ADDRESS="hello@example.com"
57
+ MAIL_FROM_NAME="${APP_NAME}"
58
+
59
+ AWS_ACCESS_KEY_ID=
60
+ AWS_SECRET_ACCESS_KEY=
61
+ AWS_DEFAULT_REGION=us-east-1
62
+ AWS_BUCKET=
63
+ AWS_USE_PATH_STYLE_ENDPOINT=false
64
+
65
+ VITE_APP_NAME="${APP_NAME}"
.gitattributes CHANGED
@@ -1,35 +1,11 @@
1
- *.7z filter=lfs diff=lfs merge=lfs -text
2
- *.arrow filter=lfs diff=lfs merge=lfs -text
3
- *.bin filter=lfs diff=lfs merge=lfs -text
4
- *.bz2 filter=lfs diff=lfs merge=lfs -text
5
- *.ckpt filter=lfs diff=lfs merge=lfs -text
6
- *.ftz filter=lfs diff=lfs merge=lfs -text
7
- *.gz filter=lfs diff=lfs merge=lfs -text
8
- *.h5 filter=lfs diff=lfs merge=lfs -text
9
- *.joblib filter=lfs diff=lfs merge=lfs -text
10
- *.lfs.* filter=lfs diff=lfs merge=lfs -text
11
- *.mlmodel filter=lfs diff=lfs merge=lfs -text
12
- *.model filter=lfs diff=lfs merge=lfs -text
13
- *.msgpack filter=lfs diff=lfs merge=lfs -text
14
- *.npy filter=lfs diff=lfs merge=lfs -text
15
- *.npz filter=lfs diff=lfs merge=lfs -text
16
- *.onnx filter=lfs diff=lfs merge=lfs -text
17
- *.ot filter=lfs diff=lfs merge=lfs -text
18
- *.parquet filter=lfs diff=lfs merge=lfs -text
19
- *.pb filter=lfs diff=lfs merge=lfs -text
20
- *.pickle filter=lfs diff=lfs merge=lfs -text
21
- *.pkl filter=lfs diff=lfs merge=lfs -text
22
- *.pt filter=lfs diff=lfs merge=lfs -text
23
- *.pth filter=lfs diff=lfs merge=lfs -text
24
- *.rar filter=lfs diff=lfs merge=lfs -text
25
- *.safetensors filter=lfs diff=lfs merge=lfs -text
26
- saved_model/**/* filter=lfs diff=lfs merge=lfs -text
27
- *.tar.* filter=lfs diff=lfs merge=lfs -text
28
- *.tar filter=lfs diff=lfs merge=lfs -text
29
- *.tflite filter=lfs diff=lfs merge=lfs -text
30
- *.tgz filter=lfs diff=lfs merge=lfs -text
31
- *.wasm filter=lfs diff=lfs merge=lfs -text
32
- *.xz filter=lfs diff=lfs merge=lfs -text
33
- *.zip filter=lfs diff=lfs merge=lfs -text
34
- *.zst filter=lfs diff=lfs merge=lfs -text
35
- *tfevents* filter=lfs diff=lfs merge=lfs -text
 
1
+ * text=auto eol=lf
2
+
3
+ *.blade.php diff=html
4
+ *.css diff=css
5
+ *.html diff=html
6
+ *.md diff=markdown
7
+ *.php diff=php
8
+
9
+ /.github export-ignore
10
+ CHANGELOG.md export-ignore
11
+ .styleci.yml export-ignore
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
.github/workflows/deploy.yml ADDED
@@ -0,0 +1,51 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: Build & Push Docker Image to GHCR
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ workflow_dispatch:
7
+
8
+ env:
9
+ REGISTRY: ghcr.io
10
+ IMAGE_NAME: ${{ github.repository }}
11
+
12
+ jobs:
13
+ build-and-push:
14
+ runs-on: ubuntu-latest
15
+ permissions:
16
+ contents: read
17
+ packages: write
18
+
19
+ steps:
20
+ - name: Checkout repository
21
+ uses: actions/checkout@v4
22
+
23
+ - name: Log in to GitHub Container Registry
24
+ uses: docker/login-action@v3
25
+ with:
26
+ registry: ${{ env.REGISTRY }}
27
+ username: ${{ github.actor }}
28
+ password: ${{ secrets.GITHUB_TOKEN }}
29
+
30
+ - name: Extract metadata (tags, labels)
31
+ id: meta
32
+ uses: docker/metadata-action@v5
33
+ with:
34
+ images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
35
+ tags: |
36
+ type=raw,value=latest,enable={{is_default_branch}}
37
+ type=sha,prefix=,format=short
38
+
39
+ - name: Set up Docker Buildx
40
+ uses: docker/setup-buildx-action@v3
41
+
42
+ - name: Build and push Docker image
43
+ uses: docker/build-push-action@v5
44
+ with:
45
+ context: .
46
+ file: ./docker/Dockerfile.sumopod
47
+ push: true
48
+ tags: ${{ steps.meta.outputs.tags }}
49
+ labels: ${{ steps.meta.outputs.labels }}
50
+ cache-from: type=gha
51
+ cache-to: type=gha,mode=max
.gitignore ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ *.log
2
+ .DS_Store
3
+ .env
4
+ .env.backup
5
+ .env.production
6
+ .phpactor.json
7
+ .phpunit.result.cache
8
+ /.fleet
9
+ /.idea
10
+ /.nova
11
+ /.phpunit.cache
12
+ /.vscode
13
+ /.zed
14
+ /auth.json
15
+ /node_modules
16
+ /public/build
17
+ /public/hot
18
+ /public/storage
19
+ /storage/*.key
20
+ /storage/pail
21
+ /vendor
22
+ Homestead.json
23
+ Homestead.yaml
24
+ Thumbs.db
Dockerfile ADDED
@@ -0,0 +1,56 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM php:8.4-apache
2
+
3
+ # Enable Apache modules and ensure correct MPM for mod_php
4
+ RUN a2dismod mpm_event mpm_worker || true \
5
+ && a2enmod mpm_prefork rewrite headers
6
+
7
+ # Install system dependencies + PHP extensions (PostgreSQL)
8
+ RUN apt-get update && apt-get install -y \
9
+ git curl libpng-dev libonig-dev libxml2-dev libpq-dev libzip-dev \
10
+ libfreetype6-dev libjpeg-dev libicu-dev zip unzip \
11
+ && docker-php-ext-configure gd --with-freetype --with-jpeg \
12
+ && docker-php-ext-install pdo pdo_pgsql pgsql mbstring exif pcntl bcmath gd zip intl \
13
+ && apt-get clean && rm -rf /var/lib/apt/lists/*
14
+
15
+ # PHP production settings
16
+ RUN cp /usr/local/etc/php/php.ini-production /usr/local/etc/php/php.ini \
17
+ && sed -i 's/memory_limit = .*/memory_limit = 256M/' /usr/local/etc/php/php.ini \
18
+ && sed -i 's/upload_max_filesize = .*/upload_max_filesize = 10M/' /usr/local/etc/php/php.ini \
19
+ && sed -i 's/post_max_size = .*/post_max_size = 12M/' /usr/local/etc/php/php.ini \
20
+ && sed -i 's/max_execution_time = .*/max_execution_time = 60/' /usr/local/etc/php/php.ini
21
+
22
+ # Install Composer
23
+ COPY --from=composer:latest /usr/bin/composer /usr/bin/composer
24
+
25
+ # Set Apache document root to Laravel's public directory
26
+ RUN sed -ri -e 's!/var/www/html!/var/www/public!g' /etc/apache2/sites-available/*.conf \
27
+ && sed -ri -e 's!/var/www/!/var/www/public/!g' /etc/apache2/apache2.conf /etc/apache2/conf-available/*.conf
28
+
29
+ # Allow .htaccess overrides (required for Laravel routing)
30
+ RUN sed -i '/<Directory \/var\/www\/public\/>/,/<\/Directory>/ s/AllowOverride None/AllowOverride All/' /etc/apache2/apache2.conf \
31
+ || echo "<Directory /var/www/public/>\n AllowOverride All\n Require all granted\n</Directory>" >> /etc/apache2/apache2.conf
32
+
33
+ WORKDIR /var/www
34
+
35
+ # Install PHP dependencies (production only)
36
+ COPY composer.json composer.lock* ./
37
+ RUN composer install --no-dev --no-scripts --no-autoloader --prefer-dist
38
+
39
+ # Copy application
40
+ COPY . /var/www
41
+
42
+ # Dump optimized autoloader
43
+ RUN composer dump-autoload --optimize
44
+
45
+ # HF Spaces runs as non-root (uid 1000), so open permissions
46
+ RUN mkdir -p storage/logs storage/framework/cache storage/framework/sessions storage/framework/views bootstrap/cache storage/app/public \
47
+ && chmod -R 777 storage bootstrap/cache \
48
+ && chmod -R 755 public
49
+
50
+ # Make start script executable
51
+ RUN chmod +x /var/www/docker/start-hfspace.sh
52
+
53
+ # HF Spaces expects port 7860
54
+ EXPOSE 7860
55
+
56
+ CMD ["bash", "/var/www/docker/start-hfspace.sh"]
README.md CHANGED
@@ -1,10 +1,59 @@
1
- ---
2
- title: Eventflow
3
- emoji: 🔥
4
- colorFrom: indigo
5
- colorTo: red
6
- sdk: docker
7
- pinned: false
8
- ---
9
-
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <p align="center"><a href="https://laravel.com" target="_blank"><img src="https://raw.githubusercontent.com/laravel/art/master/logo-lockup/5%20SVG/2%20CMYK/1%20Full%20Color/laravel-logolockup-cmyk-red.svg" width="400" alt="Laravel Logo"></a></p>
2
+
3
+ <p align="center">
4
+ <a href="https://github.com/laravel/framework/actions"><img src="https://github.com/laravel/framework/workflows/tests/badge.svg" alt="Build Status"></a>
5
+ <a href="https://packagist.org/packages/laravel/framework"><img src="https://img.shields.io/packagist/dt/laravel/framework" alt="Total Downloads"></a>
6
+ <a href="https://packagist.org/packages/laravel/framework"><img src="https://img.shields.io/packagist/v/laravel/framework" alt="Latest Stable Version"></a>
7
+ <a href="https://packagist.org/packages/laravel/framework"><img src="https://img.shields.io/packagist/l/laravel/framework" alt="License"></a>
8
+ </p>
9
+
10
+ ## About Laravel
11
+
12
+ Laravel is a web application framework with expressive, elegant syntax. We believe development must be an enjoyable and creative experience to be truly fulfilling. Laravel takes the pain out of development by easing common tasks used in many web projects, such as:
13
+
14
+ - [Simple, fast routing engine](https://laravel.com/docs/routing).
15
+ - [Powerful dependency injection container](https://laravel.com/docs/container).
16
+ - Multiple back-ends for [session](https://laravel.com/docs/session) and [cache](https://laravel.com/docs/cache) storage.
17
+ - Expressive, intuitive [database ORM](https://laravel.com/docs/eloquent).
18
+ - Database agnostic [schema migrations](https://laravel.com/docs/migrations).
19
+ - [Robust background job processing](https://laravel.com/docs/queues).
20
+ - [Real-time event broadcasting](https://laravel.com/docs/broadcasting).
21
+
22
+ Laravel is accessible, powerful, and provides tools required for large, robust applications.
23
+
24
+ ## Learning Laravel
25
+
26
+ Laravel has the most extensive and thorough [documentation](https://laravel.com/docs) and video tutorial library of all modern web application frameworks, making it a breeze to get started with the framework. You can also check out [Laravel Learn](https://laravel.com/learn), where you will be guided through building a modern Laravel application.
27
+
28
+ If you don't feel like reading, [Laracasts](https://laracasts.com) can help. Laracasts contains thousands of video tutorials on a range of topics including Laravel, modern PHP, unit testing, and JavaScript. Boost your skills by digging into our comprehensive video library.
29
+
30
+ ## Laravel Sponsors
31
+
32
+ We would like to extend our thanks to the following sponsors for funding Laravel development. If you are interested in becoming a sponsor, please visit the [Laravel Partners program](https://partners.laravel.com).
33
+
34
+ ### Premium Partners
35
+
36
+ - **[Vehikl](https://vehikl.com)**
37
+ - **[Tighten Co.](https://tighten.co)**
38
+ - **[Kirschbaum Development Group](https://kirschbaumdevelopment.com)**
39
+ - **[64 Robots](https://64robots.com)**
40
+ - **[Curotec](https://www.curotec.com/services/technologies/laravel)**
41
+ - **[DevSquad](https://devsquad.com/hire-laravel-developers)**
42
+ - **[Redberry](https://redberry.international/laravel-development)**
43
+ - **[Active Logic](https://activelogic.com)**
44
+
45
+ ## Contributing
46
+
47
+ Thank you for considering contributing to the Laravel framework! The contribution guide can be found in the [Laravel documentation](https://laravel.com/docs/contributions).
48
+
49
+ ## Code of Conduct
50
+
51
+ In order to ensure that the Laravel community is welcoming to all, please review and abide by the [Code of Conduct](https://laravel.com/docs/contributions#code-of-conduct).
52
+
53
+ ## Security Vulnerabilities
54
+
55
+ If you discover a security vulnerability within Laravel, please send an e-mail to Taylor Otwell via [taylor@laravel.com](mailto:taylor@laravel.com). All security vulnerabilities will be promptly addressed.
56
+
57
+ ## License
58
+
59
+ The Laravel framework is open-sourced software licensed under the [MIT license](https://opensource.org/licenses/MIT).
app/Console/Commands/ExpirePendingOrders.php ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ namespace App\Console\Commands;
4
+
5
+ use App\Models\Order;
6
+ use App\Services\OrderService;
7
+ use Illuminate\Console\Command;
8
+
9
+ class ExpirePendingOrders extends Command
10
+ {
11
+ protected $signature = 'orders:expire-pending';
12
+ protected $description = 'Expire pending orders that have passed their expiry time and restore stock';
13
+
14
+ public function handle(OrderService $orderService): int
15
+ {
16
+ $orders = Order::where('status', 'pending')
17
+ ->where('expires_at', '<', now())
18
+ ->get();
19
+
20
+ $count = 0;
21
+ foreach ($orders as $order) {
22
+ $orderService->expireOrder($order);
23
+ $count++;
24
+ }
25
+
26
+ $this->info("Expired {$count} pending orders.");
27
+ return Command::SUCCESS;
28
+ }
29
+ }
app/Http/Controllers/Admin/CategoryController.php ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ namespace App\Http\Controllers\Admin;
4
+
5
+ use App\Http\Controllers\Controller;
6
+ use App\Models\Category;
7
+ use Illuminate\Http\Request;
8
+
9
+ class CategoryController extends Controller
10
+ {
11
+ public function index()
12
+ {
13
+ $categories = Category::withCount('events')->orderBy('name')->paginate(20);
14
+ return view('admin.categories.index', compact('categories'));
15
+ }
16
+
17
+ public function store(Request $request)
18
+ {
19
+ $request->validate(['name' => 'required|string|max:100|unique:categories,name']);
20
+ Category::create(['name' => $request->name]);
21
+ return back()->with('success', 'Kategori berhasil ditambahkan.');
22
+ }
23
+
24
+ public function update(Request $request, Category $category)
25
+ {
26
+ $request->validate(['name' => 'required|string|max:100|unique:categories,name,' . $category->id]);
27
+ $category->update(['name' => $request->name]);
28
+ return back()->with('success', 'Kategori berhasil diperbarui.');
29
+ }
30
+
31
+ public function destroy(Category $category)
32
+ {
33
+ if ($category->events()->exists()) {
34
+ return back()->with('error', 'Kategori tidak bisa dihapus karena masih digunakan oleh event.');
35
+ }
36
+ $category->delete();
37
+ return back()->with('success', 'Kategori berhasil dihapus.');
38
+ }
39
+ }
app/Http/Controllers/Admin/DashboardController.php ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ namespace App\Http\Controllers\Admin;
4
+
5
+ use App\Http\Controllers\Controller;
6
+ use App\Models\Event;
7
+ use App\Models\Order;
8
+ use App\Models\User;
9
+
10
+ class DashboardController extends Controller
11
+ {
12
+ public function index()
13
+ {
14
+ $totalRevenue = Order::where('status', 'paid')->sum('total');
15
+ $totalOrders = Order::where('status', 'paid')->count();
16
+ $totalEvents = Event::count();
17
+ $totalUsers = User::count();
18
+ $pendingOrders = Order::where('status', 'pending')->count();
19
+
20
+ $recentOrders = Order::with('user', 'event')
21
+ ->latest()
22
+ ->take(10)
23
+ ->get();
24
+
25
+ return view('admin.dashboard', compact(
26
+ 'totalRevenue', 'totalOrders', 'totalEvents', 'totalUsers', 'pendingOrders', 'recentOrders'
27
+ ));
28
+ }
29
+ }
app/Http/Controllers/Admin/EventController.php ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ namespace App\Http\Controllers\Admin;
4
+
5
+ use App\Http\Controllers\Controller;
6
+ use App\Models\Event;
7
+ use Illuminate\Http\Request;
8
+
9
+ class EventController extends Controller
10
+ {
11
+ public function index(Request $request)
12
+ {
13
+ $query = Event::with('organizer', 'category');
14
+
15
+ if ($request->filled('status')) {
16
+ $query->where('status', $request->status);
17
+ }
18
+
19
+ $events = $query->latest()->paginate(15);
20
+
21
+ return view('admin.events.index', compact('events'));
22
+ }
23
+
24
+ public function updateStatus(Request $request, Event $event)
25
+ {
26
+ $request->validate(['status' => 'required|in:draft,published,ended,cancelled']);
27
+ $event->update(['status' => $request->status]);
28
+
29
+ return back()->with('success', "Status event diupdate ke \"{$request->status}\".");
30
+ }
31
+ }
app/Http/Controllers/Admin/ReportController.php ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ namespace App\Http\Controllers\Admin;
4
+
5
+ use App\Http\Controllers\Controller;
6
+ use App\Models\Event;
7
+ use App\Models\Order;
8
+ use Illuminate\Http\Request;
9
+ use Illuminate\Support\Facades\DB;
10
+
11
+ class ReportController extends Controller
12
+ {
13
+ public function index()
14
+ {
15
+ // Monthly revenue (last 6 months)
16
+ $monthlyRevenue = Order::where('status', 'paid')
17
+ ->where('paid_at', '>=', now()->subMonths(6))
18
+ ->select(
19
+ DB::raw("DATE_FORMAT(paid_at, '%Y-%m') as month"),
20
+ DB::raw('SUM(total) as revenue'),
21
+ DB::raw('COUNT(*) as order_count')
22
+ )
23
+ ->groupBy('month')
24
+ ->orderBy('month')
25
+ ->get();
26
+
27
+ // Top events by revenue
28
+ $topEvents = Event::withSum(['orders as revenue' => fn($q) => $q->where('status', 'paid')], 'total')
29
+ ->withCount(['orders as paid_orders' => fn($q) => $q->where('status', 'paid')])
30
+ ->orderByDesc('revenue')
31
+ ->take(10)
32
+ ->get();
33
+
34
+ // Order status breakdown
35
+ $statusBreakdown = Order::select('status', DB::raw('COUNT(*) as count'))
36
+ ->groupBy('status')
37
+ ->pluck('count', 'status');
38
+
39
+ return view('admin.reports', compact('monthlyRevenue', 'topEvents', 'statusBreakdown'));
40
+ }
41
+ }
app/Http/Controllers/Admin/UserController.php ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ namespace App\Http\Controllers\Admin;
4
+
5
+ use App\Http\Controllers\Controller;
6
+ use App\Models\User;
7
+ use Illuminate\Http\Request;
8
+
9
+ class UserController extends Controller
10
+ {
11
+ public function index(Request $request)
12
+ {
13
+ $query = User::query();
14
+
15
+ if ($request->filled('role')) {
16
+ $query->where('role', $request->role);
17
+ }
18
+ if ($request->filled('q')) {
19
+ $s = $request->q;
20
+ $query->where(fn($q) => $q->where('name', 'like', "%{$s}%")->orWhere('email', 'like', "%{$s}%"));
21
+ }
22
+
23
+ $users = $query->latest()->paginate(20);
24
+ return view('admin.users.index', compact('users'));
25
+ }
26
+
27
+ public function update(Request $request, User $user)
28
+ {
29
+ $request->validate(['role' => 'required|in:admin,organizer,customer']);
30
+ $user->update(['role' => $request->role]);
31
+ return back()->with('success', "Role user \"{$user->name}\" berhasil diupdate ke {$request->role}.");
32
+ }
33
+
34
+ public function destroy(User $user)
35
+ {
36
+ if ($user->id === auth()->id()) {
37
+ return back()->with('error', 'Tidak bisa menghapus akun sendiri.');
38
+ }
39
+ $user->delete();
40
+ return back()->with('success', 'User berhasil dihapus.');
41
+ }
42
+ }
app/Http/Controllers/Auth/AuthenticatedSessionController.php ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ namespace App\Http\Controllers\Auth;
4
+
5
+ use App\Http\Controllers\Controller;
6
+ use App\Http\Requests\Auth\LoginRequest;
7
+ use Illuminate\Http\RedirectResponse;
8
+ use Illuminate\Http\Request;
9
+ use Illuminate\Support\Facades\Auth;
10
+ use Illuminate\View\View;
11
+
12
+ class AuthenticatedSessionController extends Controller
13
+ {
14
+ /**
15
+ * Display the login view.
16
+ */
17
+ public function create(): View
18
+ {
19
+ return view('auth.login');
20
+ }
21
+
22
+ /**
23
+ * Handle an incoming authentication request.
24
+ */
25
+ public function store(LoginRequest $request): RedirectResponse
26
+ {
27
+ $request->authenticate();
28
+
29
+ $request->session()->regenerate();
30
+
31
+ return redirect()->intended(route('dashboard', absolute: false));
32
+ }
33
+
34
+ /**
35
+ * Destroy an authenticated session.
36
+ */
37
+ public function destroy(Request $request): RedirectResponse
38
+ {
39
+ Auth::guard('web')->logout();
40
+
41
+ $request->session()->invalidate();
42
+
43
+ $request->session()->regenerateToken();
44
+
45
+ return redirect('/');
46
+ }
47
+ }
app/Http/Controllers/Auth/ConfirmablePasswordController.php ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ namespace App\Http\Controllers\Auth;
4
+
5
+ use App\Http\Controllers\Controller;
6
+ use Illuminate\Http\RedirectResponse;
7
+ use Illuminate\Http\Request;
8
+ use Illuminate\Support\Facades\Auth;
9
+ use Illuminate\Validation\ValidationException;
10
+ use Illuminate\View\View;
11
+
12
+ class ConfirmablePasswordController extends Controller
13
+ {
14
+ /**
15
+ * Show the confirm password view.
16
+ */
17
+ public function show(): View
18
+ {
19
+ return view('auth.confirm-password');
20
+ }
21
+
22
+ /**
23
+ * Confirm the user's password.
24
+ */
25
+ public function store(Request $request): RedirectResponse
26
+ {
27
+ if (! Auth::guard('web')->validate([
28
+ 'email' => $request->user()->email,
29
+ 'password' => $request->password,
30
+ ])) {
31
+ throw ValidationException::withMessages([
32
+ 'password' => __('auth.password'),
33
+ ]);
34
+ }
35
+
36
+ $request->session()->put('auth.password_confirmed_at', time());
37
+
38
+ return redirect()->intended(route('dashboard', absolute: false));
39
+ }
40
+ }
app/Http/Controllers/Auth/EmailVerificationNotificationController.php ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ namespace App\Http\Controllers\Auth;
4
+
5
+ use App\Http\Controllers\Controller;
6
+ use Illuminate\Http\RedirectResponse;
7
+ use Illuminate\Http\Request;
8
+
9
+ class EmailVerificationNotificationController extends Controller
10
+ {
11
+ /**
12
+ * Send a new email verification notification.
13
+ */
14
+ public function store(Request $request): RedirectResponse
15
+ {
16
+ if ($request->user()->hasVerifiedEmail()) {
17
+ return redirect()->intended(route('dashboard', absolute: false));
18
+ }
19
+
20
+ $request->user()->sendEmailVerificationNotification();
21
+
22
+ return back()->with('status', 'verification-link-sent');
23
+ }
24
+ }
app/Http/Controllers/Auth/EmailVerificationPromptController.php ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ namespace App\Http\Controllers\Auth;
4
+
5
+ use App\Http\Controllers\Controller;
6
+ use Illuminate\Http\RedirectResponse;
7
+ use Illuminate\Http\Request;
8
+ use Illuminate\View\View;
9
+
10
+ class EmailVerificationPromptController extends Controller
11
+ {
12
+ /**
13
+ * Display the email verification prompt.
14
+ */
15
+ public function __invoke(Request $request): RedirectResponse|View
16
+ {
17
+ return $request->user()->hasVerifiedEmail()
18
+ ? redirect()->intended(route('dashboard', absolute: false))
19
+ : view('auth.verify-email');
20
+ }
21
+ }
app/Http/Controllers/Auth/NewPasswordController.php ADDED
@@ -0,0 +1,62 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ namespace App\Http\Controllers\Auth;
4
+
5
+ use App\Http\Controllers\Controller;
6
+ use App\Models\User;
7
+ use Illuminate\Auth\Events\PasswordReset;
8
+ use Illuminate\Http\RedirectResponse;
9
+ use Illuminate\Http\Request;
10
+ use Illuminate\Support\Facades\Hash;
11
+ use Illuminate\Support\Facades\Password;
12
+ use Illuminate\Support\Str;
13
+ use Illuminate\Validation\Rules;
14
+ use Illuminate\View\View;
15
+
16
+ class NewPasswordController extends Controller
17
+ {
18
+ /**
19
+ * Display the password reset view.
20
+ */
21
+ public function create(Request $request): View
22
+ {
23
+ return view('auth.reset-password', ['request' => $request]);
24
+ }
25
+
26
+ /**
27
+ * Handle an incoming new password request.
28
+ *
29
+ * @throws \Illuminate\Validation\ValidationException
30
+ */
31
+ public function store(Request $request): RedirectResponse
32
+ {
33
+ $request->validate([
34
+ 'token' => ['required'],
35
+ 'email' => ['required', 'email'],
36
+ 'password' => ['required', 'confirmed', Rules\Password::defaults()],
37
+ ]);
38
+
39
+ // Here we will attempt to reset the user's password. If it is successful we
40
+ // will update the password on an actual user model and persist it to the
41
+ // database. Otherwise we will parse the error and return the response.
42
+ $status = Password::reset(
43
+ $request->only('email', 'password', 'password_confirmation', 'token'),
44
+ function (User $user) use ($request) {
45
+ $user->forceFill([
46
+ 'password' => Hash::make($request->password),
47
+ 'remember_token' => Str::random(60),
48
+ ])->save();
49
+
50
+ event(new PasswordReset($user));
51
+ }
52
+ );
53
+
54
+ // If the password was successfully reset, we will redirect the user back to
55
+ // the application's home authenticated view. If there is an error we can
56
+ // redirect them back to where they came from with their error message.
57
+ return $status == Password::PASSWORD_RESET
58
+ ? redirect()->route('login')->with('status', __($status))
59
+ : back()->withInput($request->only('email'))
60
+ ->withErrors(['email' => __($status)]);
61
+ }
62
+ }
app/Http/Controllers/Auth/PasswordController.php ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ namespace App\Http\Controllers\Auth;
4
+
5
+ use App\Http\Controllers\Controller;
6
+ use Illuminate\Http\RedirectResponse;
7
+ use Illuminate\Http\Request;
8
+ use Illuminate\Support\Facades\Hash;
9
+ use Illuminate\Validation\Rules\Password;
10
+
11
+ class PasswordController extends Controller
12
+ {
13
+ /**
14
+ * Update the user's password.
15
+ */
16
+ public function update(Request $request): RedirectResponse
17
+ {
18
+ $validated = $request->validateWithBag('updatePassword', [
19
+ 'current_password' => ['required', 'current_password'],
20
+ 'password' => ['required', Password::defaults(), 'confirmed'],
21
+ ]);
22
+
23
+ $request->user()->update([
24
+ 'password' => Hash::make($validated['password']),
25
+ ]);
26
+
27
+ return back()->with('status', 'password-updated');
28
+ }
29
+ }
app/Http/Controllers/Auth/PasswordResetLinkController.php ADDED
@@ -0,0 +1,44 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ namespace App\Http\Controllers\Auth;
4
+
5
+ use App\Http\Controllers\Controller;
6
+ use Illuminate\Http\RedirectResponse;
7
+ use Illuminate\Http\Request;
8
+ use Illuminate\Support\Facades\Password;
9
+ use Illuminate\View\View;
10
+
11
+ class PasswordResetLinkController extends Controller
12
+ {
13
+ /**
14
+ * Display the password reset link request view.
15
+ */
16
+ public function create(): View
17
+ {
18
+ return view('auth.forgot-password');
19
+ }
20
+
21
+ /**
22
+ * Handle an incoming password reset link request.
23
+ *
24
+ * @throws \Illuminate\Validation\ValidationException
25
+ */
26
+ public function store(Request $request): RedirectResponse
27
+ {
28
+ $request->validate([
29
+ 'email' => ['required', 'email'],
30
+ ]);
31
+
32
+ // We will send the password reset link to this user. Once we have attempted
33
+ // to send the link, we will examine the response then see the message we
34
+ // need to show to the user. Finally, we'll send out a proper response.
35
+ $status = Password::sendResetLink(
36
+ $request->only('email')
37
+ );
38
+
39
+ return $status == Password::RESET_LINK_SENT
40
+ ? back()->with('status', __($status))
41
+ : back()->withInput($request->only('email'))
42
+ ->withErrors(['email' => __($status)]);
43
+ }
44
+ }
app/Http/Controllers/Auth/RegisteredUserController.php ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ namespace App\Http\Controllers\Auth;
4
+
5
+ use App\Http\Controllers\Controller;
6
+ use App\Models\User;
7
+ use Illuminate\Auth\Events\Registered;
8
+ use Illuminate\Http\RedirectResponse;
9
+ use Illuminate\Http\Request;
10
+ use Illuminate\Support\Facades\Auth;
11
+ use Illuminate\Support\Facades\Hash;
12
+ use Illuminate\Validation\Rules;
13
+ use Illuminate\View\View;
14
+
15
+ class RegisteredUserController extends Controller
16
+ {
17
+ /**
18
+ * Display the registration view.
19
+ */
20
+ public function create(): View
21
+ {
22
+ return view('auth.register');
23
+ }
24
+
25
+ /**
26
+ * Handle an incoming registration request.
27
+ *
28
+ * @throws \Illuminate\Validation\ValidationException
29
+ */
30
+ public function store(Request $request): RedirectResponse
31
+ {
32
+ $request->validate([
33
+ 'name' => ['required', 'string', 'max:255'],
34
+ 'email' => ['required', 'string', 'lowercase', 'email', 'max:255', 'unique:'.User::class],
35
+ 'password' => ['required', 'confirmed', Rules\Password::defaults()],
36
+ ]);
37
+
38
+ $user = User::create([
39
+ 'name' => $request->name,
40
+ 'email' => $request->email,
41
+ 'password' => Hash::make($request->password),
42
+ ]);
43
+
44
+ event(new Registered($user));
45
+
46
+ Auth::login($user);
47
+
48
+ return redirect(route('dashboard', absolute: false));
49
+ }
50
+ }
app/Http/Controllers/Auth/VerifyEmailController.php ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ namespace App\Http\Controllers\Auth;
4
+
5
+ use App\Http\Controllers\Controller;
6
+ use Illuminate\Auth\Events\Verified;
7
+ use Illuminate\Foundation\Auth\EmailVerificationRequest;
8
+ use Illuminate\Http\RedirectResponse;
9
+
10
+ class VerifyEmailController extends Controller
11
+ {
12
+ /**
13
+ * Mark the authenticated user's email address as verified.
14
+ */
15
+ public function __invoke(EmailVerificationRequest $request): RedirectResponse
16
+ {
17
+ if ($request->user()->hasVerifiedEmail()) {
18
+ return redirect()->intended(route('dashboard', absolute: false).'?verified=1');
19
+ }
20
+
21
+ if ($request->user()->markEmailAsVerified()) {
22
+ event(new Verified($request->user()));
23
+ }
24
+
25
+ return redirect()->intended(route('dashboard', absolute: false).'?verified=1');
26
+ }
27
+ }
app/Http/Controllers/CheckoutController.php ADDED
@@ -0,0 +1,83 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ namespace App\Http\Controllers;
4
+
5
+ use App\Models\Event;
6
+ use App\Models\Order;
7
+ use App\Services\OrderService;
8
+ use App\Services\TicketService;
9
+ use Illuminate\Http\Request;
10
+
11
+ class CheckoutController extends Controller
12
+ {
13
+ public function store(Request $request, Event $event, OrderService $orderService)
14
+ {
15
+ $request->validate([
16
+ 'tiers' => 'required|array|min:1',
17
+ 'tiers.*.tier_id' => 'required|exists:ticket_tiers,id',
18
+ 'tiers.*.qty' => 'required|integer|min:1',
19
+ 'promo_code' => 'nullable|string|max:32',
20
+ ]);
21
+
22
+ // Filter out zero-qty tiers
23
+ $items = collect($request->tiers)->filter(fn($t) => ($t['qty'] ?? 0) > 0)->values()->toArray();
24
+
25
+ if (empty($items)) {
26
+ return back()->with('error', 'Pilih minimal 1 tiket.');
27
+ }
28
+
29
+ try {
30
+ $order = $orderService->createOrder(auth()->user(), $event, $items, $request->promo_code);
31
+ return redirect()->route('checkout.summary', $order);
32
+ } catch (\Exception $e) {
33
+ return back()->with('error', $e->getMessage());
34
+ }
35
+ }
36
+
37
+ public function summary(Order $order)
38
+ {
39
+ abort_unless($order->user_id === auth()->id(), 403);
40
+ $order->load('event', 'items.ticketTier', 'user');
41
+
42
+ // Check if already expired
43
+ if ($order->isExpirable()) {
44
+ app(OrderService::class)->expireOrder($order);
45
+ return redirect()->route('events.show', $order->event->slug)
46
+ ->with('error', 'Order sudah expired. Silakan order ulang.');
47
+ }
48
+
49
+ return view('checkout.summary', compact('order'));
50
+ }
51
+
52
+ public function pay(Order $order, TicketService $ticketService)
53
+ {
54
+ abort_unless($order->user_id === auth()->id(), 403);
55
+
56
+ if (!$order->isPending()) {
57
+ return redirect()->route('orders.show', $order)
58
+ ->with('error', 'Order tidak dalam status pending.');
59
+ }
60
+
61
+ // Check expiry
62
+ if ($order->isExpirable()) {
63
+ app(OrderService::class)->expireOrder($order);
64
+ return redirect()->route('events.show', $order->event->slug)
65
+ ->with('error', 'Order sudah expired. Silakan order ulang.');
66
+ }
67
+
68
+ try {
69
+ $ticketService->processPayment($order, 'mock_gateway');
70
+ return redirect()->route('checkout.success', $order);
71
+ } catch (\Exception $e) {
72
+ return back()->with('error', $e->getMessage());
73
+ }
74
+ }
75
+
76
+ public function success(Order $order)
77
+ {
78
+ abort_unless($order->user_id === auth()->id(), 403);
79
+ $order->load('event', 'items.ticketTier', 'attendees');
80
+
81
+ return view('checkout.success', compact('order'));
82
+ }
83
+ }
app/Http/Controllers/Controller.php ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ namespace App\Http\Controllers;
4
+
5
+ abstract class Controller
6
+ {
7
+ //
8
+ }
app/Http/Controllers/DashboardController.php ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ namespace App\Http\Controllers;
4
+
5
+ use App\Models\Order;
6
+ use Illuminate\Http\Request;
7
+
8
+ class DashboardController extends Controller
9
+ {
10
+ public function index()
11
+ {
12
+ $user = auth()->user();
13
+
14
+ $activeTickets = $user->orders()
15
+ ->where('status', 'paid')
16
+ ->withCount('attendees')
17
+ ->get()
18
+ ->sum('attendees_count');
19
+
20
+ $totalOrders = $user->orders()->count();
21
+
22
+ $recentOrders = $user->orders()
23
+ ->with('event')
24
+ ->latest()
25
+ ->take(5)
26
+ ->get();
27
+
28
+ return view('dashboard', compact('activeTickets', 'totalOrders', 'recentOrders'));
29
+ }
30
+ }
app/Http/Controllers/EventController.php ADDED
@@ -0,0 +1,74 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ namespace App\Http\Controllers;
4
+
5
+ use App\Models\Category;
6
+ use App\Models\Event;
7
+ use Illuminate\Http\Request;
8
+
9
+ class EventController extends Controller
10
+ {
11
+ public function index(Request $request)
12
+ {
13
+ $query = Event::with('category', 'ticketTiers', 'organizer')->published();
14
+
15
+ // Search
16
+ if ($request->filled('q')) {
17
+ $s = $request->q;
18
+ $query->where(function ($q) use ($s) {
19
+ $q->where('title', 'like', "%{$s}%")
20
+ ->orWhere('city', 'like', "%{$s}%")
21
+ ->orWhere('venue_name', 'like', "%{$s}%")
22
+ ->orWhere('description', 'like', "%{$s}%");
23
+ });
24
+ }
25
+
26
+ // Filter: category
27
+ if ($request->filled('category')) {
28
+ $query->where('category_id', $request->category);
29
+ }
30
+
31
+ // Filter: city
32
+ if ($request->filled('city')) {
33
+ $query->where('city', $request->city);
34
+ }
35
+
36
+ // Filter: date
37
+ if ($request->filled('date_from')) {
38
+ $query->where('start_at', '>=', $request->date_from);
39
+ }
40
+ if ($request->filled('date_to')) {
41
+ $query->where('start_at', '<=', $request->date_to . ' 23:59:59');
42
+ }
43
+
44
+ // Filter: free only
45
+ if ($request->boolean('free')) {
46
+ $query->whereHas('ticketTiers', fn($q) => $q->where('price', 0));
47
+ }
48
+
49
+ // Sort
50
+ $sort = $request->input('sort', 'date');
51
+ $query = match ($sort) {
52
+ 'popular' => $query->withCount(['orders' => fn($q) => $q->where('status', 'paid')])->orderByDesc('orders_count'),
53
+ 'price' => $query->orderByRaw('(SELECT MIN(price) FROM ticket_tiers WHERE event_id = events.id)'),
54
+ default => $query->orderBy('start_at'),
55
+ };
56
+
57
+ $events = $query->paginate(12)->withQueryString();
58
+
59
+ $categories = Category::orderBy('name')->get();
60
+ $cities = Event::published()->whereNotNull('city')->distinct()->pluck('city');
61
+
62
+ return view('events.index', compact('events', 'categories', 'cities'));
63
+ }
64
+
65
+ public function show(string $slug)
66
+ {
67
+ $event = Event::where('slug', $slug)
68
+ ->published()
69
+ ->with(['category', 'organizer', 'ticketTiers'])
70
+ ->firstOrFail();
71
+
72
+ return view('events.show', compact('event'));
73
+ }
74
+ }
app/Http/Controllers/HomeController.php ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ namespace App\Http\Controllers;
4
+
5
+ use App\Models\Category;
6
+ use App\Models\Event;
7
+
8
+ class HomeController extends Controller
9
+ {
10
+ public function index()
11
+ {
12
+ $featuredEvents = Event::with('category', 'ticketTiers', 'organizer')
13
+ ->published()
14
+ ->where('start_at', '>=', now())
15
+ ->orderBy('start_at')
16
+ ->take(6)
17
+ ->get();
18
+
19
+ $categories = Category::withCount(['events' => fn($q) => $q->published()])
20
+ ->having('events_count', '>', 0)
21
+ ->orderBy('name')
22
+ ->get();
23
+
24
+ $totalEvents = Event::published()->count();
25
+
26
+ return view('welcome', compact('featuredEvents', 'categories', 'totalEvents'));
27
+ }
28
+ }
app/Http/Controllers/MyTicketsController.php ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ namespace App\Http\Controllers;
4
+
5
+ use App\Models\Attendee;
6
+ use App\Models\Order;
7
+ use Illuminate\Http\Request;
8
+
9
+ class MyTicketsController extends Controller
10
+ {
11
+ public function index()
12
+ {
13
+ $attendees = Attendee::whereHas('orderItem.order', function ($q) {
14
+ $q->where('user_id', auth()->id())->where('status', 'paid');
15
+ })
16
+ ->with('orderItem.ticketTier', 'orderItem.order.event')
17
+ ->latest()
18
+ ->paginate(12);
19
+
20
+ return view('tickets.index', compact('attendees'));
21
+ }
22
+
23
+ public function show(Attendee $attendee)
24
+ {
25
+ abort_unless($attendee->orderItem->order->user_id === auth()->id(), 403);
26
+ $attendee->load('orderItem.ticketTier', 'orderItem.order.event');
27
+
28
+ return view('tickets.show', compact('attendee'));
29
+ }
30
+
31
+ public function downloadPdf(Attendee $attendee)
32
+ {
33
+ abort_unless($attendee->orderItem->order->user_id === auth()->id(), 403);
34
+ $attendee->load('orderItem.ticketTier', 'orderItem.order.event');
35
+
36
+ $pdf = app('dompdf.wrapper')
37
+ ->loadView('tickets.pdf', compact('attendee'))
38
+ ->setPaper('a5', 'landscape');
39
+
40
+ return $pdf->download('ticket-' . $attendee->ticket_code . '.pdf');
41
+ }
42
+ }
app/Http/Controllers/OrdersController.php ADDED
@@ -0,0 +1,73 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ namespace App\Http\Controllers;
4
+
5
+ use App\Models\Order;
6
+ use App\Services\OrderService;
7
+ use Illuminate\Http\Request;
8
+ use Symfony\Component\HttpFoundation\StreamedResponse;
9
+
10
+ class OrdersController extends Controller
11
+ {
12
+ public function index(Request $request)
13
+ {
14
+ $query = Order::where('user_id', auth()->id())->with('event');
15
+
16
+ if ($request->filled('status')) {
17
+ $query->where('status', $request->status);
18
+ }
19
+
20
+ $orders = $query->latest()->paginate(10);
21
+
22
+ return view('orders.index', compact('orders'));
23
+ }
24
+
25
+ public function show(Order $order)
26
+ {
27
+ abort_unless($order->user_id === auth()->id(), 403);
28
+ $order->load('event', 'items.ticketTier', 'attendees');
29
+
30
+ return view('orders.show', compact('order'));
31
+ }
32
+
33
+ public function cancel(Order $order, OrderService $orderService)
34
+ {
35
+ abort_unless($order->user_id === auth()->id(), 403);
36
+
37
+ if (!$order->isPaid()) {
38
+ return back()->with('error', 'Hanya order yang sudah dibayar yang bisa dibatalkan.');
39
+ }
40
+
41
+ $success = $orderService->cancelOrder($order);
42
+
43
+ if (!$success) {
44
+ return back()->with('error', 'Tidak bisa membatalkan order. Batas waktu refund sudah lewat (minimal 24 jam sebelum event).');
45
+ }
46
+
47
+ return back()->with('success', 'Order berhasil dibatalkan dan akan direfund.');
48
+ }
49
+
50
+ public function exportCsv(): StreamedResponse
51
+ {
52
+ $orders = Order::where('user_id', auth()->id())
53
+ ->with('event')
54
+ ->latest()
55
+ ->get();
56
+
57
+ return response()->streamDownload(function () use ($orders) {
58
+ $handle = fopen('php://output', 'w');
59
+ fputcsv($handle, ['Order Code', 'Event', 'Total', 'Status', 'Tanggal']);
60
+
61
+ foreach ($orders as $order) {
62
+ fputcsv($handle, [
63
+ $order->order_code,
64
+ $order->event->title,
65
+ 'Rp ' . number_format($order->total, 0, ',', '.'),
66
+ $order->status,
67
+ $order->created_at->format('d/m/Y H:i'),
68
+ ]);
69
+ }
70
+ fclose($handle);
71
+ }, 'orders-' . now()->format('Ymd') . '.csv', ['Content-Type' => 'text/csv']);
72
+ }
73
+ }
app/Http/Controllers/Organizer/DashboardController.php ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ namespace App\Http\Controllers\Organizer;
4
+
5
+ use App\Http\Controllers\Controller;
6
+ use App\Models\Event;
7
+ use App\Models\Order;
8
+ use Illuminate\Http\Request;
9
+
10
+ class DashboardController extends Controller
11
+ {
12
+ public function index()
13
+ {
14
+ $user = auth()->user();
15
+ $events = $user->events()->withCount('orders')->get();
16
+
17
+ $totalRevenue = Order::whereIn('event_id', $events->pluck('id'))
18
+ ->where('status', 'paid')->sum('total');
19
+ $totalOrders = Order::whereIn('event_id', $events->pluck('id'))
20
+ ->where('status', 'paid')->count();
21
+ $totalSold = $events->sum(fn ($e) => $e->ticketTiers->sum('sold_count'));
22
+ $activeEvents = $events->where('status', 'published')->count();
23
+
24
+ $recentOrders = Order::whereIn('event_id', $events->pluck('id'))
25
+ ->with('user', 'event')
26
+ ->latest()
27
+ ->take(10)
28
+ ->get();
29
+
30
+ return view('organizer.dashboard', compact(
31
+ 'events', 'totalRevenue', 'totalOrders', 'totalSold', 'activeEvents', 'recentOrders'
32
+ ));
33
+ }
34
+ }
app/Http/Controllers/Organizer/EventController.php ADDED
@@ -0,0 +1,123 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ namespace App\Http\Controllers\Organizer;
4
+
5
+ use App\Http\Controllers\Controller;
6
+ use App\Models\Category;
7
+ use App\Models\Event;
8
+ use Illuminate\Http\Request;
9
+ use Illuminate\Support\Facades\Storage;
10
+
11
+ class EventController extends Controller
12
+ {
13
+ public function index()
14
+ {
15
+ $events = auth()->user()->events()
16
+ ->with('category', 'ticketTiers')
17
+ ->latest()
18
+ ->paginate(10);
19
+
20
+ return view('organizer.events.index', compact('events'));
21
+ }
22
+
23
+ public function create()
24
+ {
25
+ $categories = Category::orderBy('name')->get();
26
+ return view('organizer.events.create', compact('categories'));
27
+ }
28
+
29
+ public function store(Request $request)
30
+ {
31
+ $validated = $request->validate([
32
+ 'title' => 'required|string|max:255',
33
+ 'category_id' => 'required|exists:categories,id',
34
+ 'description' => 'required|string',
35
+ 'venue_name' => 'nullable|string|max:255',
36
+ 'venue_address' => 'nullable|string|max:500',
37
+ 'city' => 'nullable|string|max:100',
38
+ 'is_online' => 'boolean',
39
+ 'start_at' => 'required|date|after:now',
40
+ 'end_at' => 'required|date|after:start_at',
41
+ 'status' => 'required|in:draft,published',
42
+ 'banner' => 'nullable|image|max:2048',
43
+ 'terms' => 'nullable|string',
44
+ 'refund_policy' => 'nullable|string',
45
+ ]);
46
+
47
+ if ($request->hasFile('banner')) {
48
+ $validated['banner'] = $request->file('banner')->store('banners', 'public');
49
+ }
50
+
51
+ $validated['organizer_id'] = auth()->id();
52
+ $validated['is_online'] = $request->boolean('is_online');
53
+
54
+ $event = Event::create($validated);
55
+
56
+ return redirect()->route('organizer.events.edit', $event)
57
+ ->with('success', 'Event berhasil dibuat! Sekarang tambahkan tiket tier.');
58
+ }
59
+
60
+ public function edit(Event $event)
61
+ {
62
+ $this->authorizeOrganizer($event);
63
+ $categories = Category::orderBy('name')->get();
64
+ $event->load('ticketTiers');
65
+
66
+ return view('organizer.events.edit', compact('event', 'categories'));
67
+ }
68
+
69
+ public function update(Request $request, Event $event)
70
+ {
71
+ $this->authorizeOrganizer($event);
72
+
73
+ $validated = $request->validate([
74
+ 'title' => 'required|string|max:255',
75
+ 'category_id' => 'required|exists:categories,id',
76
+ 'description' => 'required|string',
77
+ 'venue_name' => 'nullable|string|max:255',
78
+ 'venue_address' => 'nullable|string|max:500',
79
+ 'city' => 'nullable|string|max:100',
80
+ 'is_online' => 'boolean',
81
+ 'start_at' => 'required|date',
82
+ 'end_at' => 'required|date|after:start_at',
83
+ 'status' => 'required|in:draft,published,ended,cancelled',
84
+ 'banner' => 'nullable|image|max:2048',
85
+ 'terms' => 'nullable|string',
86
+ 'refund_policy' => 'nullable|string',
87
+ ]);
88
+
89
+ if ($request->hasFile('banner')) {
90
+ if ($event->banner) Storage::disk('public')->delete($event->banner);
91
+ $validated['banner'] = $request->file('banner')->store('banners', 'public');
92
+ }
93
+
94
+ $validated['is_online'] = $request->boolean('is_online');
95
+
96
+ $event->update($validated);
97
+
98
+ return redirect()->route('organizer.events.index')
99
+ ->with('success', 'Event berhasil diperbarui!');
100
+ }
101
+
102
+ public function destroy(Event $event)
103
+ {
104
+ $this->authorizeOrganizer($event);
105
+
106
+ if ($event->orders()->where('status', 'paid')->exists()) {
107
+ return back()->with('error', 'Event tidak bisa dihapus karena sudah memiliki order yang dibayar.');
108
+ }
109
+
110
+ if ($event->banner) Storage::disk('public')->delete($event->banner);
111
+ $event->delete();
112
+
113
+ return redirect()->route('organizer.events.index')
114
+ ->with('success', 'Event berhasil dihapus.');
115
+ }
116
+
117
+ private function authorizeOrganizer(Event $event): void
118
+ {
119
+ if ($event->organizer_id !== auth()->id() && !auth()->user()->isAdmin()) {
120
+ abort(403, 'Anda tidak memiliki akses ke event ini.');
121
+ }
122
+ }
123
+ }
app/Http/Controllers/Organizer/OrderController.php ADDED
@@ -0,0 +1,71 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ namespace App\Http\Controllers\Organizer;
4
+
5
+ use App\Http\Controllers\Controller;
6
+ use App\Models\Attendee;
7
+ use App\Models\Event;
8
+ use App\Models\Order;
9
+ use Illuminate\Http\Request;
10
+ use Symfony\Component\HttpFoundation\StreamedResponse;
11
+
12
+ class OrderController extends Controller
13
+ {
14
+ public function index(Event $event)
15
+ {
16
+ $this->authorizeOrganizer($event);
17
+
18
+ $orders = Order::where('event_id', $event->id)
19
+ ->with('user', 'items.ticketTier')
20
+ ->latest()
21
+ ->paginate(20);
22
+
23
+ return view('organizer.orders.index', compact('event', 'orders'));
24
+ }
25
+
26
+ public function show(Event $event, Order $order)
27
+ {
28
+ $this->authorizeOrganizer($event);
29
+ abort_unless($order->event_id === $event->id, 404);
30
+
31
+ $order->load('user', 'items.ticketTier', 'attendees');
32
+
33
+ return view('organizer.orders.show', compact('event', 'order'));
34
+ }
35
+
36
+ public function exportCsv(Event $event): StreamedResponse
37
+ {
38
+ $this->authorizeOrganizer($event);
39
+
40
+ $attendees = Attendee::whereHas('orderItem.order', function ($q) use ($event) {
41
+ $q->where('event_id', $event->id)->where('status', 'paid');
42
+ })->with('orderItem.ticketTier', 'orderItem.order')->get();
43
+
44
+ $filename = 'attendees-' . $event->slug . '-' . now()->format('Ymd') . '.csv';
45
+
46
+ return response()->streamDownload(function () use ($attendees) {
47
+ $handle = fopen('php://output', 'w');
48
+ fputcsv($handle, ['No', 'Nama', 'Email', 'Telepon', 'Ticket Tier', 'Ticket Code', 'Checked In']);
49
+
50
+ foreach ($attendees as $i => $att) {
51
+ fputcsv($handle, [
52
+ $i + 1,
53
+ $att->full_name,
54
+ $att->email,
55
+ $att->phone ?? '-',
56
+ $att->orderItem->ticketTier->name,
57
+ $att->ticket_code,
58
+ $att->checkin_at ? $att->checkin_at->format('d/m/Y H:i') : 'Belum',
59
+ ]);
60
+ }
61
+ fclose($handle);
62
+ }, $filename, ['Content-Type' => 'text/csv']);
63
+ }
64
+
65
+ private function authorizeOrganizer(Event $event): void
66
+ {
67
+ if ($event->organizer_id !== auth()->id() && !auth()->user()->isAdmin()) {
68
+ abort(403);
69
+ }
70
+ }
71
+ }
app/Http/Controllers/Organizer/ScannerController.php ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ namespace App\Http\Controllers\Organizer;
4
+
5
+ use App\Http\Controllers\Controller;
6
+ use App\Services\TicketService;
7
+ use Illuminate\Http\Request;
8
+
9
+ class ScannerController extends Controller
10
+ {
11
+ public function index()
12
+ {
13
+ return view('organizer.scanner');
14
+ }
15
+
16
+ public function checkIn(Request $request, TicketService $ticketService)
17
+ {
18
+ $request->validate(['ticket_code' => 'required|string|max:16']);
19
+
20
+ $result = $ticketService->checkIn($request->ticket_code);
21
+
22
+ if ($request->wantsJson()) {
23
+ return response()->json($result, $result['success'] ? 200 : 422);
24
+ }
25
+
26
+ return back()->with(
27
+ $result['success'] ? 'scan_success' : 'scan_error',
28
+ $result['message']
29
+ )->with('scan_attendee', $result['attendee']);
30
+ }
31
+ }
app/Http/Controllers/Organizer/TicketTierController.php ADDED
@@ -0,0 +1,74 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ namespace App\Http\Controllers\Organizer;
4
+
5
+ use App\Http\Controllers\Controller;
6
+ use App\Models\Event;
7
+ use App\Models\TicketTier;
8
+ use Illuminate\Http\Request;
9
+
10
+ class TicketTierController extends Controller
11
+ {
12
+ public function store(Request $request, Event $event)
13
+ {
14
+ $this->authorizeOrganizer($event);
15
+
16
+ $validated = $request->validate([
17
+ 'name' => 'required|string|max:255',
18
+ 'price' => 'required|numeric|min:0',
19
+ 'quota' => 'required|integer|min:1',
20
+ 'max_per_order' => 'required|integer|min:1|max:50',
21
+ 'is_refundable' => 'boolean',
22
+ 'sales_start' => 'nullable|date',
23
+ 'sales_end' => 'nullable|date|after:sales_start',
24
+ ]);
25
+
26
+ $validated['is_refundable'] = $request->boolean('is_refundable');
27
+ $validated['event_id'] = $event->id;
28
+
29
+ TicketTier::create($validated);
30
+
31
+ return back()->with('success', 'Ticket tier berhasil ditambahkan!');
32
+ }
33
+
34
+ public function update(Request $request, Event $event, TicketTier $tier)
35
+ {
36
+ $this->authorizeOrganizer($event);
37
+ abort_unless($tier->event_id === $event->id, 404);
38
+
39
+ $validated = $request->validate([
40
+ 'name' => 'required|string|max:255',
41
+ 'price' => 'required|numeric|min:0',
42
+ 'quota' => 'required|integer|min:' . $tier->sold_count,
43
+ 'max_per_order' => 'required|integer|min:1|max:50',
44
+ 'is_refundable' => 'boolean',
45
+ 'sales_start' => 'nullable|date',
46
+ 'sales_end' => 'nullable|date|after:sales_start',
47
+ ]);
48
+
49
+ $validated['is_refundable'] = $request->boolean('is_refundable');
50
+ $tier->update($validated);
51
+
52
+ return back()->with('success', 'Ticket tier berhasil diperbarui!');
53
+ }
54
+
55
+ public function destroy(Event $event, TicketTier $tier)
56
+ {
57
+ $this->authorizeOrganizer($event);
58
+ abort_unless($tier->event_id === $event->id, 404);
59
+
60
+ if ($tier->sold_count > 0) {
61
+ return back()->with('error', 'Tidak bisa menghapus tier yang sudah terjual.');
62
+ }
63
+
64
+ $tier->delete();
65
+ return back()->with('success', 'Ticket tier berhasil dihapus.');
66
+ }
67
+
68
+ private function authorizeOrganizer(Event $event): void
69
+ {
70
+ if ($event->organizer_id !== auth()->id() && !auth()->user()->isAdmin()) {
71
+ abort(403);
72
+ }
73
+ }
74
+ }
app/Http/Controllers/ProfileController.php ADDED
@@ -0,0 +1,60 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ namespace App\Http\Controllers;
4
+
5
+ use App\Http\Requests\ProfileUpdateRequest;
6
+ use Illuminate\Http\RedirectResponse;
7
+ use Illuminate\Http\Request;
8
+ use Illuminate\Support\Facades\Auth;
9
+ use Illuminate\Support\Facades\Redirect;
10
+ use Illuminate\View\View;
11
+
12
+ class ProfileController extends Controller
13
+ {
14
+ /**
15
+ * Display the user's profile form.
16
+ */
17
+ public function edit(Request $request): View
18
+ {
19
+ return view('profile.edit', [
20
+ 'user' => $request->user(),
21
+ ]);
22
+ }
23
+
24
+ /**
25
+ * Update the user's profile information.
26
+ */
27
+ public function update(ProfileUpdateRequest $request): RedirectResponse
28
+ {
29
+ $request->user()->fill($request->validated());
30
+
31
+ if ($request->user()->isDirty('email')) {
32
+ $request->user()->email_verified_at = null;
33
+ }
34
+
35
+ $request->user()->save();
36
+
37
+ return Redirect::route('profile.edit')->with('status', 'profile-updated');
38
+ }
39
+
40
+ /**
41
+ * Delete the user's account.
42
+ */
43
+ public function destroy(Request $request): RedirectResponse
44
+ {
45
+ $request->validateWithBag('userDeletion', [
46
+ 'password' => ['required', 'current_password'],
47
+ ]);
48
+
49
+ $user = $request->user();
50
+
51
+ Auth::logout();
52
+
53
+ $user->delete();
54
+
55
+ $request->session()->invalidate();
56
+ $request->session()->regenerateToken();
57
+
58
+ return Redirect::to('/');
59
+ }
60
+ }
app/Http/Middleware/RoleMiddleware.php ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ namespace App\Http\Middleware;
4
+
5
+ use Closure;
6
+ use Illuminate\Http\Request;
7
+ use Symfony\Component\HttpFoundation\Response;
8
+
9
+ class RoleMiddleware
10
+ {
11
+ public function handle(Request $request, Closure $next, string ...$roles): Response
12
+ {
13
+ if (!auth()->check()) {
14
+ return redirect()->route('login');
15
+ }
16
+
17
+ if (!in_array(auth()->user()->role, $roles)) {
18
+ abort(403, 'Anda tidak memiliki akses ke halaman ini.');
19
+ }
20
+
21
+ return $next($request);
22
+ }
23
+ }
app/Http/Requests/Auth/LoginRequest.php ADDED
@@ -0,0 +1,85 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ namespace App\Http\Requests\Auth;
4
+
5
+ use Illuminate\Auth\Events\Lockout;
6
+ use Illuminate\Foundation\Http\FormRequest;
7
+ use Illuminate\Support\Facades\Auth;
8
+ use Illuminate\Support\Facades\RateLimiter;
9
+ use Illuminate\Support\Str;
10
+ use Illuminate\Validation\ValidationException;
11
+
12
+ class LoginRequest extends FormRequest
13
+ {
14
+ /**
15
+ * Determine if the user is authorized to make this request.
16
+ */
17
+ public function authorize(): bool
18
+ {
19
+ return true;
20
+ }
21
+
22
+ /**
23
+ * Get the validation rules that apply to the request.
24
+ *
25
+ * @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
26
+ */
27
+ public function rules(): array
28
+ {
29
+ return [
30
+ 'email' => ['required', 'string', 'email'],
31
+ 'password' => ['required', 'string'],
32
+ ];
33
+ }
34
+
35
+ /**
36
+ * Attempt to authenticate the request's credentials.
37
+ *
38
+ * @throws \Illuminate\Validation\ValidationException
39
+ */
40
+ public function authenticate(): void
41
+ {
42
+ $this->ensureIsNotRateLimited();
43
+
44
+ if (! Auth::attempt($this->only('email', 'password'), $this->boolean('remember'))) {
45
+ RateLimiter::hit($this->throttleKey());
46
+
47
+ throw ValidationException::withMessages([
48
+ 'email' => trans('auth.failed'),
49
+ ]);
50
+ }
51
+
52
+ RateLimiter::clear($this->throttleKey());
53
+ }
54
+
55
+ /**
56
+ * Ensure the login request is not rate limited.
57
+ *
58
+ * @throws \Illuminate\Validation\ValidationException
59
+ */
60
+ public function ensureIsNotRateLimited(): void
61
+ {
62
+ if (! RateLimiter::tooManyAttempts($this->throttleKey(), 5)) {
63
+ return;
64
+ }
65
+
66
+ event(new Lockout($this));
67
+
68
+ $seconds = RateLimiter::availableIn($this->throttleKey());
69
+
70
+ throw ValidationException::withMessages([
71
+ 'email' => trans('auth.throttle', [
72
+ 'seconds' => $seconds,
73
+ 'minutes' => ceil($seconds / 60),
74
+ ]),
75
+ ]);
76
+ }
77
+
78
+ /**
79
+ * Get the rate limiting throttle key for the request.
80
+ */
81
+ public function throttleKey(): string
82
+ {
83
+ return Str::transliterate(Str::lower($this->string('email')).'|'.$this->ip());
84
+ }
85
+ }
app/Http/Requests/ProfileUpdateRequest.php ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ namespace App\Http\Requests;
4
+
5
+ use App\Models\User;
6
+ use Illuminate\Foundation\Http\FormRequest;
7
+ use Illuminate\Validation\Rule;
8
+
9
+ class ProfileUpdateRequest extends FormRequest
10
+ {
11
+ /**
12
+ * Get the validation rules that apply to the request.
13
+ *
14
+ * @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
15
+ */
16
+ public function rules(): array
17
+ {
18
+ return [
19
+ 'name' => ['required', 'string', 'max:255'],
20
+ 'email' => [
21
+ 'required',
22
+ 'string',
23
+ 'lowercase',
24
+ 'email',
25
+ 'max:255',
26
+ Rule::unique(User::class)->ignore($this->user()->id),
27
+ ],
28
+ ];
29
+ }
30
+ }
app/Models/Attendee.php ADDED
@@ -0,0 +1,57 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ namespace App\Models;
4
+
5
+ use Illuminate\Database\Eloquent\Model;
6
+ use Illuminate\Support\Str;
7
+
8
+ class Attendee extends Model
9
+ {
10
+ protected $fillable = ['order_item_id', 'full_name', 'email', 'phone', 'ticket_code', 'checkin_at'];
11
+
12
+ protected function casts(): array
13
+ {
14
+ return [
15
+ 'checkin_at' => 'datetime',
16
+ ];
17
+ }
18
+
19
+ protected static function booted(): void
20
+ {
21
+ static::creating(function (Attendee $attendee) {
22
+ if (empty($attendee->ticket_code)) {
23
+ do {
24
+ $code = 'TKT-' . strtoupper(Str::random(8));
25
+ } while (static::where('ticket_code', $code)->exists());
26
+ $attendee->ticket_code = $code;
27
+ }
28
+ });
29
+ }
30
+
31
+ // ── Relationships ────────────────────────────────────
32
+
33
+ public function orderItem()
34
+ {
35
+ return $this->belongsTo(OrderItem::class);
36
+ }
37
+
38
+ public function order()
39
+ {
40
+ return $this->hasOneThrough(Order::class, OrderItem::class, 'id', 'id', 'order_item_id', 'order_id');
41
+ }
42
+
43
+ // ── Helpers ──────────────────────────────────────────
44
+
45
+ public function isCheckedIn(): bool
46
+ {
47
+ return $this->checkin_at !== null;
48
+ }
49
+
50
+ public function checkIn(): bool
51
+ {
52
+ if ($this->isCheckedIn()) return false;
53
+
54
+ $this->update(['checkin_at' => now()]);
55
+ return true;
56
+ }
57
+ }
app/Models/Category.php ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ namespace App\Models;
4
+
5
+ use Illuminate\Database\Eloquent\Model;
6
+ use Illuminate\Support\Str;
7
+
8
+ class Category extends Model
9
+ {
10
+ protected $fillable = ['name', 'slug'];
11
+
12
+ protected static function booted(): void
13
+ {
14
+ static::creating(function (Category $cat) {
15
+ if (empty($cat->slug)) {
16
+ $cat->slug = Str::slug($cat->name);
17
+ }
18
+ });
19
+ }
20
+
21
+ // ── Relationships ────────────────────────────────────
22
+
23
+ public function events()
24
+ {
25
+ return $this->hasMany(Event::class);
26
+ }
27
+ }
app/Models/Event.php ADDED
@@ -0,0 +1,100 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ namespace App\Models;
4
+
5
+ use Illuminate\Database\Eloquent\Model;
6
+ use Illuminate\Support\Str;
7
+
8
+ class Event extends Model
9
+ {
10
+ protected $fillable = [
11
+ 'organizer_id', 'category_id', 'title', 'slug', 'description', 'banner',
12
+ 'venue_name', 'venue_address', 'city', 'is_online',
13
+ 'start_at', 'end_at', 'status', 'terms', 'refund_policy',
14
+ ];
15
+
16
+ protected function casts(): array
17
+ {
18
+ return [
19
+ 'start_at' => 'datetime',
20
+ 'end_at' => 'datetime',
21
+ 'is_online' => 'boolean',
22
+ ];
23
+ }
24
+
25
+ protected static function booted(): void
26
+ {
27
+ static::creating(function (Event $event) {
28
+ if (empty($event->slug)) {
29
+ $base = Str::slug($event->title);
30
+ $slug = $base;
31
+ $i = 1;
32
+ while (static::where('slug', $slug)->exists()) {
33
+ $slug = $base . '-' . $i++;
34
+ }
35
+ $event->slug = $slug;
36
+ }
37
+ });
38
+ }
39
+
40
+ // ── Relationships ────────────────────────────────────
41
+
42
+ public function organizer()
43
+ {
44
+ return $this->belongsTo(User::class, 'organizer_id');
45
+ }
46
+
47
+ public function category()
48
+ {
49
+ return $this->belongsTo(Category::class);
50
+ }
51
+
52
+ public function ticketTiers()
53
+ {
54
+ return $this->hasMany(TicketTier::class);
55
+ }
56
+
57
+ public function orders()
58
+ {
59
+ return $this->hasMany(Order::class);
60
+ }
61
+
62
+ // ── Scopes ───────────────────────────────────────────
63
+
64
+ public function scopePublished($query)
65
+ {
66
+ return $query->where('status', 'published');
67
+ }
68
+
69
+ public function scopeUpcoming($query)
70
+ {
71
+ return $query->published()->where('start_at', '>=', now());
72
+ }
73
+
74
+ // ── Helpers ──────────────────────────────────────────
75
+
76
+ public function getMinPrice(): float
77
+ {
78
+ return $this->ticketTiers->min('price') ?? 0;
79
+ }
80
+
81
+ public function getTotalQuota(): int
82
+ {
83
+ return $this->ticketTiers->sum('quota');
84
+ }
85
+
86
+ public function getTotalSold(): int
87
+ {
88
+ return $this->ticketTiers->sum('sold_count');
89
+ }
90
+
91
+ public function getAvailableStock(): int
92
+ {
93
+ return $this->getTotalQuota() - $this->getTotalSold();
94
+ }
95
+
96
+ public function isPublished(): bool
97
+ {
98
+ return $this->status === 'published';
99
+ }
100
+ }
app/Models/Order.php ADDED
@@ -0,0 +1,70 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ namespace App\Models;
4
+
5
+ use Illuminate\Database\Eloquent\Model;
6
+ use Illuminate\Support\Str;
7
+
8
+ class Order extends Model
9
+ {
10
+ protected $fillable = [
11
+ 'user_id', 'event_id', 'order_code', 'subtotal', 'fee', 'tax',
12
+ 'discount', 'total', 'status', 'payment_method', 'paid_at', 'expires_at',
13
+ ];
14
+
15
+ protected function casts(): array
16
+ {
17
+ return [
18
+ 'subtotal' => 'decimal:2',
19
+ 'fee' => 'decimal:2',
20
+ 'tax' => 'decimal:2',
21
+ 'discount' => 'decimal:2',
22
+ 'total' => 'decimal:2',
23
+ 'paid_at' => 'datetime',
24
+ 'expires_at' => 'datetime',
25
+ ];
26
+ }
27
+
28
+ protected static function booted(): void
29
+ {
30
+ static::creating(function (Order $order) {
31
+ if (empty($order->order_code)) {
32
+ $order->order_code = 'ORD-' . now()->format('Ymd') . '-' . strtoupper(Str::random(5));
33
+ }
34
+ });
35
+ }
36
+
37
+ // ── Relationships ────────────────────────────────────
38
+
39
+ public function user()
40
+ {
41
+ return $this->belongsTo(User::class);
42
+ }
43
+
44
+ public function event()
45
+ {
46
+ return $this->belongsTo(Event::class);
47
+ }
48
+
49
+ public function items()
50
+ {
51
+ return $this->hasMany(OrderItem::class);
52
+ }
53
+
54
+ public function attendees()
55
+ {
56
+ return $this->hasManyThrough(Attendee::class, OrderItem::class);
57
+ }
58
+
59
+ // ── Status Helpers ───────────────────────────────────
60
+
61
+ public function isPending(): bool { return $this->status === 'pending'; }
62
+ public function isPaid(): bool { return $this->status === 'paid'; }
63
+ public function isExpired(): bool { return $this->status === 'expired'; }
64
+ public function isRefunded(): bool { return $this->status === 'refunded'; }
65
+
66
+ public function isExpirable(): bool
67
+ {
68
+ return $this->isPending() && $this->expires_at && now()->gte($this->expires_at);
69
+ }
70
+ }
app/Models/OrderItem.php ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ namespace App\Models;
4
+
5
+ use Illuminate\Database\Eloquent\Model;
6
+
7
+ class OrderItem extends Model
8
+ {
9
+ protected $fillable = ['order_id', 'ticket_tier_id', 'qty', 'unit_price', 'total'];
10
+
11
+ protected function casts(): array
12
+ {
13
+ return [
14
+ 'unit_price' => 'decimal:2',
15
+ 'total' => 'decimal:2',
16
+ ];
17
+ }
18
+
19
+ // ── Relationships ────────────────────────────────────
20
+
21
+ public function order()
22
+ {
23
+ return $this->belongsTo(Order::class);
24
+ }
25
+
26
+ public function ticketTier()
27
+ {
28
+ return $this->belongsTo(TicketTier::class);
29
+ }
30
+
31
+ public function attendees()
32
+ {
33
+ return $this->hasMany(Attendee::class);
34
+ }
35
+ }
app/Models/PromoCode.php ADDED
@@ -0,0 +1,43 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ namespace App\Models;
4
+
5
+ use Illuminate\Database\Eloquent\Model;
6
+
7
+ class PromoCode extends Model
8
+ {
9
+ protected $fillable = ['code', 'type', 'value', 'start_at', 'end_at', 'usage_limit', 'used_count'];
10
+
11
+ protected function casts(): array
12
+ {
13
+ return [
14
+ 'value' => 'decimal:2',
15
+ 'start_at' => 'datetime',
16
+ 'end_at' => 'datetime',
17
+ ];
18
+ }
19
+
20
+ // ── Helpers ──────────────────────────────────────────
21
+
22
+ public function isValid(): bool
23
+ {
24
+ $now = now();
25
+
26
+ if ($this->start_at && $now->lt($this->start_at)) return false;
27
+ if ($this->end_at && $now->gt($this->end_at)) return false;
28
+ if ($this->usage_limit !== null && $this->used_count >= $this->usage_limit) return false;
29
+
30
+ return true;
31
+ }
32
+
33
+ public function applyDiscount(float $subtotal): float
34
+ {
35
+ if (!$this->isValid()) return 0;
36
+
37
+ if ($this->type === 'percent') {
38
+ return round($subtotal * ($this->value / 100), 2);
39
+ }
40
+
41
+ return min($this->value, $subtotal);
42
+ }
43
+ }
app/Models/TicketTier.php ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ namespace App\Models;
4
+
5
+ use Illuminate\Database\Eloquent\Model;
6
+
7
+ class TicketTier extends Model
8
+ {
9
+ protected $fillable = [
10
+ 'event_id', 'name', 'price', 'quota', 'sold_count',
11
+ 'sales_start', 'sales_end', 'max_per_order', 'is_refundable',
12
+ ];
13
+
14
+ protected function casts(): array
15
+ {
16
+ return [
17
+ 'price' => 'decimal:2',
18
+ 'sales_start' => 'datetime',
19
+ 'sales_end' => 'datetime',
20
+ 'is_refundable' => 'boolean',
21
+ ];
22
+ }
23
+
24
+ // ── Relationships ────────────────────────────────────
25
+
26
+ public function event()
27
+ {
28
+ return $this->belongsTo(Event::class);
29
+ }
30
+
31
+ public function orderItems()
32
+ {
33
+ return $this->hasMany(OrderItem::class);
34
+ }
35
+
36
+ // ── Helpers ──────────────────────────────────────────
37
+
38
+ public function availableStock(): int
39
+ {
40
+ return max(0, $this->quota - $this->sold_count);
41
+ }
42
+
43
+ public function isOnSale(): bool
44
+ {
45
+ $now = now();
46
+
47
+ if ($this->sales_start && $now->lt($this->sales_start)) return false;
48
+ if ($this->sales_end && $now->gt($this->sales_end)) return false;
49
+ if ($this->availableStock() <= 0) return false;
50
+
51
+ return true;
52
+ }
53
+ }
app/Models/User.php ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ namespace App\Models;
4
+
5
+ use Illuminate\Database\Eloquent\Factories\HasFactory;
6
+ use Illuminate\Foundation\Auth\User as Authenticatable;
7
+ use Illuminate\Notifications\Notifiable;
8
+
9
+ class User extends Authenticatable
10
+ {
11
+ use HasFactory, Notifiable;
12
+
13
+ protected $fillable = ['name', 'email', 'password', 'role', 'phone'];
14
+
15
+ protected $hidden = ['password', 'remember_token'];
16
+
17
+ protected function casts(): array
18
+ {
19
+ return [
20
+ 'email_verified_at' => 'datetime',
21
+ 'password' => 'hashed',
22
+ ];
23
+ }
24
+
25
+ // ── Role Helpers ─────────────────────────────────────
26
+
27
+ public function isAdmin(): bool { return $this->role === 'admin'; }
28
+ public function isOrganizer(): bool { return $this->role === 'organizer'; }
29
+ public function isCustomer(): bool { return $this->role === 'customer'; }
30
+
31
+ public function hasAdminAccess(): bool
32
+ {
33
+ return in_array($this->role, ['admin', 'organizer']);
34
+ }
35
+
36
+ // ── Relationships ────────────────────────────────────
37
+
38
+ public function events()
39
+ {
40
+ return $this->hasMany(Event::class, 'organizer_id');
41
+ }
42
+
43
+ public function orders()
44
+ {
45
+ return $this->hasMany(Order::class);
46
+ }
47
+ }
app/Providers/AppServiceProvider.php ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ namespace App\Providers;
4
+
5
+ use Illuminate\Support\ServiceProvider;
6
+
7
+ class AppServiceProvider extends ServiceProvider
8
+ {
9
+ /**
10
+ * Register any application services.
11
+ */
12
+ public function register(): void
13
+ {
14
+ //
15
+ }
16
+
17
+ /**
18
+ * Bootstrap any application services.
19
+ */
20
+ public function boot(): void
21
+ {
22
+ //
23
+ }
24
+ }
app/Services/OrderService.php ADDED
@@ -0,0 +1,154 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ namespace App\Services;
4
+
5
+ use App\Models\Event;
6
+ use App\Models\Order;
7
+ use App\Models\OrderItem;
8
+ use App\Models\PromoCode;
9
+ use App\Models\TicketTier;
10
+ use App\Models\User;
11
+ use Illuminate\Support\Facades\DB;
12
+
13
+ class OrderService
14
+ {
15
+ const FEE_RATE = 0.03; // 3% service fee
16
+ const TAX_RATE = 0.11; // 11% PPN
17
+ const EXPIRY_MINUTES = 15;
18
+
19
+ /**
20
+ * Create a pending order with race-condition-safe stock deduction.
21
+ *
22
+ * @param User $user
23
+ * @param Event $event
24
+ * @param array $items [['tier_id' => int, 'qty' => int], ...]
25
+ * @param string|null $promoCode
26
+ * @return Order
27
+ *
28
+ * @throws \Exception
29
+ */
30
+ public function createOrder(User $user, Event $event, array $items, ?string $promoCode = null): Order
31
+ {
32
+ return DB::transaction(function () use ($user, $event, $items, $promoCode) {
33
+
34
+ $subtotal = 0;
35
+ $orderItems = [];
36
+
37
+ foreach ($items as $item) {
38
+ // Lock the tier row to prevent oversell
39
+ $tier = TicketTier::where('id', $item['tier_id'])
40
+ ->where('event_id', $event->id)
41
+ ->lockForUpdate()
42
+ ->firstOrFail();
43
+
44
+ if (!$tier->isOnSale()) {
45
+ throw new \Exception("Tiket \"{$tier->name}\" tidak tersedia untuk dijual saat ini.");
46
+ }
47
+
48
+ $qty = (int) $item['qty'];
49
+
50
+ if ($qty > $tier->max_per_order) {
51
+ throw new \Exception("Maksimal {$tier->max_per_order} tiket per order untuk \"{$tier->name}\".");
52
+ }
53
+
54
+ if ($tier->availableStock() < $qty) {
55
+ throw new \Exception("Kuota tiket \"{$tier->name}\" tidak mencukupi. Sisa: {$tier->availableStock()}.");
56
+ }
57
+
58
+ // Deduct stock
59
+ $tier->increment('sold_count', $qty);
60
+
61
+ $lineTotal = $tier->price * $qty;
62
+ $subtotal += $lineTotal;
63
+
64
+ $orderItems[] = [
65
+ 'tier' => $tier,
66
+ 'qty' => $qty,
67
+ 'unit_price' => $tier->price,
68
+ 'total' => $lineTotal,
69
+ ];
70
+ }
71
+
72
+ // Calculate fees
73
+ $fee = round($subtotal * self::FEE_RATE, 2);
74
+ $tax = round($subtotal * self::TAX_RATE, 2);
75
+ $discount = 0;
76
+
77
+ // Apply promo code
78
+ if ($promoCode) {
79
+ $promo = PromoCode::where('code', $promoCode)->first();
80
+ if ($promo && $promo->isValid()) {
81
+ $discount = $promo->applyDiscount($subtotal);
82
+ $promo->increment('used_count');
83
+ }
84
+ }
85
+
86
+ $total = max(0, $subtotal + $fee + $tax - $discount);
87
+
88
+ // Create order
89
+ $order = Order::create([
90
+ 'user_id' => $user->id,
91
+ 'event_id' => $event->id,
92
+ 'subtotal' => $subtotal,
93
+ 'fee' => $fee,
94
+ 'tax' => $tax,
95
+ 'discount' => $discount,
96
+ 'total' => $total,
97
+ 'status' => 'pending',
98
+ 'expires_at' => now()->addMinutes(self::EXPIRY_MINUTES),
99
+ ]);
100
+
101
+ // Create order items
102
+ foreach ($orderItems as $oi) {
103
+ OrderItem::create([
104
+ 'order_id' => $order->id,
105
+ 'ticket_tier_id' => $oi['tier']->id,
106
+ 'qty' => $oi['qty'],
107
+ 'unit_price' => $oi['unit_price'],
108
+ 'total' => $oi['total'],
109
+ ]);
110
+ }
111
+
112
+ return $order->load('items.ticketTier', 'event');
113
+ });
114
+ }
115
+
116
+ /**
117
+ * Expire a pending order and restore stock.
118
+ */
119
+ public function expireOrder(Order $order): void
120
+ {
121
+ if (!$order->isPending()) return;
122
+
123
+ DB::transaction(function () use ($order) {
124
+ $order->update(['status' => 'expired']);
125
+
126
+ foreach ($order->items as $item) {
127
+ $item->ticketTier->decrement('sold_count', $item->qty);
128
+ }
129
+ });
130
+ }
131
+
132
+ /**
133
+ * Cancel/refund an order if the event's refund policy allows.
134
+ */
135
+ public function cancelOrder(Order $order): bool
136
+ {
137
+ if (!$order->isPaid()) return false;
138
+
139
+ // Check refund deadline: must be at least 24h before event start
140
+ $event = $order->event;
141
+ if ($event->start_at->subDay()->isPast()) return false;
142
+
143
+ DB::transaction(function () use ($order) {
144
+ $order->update(['status' => 'refunded']);
145
+
146
+ foreach ($order->items as $item) {
147
+ $item->ticketTier->decrement('sold_count', $item->qty);
148
+ $item->attendees()->delete();
149
+ }
150
+ });
151
+
152
+ return true;
153
+ }
154
+ }
app/Services/TicketService.php ADDED
@@ -0,0 +1,77 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ namespace App\Services;
4
+
5
+ use App\Models\Attendee;
6
+ use App\Models\Order;
7
+ use Illuminate\Support\Facades\DB;
8
+
9
+ class TicketService
10
+ {
11
+ /**
12
+ * Process payment (mock) and generate attendees + ticket codes.
13
+ */
14
+ public function processPayment(Order $order, string $paymentMethod = 'mock_gateway'): Order
15
+ {
16
+ if (!$order->isPending()) {
17
+ throw new \Exception('Order tidak dalam status pending.');
18
+ }
19
+
20
+ return DB::transaction(function () use ($order, $paymentMethod) {
21
+ $order->update([
22
+ 'status' => 'paid',
23
+ 'payment_method' => $paymentMethod,
24
+ 'paid_at' => now(),
25
+ ]);
26
+
27
+ // Generate attendees for each order item
28
+ foreach ($order->items as $item) {
29
+ for ($i = 0; $i < $item->qty; $i++) {
30
+ Attendee::create([
31
+ 'order_item_id' => $item->id,
32
+ 'full_name' => $order->user->name,
33
+ 'email' => $order->user->email,
34
+ 'phone' => $order->user->phone,
35
+ ]);
36
+ }
37
+ }
38
+
39
+ return $order->fresh(['items.ticketTier', 'attendees', 'event', 'user']);
40
+ });
41
+ }
42
+
43
+ /**
44
+ * Check in an attendee by ticket code.
45
+ *
46
+ * @return array{success: bool, message: string, attendee: ?Attendee}
47
+ */
48
+ public function checkIn(string $ticketCode): array
49
+ {
50
+ $attendee = Attendee::where('ticket_code', $ticketCode)
51
+ ->with('orderItem.ticketTier', 'orderItem.order.event')
52
+ ->first();
53
+
54
+ if (!$attendee) {
55
+ return ['success' => false, 'message' => 'Kode tiket tidak ditemukan.', 'attendee' => null];
56
+ }
57
+
58
+ // Verify order is paid
59
+ $order = $attendee->orderItem->order;
60
+ if (!$order->isPaid()) {
61
+ return ['success' => false, 'message' => 'Order belum dibayar (status: ' . $order->status . ').', 'attendee' => $attendee];
62
+ }
63
+
64
+ // Check if already scanned
65
+ if ($attendee->isCheckedIn()) {
66
+ return [
67
+ 'success' => false,
68
+ 'message' => 'Tiket sudah di-check-in pada ' . $attendee->checkin_at->format('d M Y H:i') . '.',
69
+ 'attendee' => $attendee,
70
+ ];
71
+ }
72
+
73
+ $attendee->checkIn();
74
+
75
+ return ['success' => true, 'message' => 'Check-in berhasil! ✅', 'attendee' => $attendee->fresh()];
76
+ }
77
+ }
artisan ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env php
2
+ <?php
3
+
4
+ use Illuminate\Foundation\Application;
5
+ use Symfony\Component\Console\Input\ArgvInput;
6
+
7
+ define('LARAVEL_START', microtime(true));
8
+
9
+ // Register the Composer autoloader...
10
+ require __DIR__.'/vendor/autoload.php';
11
+
12
+ // Bootstrap Laravel and handle the command...
13
+ /** @var Application $app */
14
+ $app = require_once __DIR__.'/bootstrap/app.php';
15
+
16
+ $status = $app->handleCommand(new ArgvInput);
17
+
18
+ exit($status);