├── .circleci └── config.yml ├── .docker ├── octane │ ├── .rr.prod.yaml │ ├── FrankenPHP │ │ └── supervisord.frankenphp.conf │ ├── RoadRunner │ │ ├── .rr.prod.yaml │ │ └── supervisord.roadrunner.conf │ ├── Swoole │ │ └── supervisord.swoole.conf │ ├── entrypoint.sh │ ├── opcache.ini │ ├── php.ini │ ├── supervisord.app.conf │ ├── supervisord.app.roadrunner.conf │ ├── supervisord.horizon.conf │ └── utilities.sh ├── php.ini ├── start-container ├── supervisord.horizon.conf ├── supervisord.scheduler.conf ├── supervisord.worker.conf └── utilities.sh ├── .env.example ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ └── sweep-template.yml ├── dependabot.yml ├── issue_template.md └── workflows │ └── main.yml ├── .gitignore ├── Dockerfile ├── README.md ├── app ├── Console │ └── Kernel.php ├── Exceptions │ └── Handler.php ├── Filament │ └── Admin │ │ └── Resources │ │ ├── CartItemResource.php │ │ ├── CartItemResource │ │ └── Pages │ │ │ ├── CreateCartItem.php │ │ │ ├── EditCartItem.php │ │ │ └── ListCartItems.php │ │ ├── CustomerResource.php │ │ ├── CustomerResource │ │ └── Pages │ │ │ ├── CreateCustomer.php │ │ │ ├── EditCustomer.php │ │ │ └── ListCustomers.php │ │ ├── GroupResource.php │ │ ├── GroupResource │ │ └── Pages │ │ │ ├── CreateGroup.php │ │ │ ├── EditGroup.php │ │ │ └── ListGroups.php │ │ ├── InvoiceResource.php │ │ ├── InvoiceResource │ │ └── Pages │ │ │ ├── CreateInvoice.php │ │ │ ├── EditInvoice.php │ │ │ └── ListInvoices.php │ │ ├── OrderItemResource.php │ │ ├── OrderItemResource │ │ └── Pages │ │ │ ├── CreateOrderItem.php │ │ │ ├── EditOrderItem.php │ │ │ └── ListOrderItems.php │ │ ├── OrderResource.php │ │ ├── OrderResource │ │ └── Pages │ │ │ ├── CreateOrder.php │ │ │ ├── EditOrder.php │ │ │ └── ListOrders.php │ │ ├── ProductCategoryResource.php │ │ ├── ProductCategoryResource │ │ └── Pages │ │ │ ├── CreateProductCategory.php │ │ │ ├── EditProductCategory.php │ │ │ └── ListProductCategories.php │ │ ├── ProductRatingResource.php │ │ ├── ProductRatingResource │ │ └── Pages │ │ │ ├── CreateProductRating.php │ │ │ ├── EditProductRating.php │ │ │ └── ListProductRatings.php │ │ ├── ProductResource.php │ │ ├── ProductResource │ │ └── Pages │ │ │ ├── CreateProduct.php │ │ │ ├── EditProduct.php │ │ │ └── ListProducts.php │ │ ├── ProductReviewResource.php │ │ ├── ProductReviewResource │ │ └── Pages │ │ │ ├── CreateProductReview.php │ │ │ ├── EditProductReview.php │ │ │ └── ListProductReviews.php │ │ ├── ProductTagResource.php │ │ ├── ProductTagResource │ │ └── Pages │ │ │ ├── CreateProductTag.php │ │ │ ├── EditProductTag.php │ │ │ └── ListProductTags.php │ │ ├── SimpleProductResource.php │ │ └── SimpleProductResource │ │ └── Pages │ │ ├── CreateSimpleProduct.php │ │ ├── EditSimpleProduct.php │ │ └── ListSimpleProducts.php ├── Http │ ├── Controllers │ │ ├── CheckoutController.php │ │ ├── Controller.php │ │ ├── DownloadController.php │ │ ├── HomeController.php │ │ ├── InventoryController.php │ │ ├── InvoiceController.php │ │ ├── PaymentMethodController.php │ │ ├── PaypalPaymentController.php │ │ ├── ProductController.php │ │ ├── RatingController.php │ │ ├── ReviewController.php │ │ ├── SiteSettingController.php │ │ ├── StripePaymentController.php │ │ └── SubscriptionController.php │ ├── Kernel.php │ ├── Livewire │ │ ├── InvoicePdf.php │ │ └── ShoppingCart.php │ └── Middleware │ │ ├── Authenticate.php │ │ ├── EncryptCookies.php │ │ ├── PreventRequestsDuringMaintenance.php │ │ ├── RedirectIfAuthenticated.php │ │ ├── TrimStrings.php │ │ ├── TrustHosts.php │ │ ├── TrustProxies.php │ │ ├── ValidateSignature.php │ │ └── VerifyCsrfToken.php ├── Mail │ └── InvoiceMail.php ├── Models │ ├── CartItem.php │ ├── Customer.php │ ├── DownloadableProduct.php │ ├── Group.php │ ├── InventoryLog.php │ ├── Invoice.php │ ├── Order.php │ ├── OrderItem.php │ ├── PaymentMethod.php │ ├── Product.php │ ├── ProductCategory.php │ ├── ProductRating.php │ ├── ProductReview.php │ ├── ProductTag.php │ ├── Rating.php │ ├── Review.php │ ├── SimpleProduct.php │ ├── SiteSetting.php │ ├── Subscription.php │ └── User.php ├── Notifications │ ├── PaypalTransactionNotification.php │ ├── SubscriptionUpdatedNotification.php │ └── TransactionSuccessNotification.php ├── Providers │ ├── AppServiceProvider.php │ ├── AuthServiceProvider.php │ ├── BroadcastServiceProvider.php │ ├── EventServiceProvider.php │ ├── Filament │ │ └── AdminPanelProvider.php │ └── RouteServiceProvider.php └── Services │ ├── PaymentGatewayService.php │ └── SubscriptionService.php ├── artisan ├── bootstrap ├── app.php └── cache │ └── .gitignore ├── composer.json ├── composer.lock ├── config ├── app.php ├── auth.php ├── broadcasting.php ├── cache.php ├── cors.php ├── database.php ├── filesystems.php ├── hashing.php ├── logging.php ├── mail.php ├── octane.php ├── queue.php ├── sanctum.php ├── services.php ├── session.php └── view.php ├── database ├── .gitignore ├── factories │ └── UserFactory.php ├── migrations │ ├── 2014_10_12_000000_create_users_table.php │ ├── 2014_10_12_100000_create_password_reset_tokens_table.php │ ├── 2019_08_19_000000_create_failed_jobs_table.php │ ├── 2019_12_14_000001_create_personal_access_tokens_table.php │ ├── 2023_04_01_000000_create_downloadable_products_table.php │ ├── 2023_04_01_000000_create_payment_methods_table.php │ ├── 2023_04_01_000000_create_products_table.php │ ├── 2023_04_01_000000_create_reviews_table.php │ ├── 2023_04_01_000000_create_site_settings_table.php │ ├── 2023_04_01_000001_create_ratings_table.php │ ├── 2023_09_26_113707_create_product_categories_table.php │ ├── 2023_09_26_113708_create_products_table.php │ ├── 2023_09_26_113743_create_customers_table.php │ ├── 2023_09_26_113752_create_orders_table.php │ ├── 2023_09_27_130936_create_cart_items_table.php │ ├── 2023_09_27_132432_create_order_items_table.php │ ├── 2023_09_28_010654_create_table_tags.php │ ├── 2023_09_28_010821_create_images_table.php │ ├── 2023_09_28_010935_create_variations_table.php │ ├── 2023_09_28_011302_create_product_tag_table.php │ ├── 2023_09_28_011516_create_product_image_table.php │ ├── 2023_09_28_013747_create_product_variation_table.php │ ├── 2023_09_28_014231_create_group_table.php │ ├── 2023_09_28_014549_create_product_group_table.php │ ├── 2023_09_28_015116_create_simple_product_table.php │ ├── 2023_09_28_021219_create_product_rating_table.php │ ├── 2023_09_28_021229_create_product_reviews_table.php │ ├── 2023_09_29_151612_create_invoices_table.php │ ├── 2023_10_01_000000_create_inventory_logs_table.php │ ├── xxxx_xx_xx_create_cart_items_table.php │ └── xxxx_xx_xx_xxxxxx_create_invoices_table.php └── seeders │ └── DatabaseSeeder.php ├── package-lock.json ├── package.json ├── phpunit.xml ├── postcss.config.cjs ├── public ├── .htaccess ├── css │ └── filament │ │ ├── filament │ │ └── app.css │ │ ├── forms │ │ └── forms.css │ │ └── support │ │ └── support.css ├── favicon.ico ├── index.php ├── js │ └── filament │ │ ├── filament │ │ ├── app.js │ │ └── echo.js │ │ ├── forms │ │ └── components │ │ │ ├── color-picker.js │ │ │ ├── date-time-picker.js │ │ │ ├── file-upload.js │ │ │ ├── key-value.js │ │ │ ├── markdown-editor.js │ │ │ ├── rich-editor.js │ │ │ ├── select.js │ │ │ ├── tags-input.js │ │ │ └── textarea.js │ │ ├── notifications │ │ └── notifications.js │ │ ├── support │ │ ├── async-alpine.js │ │ └── support.js │ │ ├── tables │ │ └── components │ │ │ └── table.js │ │ └── widgets │ │ └── components │ │ ├── chart.js │ │ └── stats-overview │ │ └── stat │ │ └── chart.js └── robots.txt ├── resources ├── css │ └── app.css ├── js │ └── app.js └── views │ ├── checkout.blade.php │ ├── checkout │ ├── address_autofill.blade.php │ ├── checkout.blade.php │ └── order_summary.blade.php │ ├── components │ └── layouts │ │ └── app.blade.php │ ├── home.blade.php │ ├── invoices │ ├── index.blade.php │ └── show.blade.php │ ├── livewire │ └── shopping-cart.blade.php │ ├── payment_methods │ └── index.blade.php │ ├── product.blade.php │ ├── products │ ├── create.blade.php │ ├── index.blade.php │ └── show.blade.php │ ├── reviews.blade.php │ ├── site_settings │ ├── edit.blade.php │ └── index.blade.php │ ├── subscriptions │ └── manage.blade.php │ └── welcome.blade.php ├── routes ├── api.php ├── channels.php ├── console.php └── web.php ├── storage ├── app │ ├── .gitignore │ └── public │ │ └── .gitignore ├── framework │ ├── .gitignore │ ├── cache │ │ ├── .gitignore │ │ └── data │ │ │ └── .gitignore │ ├── sessions │ │ └── .gitignore │ ├── testing │ │ └── .gitignore │ └── views │ │ └── .gitignore └── logs │ └── .gitignore ├── sweep.yaml ├── tailwind.config.js ├── tests ├── CreatesApplication.php ├── Feature │ ├── CheckoutControllerTest.php │ ├── DownloadableProductTest.php │ ├── ExampleTest.php │ ├── InvoiceTest.php │ ├── ProductControllerTest.php │ └── ShoppingCartTest.php ├── TestCase.php └── Unit │ ├── Controllers │ └── ProductControllerTest.php │ ├── ExampleTest.php │ ├── PaymentGatewayServiceTest.php │ ├── PaypalPaymentControllerTest.php │ ├── PaypalTransactionNotificationTest.php │ ├── RatingControllerTest.php │ ├── RatingTest.php │ ├── ReviewControllerTest.php │ ├── ReviewTest.php │ ├── SubscriptionControllerTest.php │ └── SubscriptionServiceTest.php └── vite.config.js /.docker/octane/.rr.prod.yaml: -------------------------------------------------------------------------------- 1 | version: '2.7' 2 | rpc: 3 | listen: 'tcp://127.0.0.1:6001' 4 | http: 5 | middleware: [ "static", "gzip", "headers" ] 6 | max_request_size: 20 7 | static: 8 | dir: "public" 9 | forbid: [ ".php", ".htaccess" ] 10 | uploads: 11 | forbid: [".php", ".exe", ".bat", ".sh"] 12 | pool: 13 | allocate_timeout: 10s 14 | destroy_timeout: 10s 15 | supervisor: 16 | max_worker_memory: 128 17 | exec_ttl: 60s 18 | logs: 19 | mode: production 20 | level: debug 21 | encoding: json 22 | -------------------------------------------------------------------------------- /.docker/octane/FrankenPHP/supervisord.frankenphp.conf: -------------------------------------------------------------------------------- 1 | [supervisord] 2 | nodaemon=true 3 | user=%(ENV_USER)s 4 | logfile=/var/log/supervisor/supervisord.log 5 | pidfile=/var/run/supervisord.pid 6 | 7 | [program:octane] 8 | process_name=%(program_name)s_%(process_num)02d 9 | command=php %(ENV_ROOT)s/artisan octane:start --server=frankenphp --host=0.0.0.0 --port=80 --admin-port=2019 10 | user=%(ENV_USER)s 11 | autostart=true 12 | autorestart=true 13 | environment=LARAVEL_OCTANE="1" 14 | stdout_logfile=/dev/stdout 15 | stdout_logfile_maxbytes=0 16 | stderr_logfile=/dev/stderr 17 | stderr_logfile_maxbytes=0 18 | 19 | [program:horizon] 20 | process_name=%(program_name)s_%(process_num)02d 21 | command=php %(ENV_ROOT)s/artisan horizon 22 | user=%(ENV_USER)s 23 | autostart=%(ENV_WITH_HORIZON)s 24 | autorestart=true 25 | stdout_logfile=%(ENV_ROOT)s/horizon.log 26 | stopwaitsecs=3600 27 | 28 | [program:scheduler] 29 | process_name=%(program_name)s_%(process_num)02d 30 | command=supercronic -overlapping /etc/supercronic/laravel 31 | user=%(ENV_USER)s 32 | autostart=%(ENV_WITH_SCHEDULER)s 33 | autorestart=true 34 | stdout_logfile=%(ENV_ROOT)s/scheduler.log 35 | 36 | [program:clear-scheduler-cache] 37 | process_name=%(program_name)s_%(process_num)02d 38 | command=php %(ENV_ROOT)s/artisan schedule:clear-cache 39 | user=%(ENV_USER)s 40 | autostart=%(ENV_WITH_SCHEDULER)s 41 | autorestart=false 42 | stdout_logfile=%(ENV_ROOT)s/scheduler.log -------------------------------------------------------------------------------- /.docker/octane/RoadRunner/.rr.prod.yaml: -------------------------------------------------------------------------------- 1 | version: '2.7' 2 | rpc: 3 | listen: 'tcp://127.0.0.1:6001' 4 | server: 5 | relay: pipes 6 | http: 7 | middleware: [ "static", "gzip", "headers" ] 8 | max_request_size: 20 9 | static: 10 | dir: "public" 11 | forbid: [ ".php", ".htaccess" ] 12 | uploads: 13 | forbid: [".php", ".exe", ".bat", ".sh"] 14 | pool: 15 | allocate_timeout: 10s 16 | destroy_timeout: 10s 17 | supervisor: 18 | max_worker_memory: 128 19 | exec_ttl: 60s 20 | logs: 21 | mode: production 22 | level: debug 23 | encoding: json 24 | status: 25 | address: localhost:2114 26 | -------------------------------------------------------------------------------- /.docker/octane/RoadRunner/supervisord.roadrunner.conf: -------------------------------------------------------------------------------- 1 | [supervisord] 2 | nodaemon=true 3 | user=%(ENV_USER)s 4 | logfile=/var/log/supervisor/supervisord.log 5 | pidfile=/var/run/supervisord.pid 6 | 7 | [program:octane] 8 | process_name=%(program_name)s_%(process_num)02d 9 | command=php %(ENV_ROOT)s/artisan octane:start --server=roadrunner --host=0.0.0.0 --port=80 --rpc-port=6001 --rr-config=%(ENV_ROOT)s/.rr.yaml 10 | user=%(ENV_USER)s 11 | autostart=true 12 | autorestart=true 13 | environment=LARAVEL_OCTANE="1" 14 | stdout_logfile=/dev/stdout 15 | stdout_logfile_maxbytes=0 16 | stderr_logfile=/dev/stderr 17 | stderr_logfile_maxbytes=0 18 | 19 | [program:horizon] 20 | process_name=%(program_name)s_%(process_num)02d 21 | command=php %(ENV_ROOT)s/artisan horizon 22 | user=%(ENV_USER)s 23 | autostart=%(ENV_WITH_HORIZON)s 24 | autorestart=true 25 | stdout_logfile=%(ENV_ROOT)s/horizon.log 26 | stopwaitsecs=3600 27 | 28 | [program:scheduler] 29 | process_name=%(program_name)s_%(process_num)02d 30 | command=supercronic -overlapping /etc/supercronic/laravel 31 | user=%(ENV_USER)s 32 | autostart=%(ENV_WITH_SCHEDULER)s 33 | autorestart=true 34 | stdout_logfile=%(ENV_ROOT)s/scheduler.log 35 | 36 | [program:clear-scheduler-cache] 37 | process_name=%(program_name)s_%(process_num)02d 38 | command=php %(ENV_ROOT)s/artisan schedule:clear-cache 39 | user=%(ENV_USER)s 40 | autostart=%(ENV_WITH_SCHEDULER)s 41 | autorestart=false 42 | stdout_logfile=%(ENV_ROOT)s/scheduler.log -------------------------------------------------------------------------------- /.docker/octane/Swoole/supervisord.swoole.conf: -------------------------------------------------------------------------------- 1 | [supervisord] 2 | nodaemon=true 3 | user=%(ENV_USER)s 4 | logfile=/var/log/supervisor/supervisord.log 5 | pidfile=/var/run/supervisord.pid 6 | 7 | [program:octane] 8 | process_name=%(program_name)s_%(process_num)02d 9 | command=php %(ENV_ROOT)s/artisan octane:start --server=swoole --host=0.0.0.0 --port=80 10 | user=%(ENV_USER)s 11 | autostart=true 12 | autorestart=true 13 | environment=LARAVEL_OCTANE="1" 14 | stdout_logfile=/dev/stdout 15 | stdout_logfile_maxbytes=0 16 | stderr_logfile=/dev/stderr 17 | stderr_logfile_maxbytes=0 18 | 19 | [program:horizon] 20 | process_name=%(program_name)s_%(process_num)02d 21 | command=php %(ENV_ROOT)s/artisan horizon 22 | user=%(ENV_USER)s 23 | autostart=%(ENV_WITH_HORIZON)s 24 | autorestart=true 25 | stdout_logfile=%(ENV_ROOT)s/horizon.log 26 | stopwaitsecs=3600 27 | 28 | [program:scheduler] 29 | process_name=%(program_name)s_%(process_num)02d 30 | command=supercronic -overlapping /etc/supercronic/laravel 31 | user=%(ENV_USER)s 32 | autostart=%(ENV_WITH_SCHEDULER)s 33 | autorestart=true 34 | stdout_logfile=%(ENV_ROOT)s/scheduler.log 35 | 36 | [program:clear-scheduler-cache] 37 | process_name=%(program_name)s_%(process_num)02d 38 | command=php %(ENV_ROOT)s/artisan schedule:clear-cache 39 | user=%(ENV_USER)s 40 | autostart=%(ENV_WITH_SCHEDULER)s 41 | autorestart=false 42 | stdout_logfile=%(ENV_ROOT)s/scheduler.log -------------------------------------------------------------------------------- /.docker/octane/entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | 4 | container_mode=${CONTAINER_MODE:-app} 5 | octane_server=${OCTANE_SERVER:-swoole} 6 | echo "Container mode: $container_mode" 7 | 8 | php() { 9 | su octane -c "php $*" 10 | } 11 | 12 | initialStuff() { 13 | php artisan optimize:clear; \ 14 | php artisan package:discover --ansi; \ 15 | php artisan event:cache; \ 16 | php artisan config:cache; \ 17 | php artisan route:cache; 18 | } 19 | 20 | if [ "$1" != "" ]; then 21 | exec "$@" 22 | elif [ ${container_mode} = "app" ]; then 23 | echo "Octane server: $octane_server" 24 | initialStuff 25 | if [ ${octane_server} = "swoole" ]; then 26 | exec /usr/bin/supervisord -c /etc/supervisor/conf.d/supervisord.app.conf 27 | elif [ ${octane_server} = "roadrunner" ]; then 28 | exec /usr/bin/supervisord -c /etc/supervisor/conf.d/supervisord.app.roadrunner.conf 29 | else 30 | echo "Invalid Octane server supplied." 31 | exit 1 32 | fi 33 | elif [ ${container_mode} = "horizon" ]; then 34 | initialStuff 35 | exec /usr/bin/supervisord -c /etc/supervisor/conf.d/supervisord.horizon.conf 36 | elif [ ${container_mode} = "scheduler" ]; then 37 | initialStuff 38 | exec supercronic /etc/supercronic/laravel 39 | else 40 | echo "Container mode mismatched." 41 | exit 1 42 | fi 43 | -------------------------------------------------------------------------------- /.docker/octane/opcache.ini: -------------------------------------------------------------------------------- 1 | [Opcache] 2 | opcache.enable = 1 3 | opcache.enable_cli = 1 4 | opcache.memory_consumption = 256M 5 | opcache.use_cwd = 0 6 | opcache.max_file_size = 0 7 | opcache.max_accelerated_files = 32531 8 | opcache.validate_timestamps = 0 9 | opcache.revalidate_freq = 0 10 | 11 | [JIT] 12 | opcache.jit_buffer_size = 100M 13 | opcache.jit = function -------------------------------------------------------------------------------- /.docker/octane/php.ini: -------------------------------------------------------------------------------- 1 | [PHP] 2 | post_max_size = 100M 3 | upload_max_filesize = 100M 4 | expose_php = 0 5 | variables_order = "GPCS" 6 | -------------------------------------------------------------------------------- /.docker/octane/supervisord.app.conf: -------------------------------------------------------------------------------- 1 | [supervisord] 2 | nodaemon=true 3 | user=root 4 | logfile=/var/log/supervisor/supervisord.log 5 | pidfile=/var/run/supervisord.pid 6 | 7 | [program:octane] 8 | process_name=%(program_name)s_%(process_num)02d 9 | command=php /var/www/html/artisan octane:start --server=swoole --host=0.0.0.0 --port=9000 --workers=auto --task-workers=auto --max-requests=500 10 | user=octane 11 | autostart=true 12 | autorestart=true 13 | environment=LARAVEL_OCTANE="1" 14 | stdout_logfile=/dev/stdout 15 | stdout_logfile_maxbytes=0 16 | stderr_logfile=/dev/stderr 17 | stderr_logfile_maxbytes=0 18 | 19 | [program:horizon] 20 | process_name=%(program_name)s_%(process_num)02d 21 | command=php /var/www/html/artisan horizon 22 | user=octane 23 | autostart=%(ENV_APP_WITH_HORIZON)s 24 | autorestart=true 25 | stdout_logfile=/var/www/html/horizon.log 26 | stopwaitsecs=3600 27 | 28 | [program:scheduler] 29 | process_name=%(program_name)s_%(process_num)02d 30 | command=supercronic /etc/supercronic/laravel 31 | user=octane 32 | autostart=%(ENV_APP_WITH_SCHEDULER)s 33 | autorestart=true 34 | stdout_logfile=/var/www/html/scheduler.log -------------------------------------------------------------------------------- /.docker/octane/supervisord.app.roadrunner.conf: -------------------------------------------------------------------------------- 1 | [supervisord] 2 | nodaemon=true 3 | user=root 4 | logfile=/var/log/supervisor/supervisord.log 5 | pidfile=/var/run/supervisord.pid 6 | 7 | [program:octane] 8 | process_name=%(program_name)s_%(process_num)02d 9 | command=php /var/www/html/artisan octane:start --server=roadrunner --host=0.0.0.0 --port=9000 --rpc-port=6001 --workers=auto --max-requests=500 --rr-config=/var/www/html/.rr.yaml 10 | user=octane 11 | autostart=true 12 | autorestart=true 13 | environment=LARAVEL_OCTANE="1" 14 | stdout_logfile=/dev/stdout 15 | stdout_logfile_maxbytes=0 16 | stderr_logfile=/dev/stderr 17 | stderr_logfile_maxbytes=0 18 | 19 | [program:horizon] 20 | process_name=%(program_name)s_%(process_num)02d 21 | command=php /var/www/html/artisan horizon 22 | user=octane 23 | autostart=%(ENV_APP_WITH_HORIZON)s 24 | autorestart=true 25 | stdout_logfile=/var/www/html/horizon.log 26 | stopwaitsecs=3600 27 | 28 | [program:scheduler] 29 | process_name=%(program_name)s_%(process_num)02d 30 | command=supercronic /etc/supercronic/laravel 31 | user=octane 32 | autostart=%(ENV_APP_WITH_SCHEDULER)s 33 | autorestart=true 34 | stdout_logfile=/var/www/html/scheduler.log 35 | -------------------------------------------------------------------------------- /.docker/octane/supervisord.horizon.conf: -------------------------------------------------------------------------------- 1 | [supervisord] 2 | nodaemon=true 3 | user=root 4 | logfile=/var/log/supervisor/supervisord.log 5 | pidfile=/var/run/supervisord.pid 6 | 7 | [program:horizon] 8 | process_name=%(program_name)s_%(process_num)02d 9 | command=php /var/www/html/artisan horizon 10 | user=octane 11 | autostart=true 12 | autorestart=true 13 | stdout_logfile=/dev/stdout 14 | stdout_logfile_maxbytes=0 15 | stderr_logfile=/dev/stderr 16 | stderr_logfile_maxbytes=0 17 | stopwaitsecs=3600 18 | -------------------------------------------------------------------------------- /.docker/octane/utilities.sh: -------------------------------------------------------------------------------- 1 | php() { 2 | echo "Running PHP as octane user ..." 3 | su octane -c "php $*" 4 | } 5 | 6 | tinker() { 7 | if [ -z "$1" ]; then 8 | php artisan tinker 9 | else 10 | php artisan tinker --execute="\"dd($1);\"" 11 | fi 12 | } 13 | 14 | # Determine size of a file or total size of a directory 15 | fs() { 16 | if du -b /dev/null >/dev/null 2>&1; then 17 | local arg=-sbh 18 | else 19 | local arg=-sh 20 | fi 21 | if [[ -n "$@" ]]; then 22 | du $arg -- "$@" 23 | else 24 | du $arg .[^.]* ./* 25 | fi 26 | } 27 | 28 | # Commonly used aliases 29 | alias ..="cd .." 30 | alias ...="cd ../.." 31 | alias art="php artisan" 32 | -------------------------------------------------------------------------------- /.docker/php.ini: -------------------------------------------------------------------------------- 1 | [PHP] 2 | post_max_size = 100M 3 | upload_max_filesize = 100M 4 | expose_php = 0 5 | realpath_cache_size = 16M 6 | realpath_cache_ttl = 360 7 | 8 | [Opcache] 9 | opcache.enable = 1 10 | opcache.enable_cli = 1 11 | opcache.memory_consumption = 256M 12 | opcache.use_cwd = 0 13 | opcache.max_file_size = 0 14 | opcache.max_accelerated_files = 32531 15 | opcache.validate_timestamps = 0 16 | opcache.file_update_protection = 0 17 | opcache.interned_strings_buffer = 16 18 | opcache.file_cache = 60 19 | 20 | [JIT] 21 | opcache.jit_buffer_size = 128M 22 | opcache.jit = function 23 | opcache.jit_prof_threshold = 0.001 24 | opcache.jit_max_root_traces = 2048 25 | opcache.jit_max_side_traces = 256 26 | 27 | [zlib] 28 | zlib.output_compression = On 29 | zlib.output_compression_level = 9 30 | -------------------------------------------------------------------------------- /.docker/start-container: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | 4 | container_mode=${CONTAINER_MODE:-http} 5 | octane_server=${OCTANE_SERVER} 6 | echo "Container mode: $container_mode" 7 | 8 | initialStuff() { 9 | php artisan optimize:clear; \ 10 | php artisan event:cache; \ 11 | php artisan config:cache; \ 12 | php artisan route:cache; 13 | } 14 | 15 | if [ "$1" != "" ]; then 16 | exec "$@" 17 | elif [ ${container_mode} = "http" ]; then 18 | echo "Octane Server: $octane_server" 19 | initialStuff 20 | if [ ${octane_server} = "frankenphp" ]; then 21 | exec /usr/bin/supervisord -c /etc/supervisor/conf.d/supervisord.frankenphp.conf 22 | elif [ ${octane_server} = "swoole" ]; then 23 | exec /usr/bin/supervisord -c /etc/supervisor/conf.d/supervisord.swoole.conf 24 | elif [ ${octane_server} = "roadrunner" ]; then 25 | exec /usr/bin/supervisord -c /etc/supervisor/conf.d/supervisord.roadrunner.conf 26 | else 27 | echo "Invalid Octane server supplied." 28 | exit 1 29 | fi 30 | elif [ ${container_mode} = "horizon" ]; then 31 | initialStuff 32 | exec /usr/bin/supervisord -c /etc/supervisor/conf.d/supervisord.horizon.conf 33 | elif [ ${container_mode} = "scheduler" ]; then 34 | initialStuff 35 | exec /usr/bin/supervisord -c /etc/supervisor/conf.d/supervisord.scheduler.conf 36 | elif [ ${container_mode} = "worker" ]; then 37 | initialStuff 38 | exec /usr/bin/supervisord -c /etc/supervisor/conf.d/supervisord.worker.conf 39 | else 40 | echo "Container mode mismatched." 41 | exit 1 42 | fi 43 | -------------------------------------------------------------------------------- /.docker/supervisord.horizon.conf: -------------------------------------------------------------------------------- 1 | [supervisord] 2 | nodaemon=true 3 | user=%(ENV_USER)s 4 | logfile=/var/log/supervisor/supervisord.log 5 | pidfile=/var/run/supervisord.pid 6 | 7 | [program:horizon] 8 | process_name=%(program_name)s_%(process_num)02d 9 | command=php %(ENV_ROOT)s/artisan horizon 10 | user=%(ENV_USER)s 11 | autostart=true 12 | autorestart=true 13 | stdout_logfile=/dev/stdout 14 | stdout_logfile_maxbytes=0 15 | stderr_logfile=/dev/stderr 16 | stderr_logfile_maxbytes=0 17 | stopwaitsecs=3600 18 | -------------------------------------------------------------------------------- /.docker/supervisord.scheduler.conf: -------------------------------------------------------------------------------- 1 | [supervisord] 2 | nodaemon=true 3 | user=%(ENV_USER)s 4 | logfile=/var/log/supervisor/supervisord.log 5 | pidfile=/var/run/supervisord.pid 6 | 7 | [program:scheduler] 8 | process_name=%(program_name)s_%(process_num)02d 9 | command=supercronic -overlapping /etc/supercronic/laravel 10 | user=%(ENV_USER)s 11 | autostart=%(ENV_WITH_SCHEDULER)s 12 | autorestart=true 13 | stdout_logfile=/dev/stdout 14 | stdout_logfile_maxbytes=0 15 | stderr_logfile=/dev/stderr 16 | stderr_logfile_maxbytes=0 17 | 18 | [program:clear-scheduler-cache] 19 | process_name=%(program_name)s_%(process_num)02d 20 | command=php %(ENV_ROOT)s/artisan schedule:clear-cache 21 | user=%(ENV_USER)s 22 | autostart=%(ENV_WITH_SCHEDULER)s 23 | autorestart=false 24 | stdout_logfile=/dev/stdout 25 | stdout_logfile_maxbytes=0 26 | stderr_logfile=/dev/stderr 27 | stderr_logfile_maxbytes=0 -------------------------------------------------------------------------------- /.docker/supervisord.worker.conf: -------------------------------------------------------------------------------- 1 | [supervisord] 2 | nodaemon=true 3 | user=%(ENV_USER)s 4 | logfile=/var/log/supervisor/supervisord.log 5 | pidfile=/var/run/supervisord.pid 6 | 7 | [program:worker] 8 | process_name=%(program_name)s_%(process_num)02d 9 | command=%(ENV_WORKER_COMMAND)s 10 | user=%(ENV_USER)s 11 | autostart=true 12 | autorestart=true 13 | stdout_logfile=/dev/stdout 14 | stdout_logfile_maxbytes=0 15 | stderr_logfile=/dev/stderr 16 | stderr_logfile_maxbytes=0 17 | -------------------------------------------------------------------------------- /.docker/utilities.sh: -------------------------------------------------------------------------------- 1 | tinker() { 2 | if [ -z "$1" ]; then 3 | php artisan tinker 4 | else 5 | php artisan tinker --execute="\"dd($1);\"" 6 | fi 7 | } 8 | 9 | # Commonly used aliases 10 | alias ..="cd .." 11 | alias ...="cd ../.." 12 | alias art="php artisan" 13 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | COMPOSE_FILE=docker-compose.yml 2 | BASE_IMAGE_DOCKERFILE=./.docker/prod/base/Dockerfile 3 | IMAGE_REGISTRY=familytree365 4 | IMAGE_TAG=latest 5 | 6 | APP_NAME="Liberu Ecommerce" 7 | APP_ENV=local 8 | APP_KEY=base64:2DzJhWedD2gRkRfhK/ENYEBBiQJACDRVfiYg0NdImvw= 9 | APP_DEBUG=true 10 | APP_URL=http://localhost 11 | 12 | OWNER_COMPANY_ID=1 13 | DB_TENANT_DATABASE=tenant 14 | 15 | LOG_CHANNEL=stack 16 | LOG_DEPRECATIONS_CHANNEL=null 17 | LOG_LEVEL=debug 18 | 19 | DB_CONNECTION=mysql 20 | DB_HOST=127.0.0.1 21 | DB_PORT=3306 22 | DB_DATABASE=ecommerce 23 | DB_USERNAME=root 24 | DB_PASSWORD= 25 | 26 | BROADCAST_DRIVER=redis 27 | CACHE_DRIVER=array 28 | FILESYSTEM_DISK=local 29 | SESSION_DRIVER=file 30 | SESSION_LIFETIME=120 31 | SESSION_DOMAIN=localhost 32 | 33 | CACHE_LIFETIME=60 34 | 35 | LOGIN_ATTEMPTS_PER_MINUTE=5 36 | PASSWORD_LIFETIME=0 37 | PASSWORD_MIN_LENGTH=6 38 | PASSWORD_MIXED_CASE=0 39 | PASSWORD_NUMERIC=0 40 | PASSWORD_SPECIAL=0 41 | 42 | REDIS_HOST=127.0.0.1 43 | REDIS_PREFIX= 44 | REDIS_PASSWORD=null 45 | REDIS_PORT=6379 46 | 47 | MAIL_MAILER=smtp 48 | MAIL_HOST=smtp.mailtrap.io 49 | MAIL_PORT=2525 50 | MAIL_USERNAME=null 51 | MAIL_PASSWORD=null 52 | MAIL_ENCRYPTION=null 53 | MAIL_FROM_ADDRESS=null 54 | MAIL_FROM_NAME="${APP_NAME}" 55 | 56 | MAILGUN_DOMAIN= 57 | MAILGUN_SECRET= 58 | MAILGUN_ENDPOINT=api.mailgun.net 59 | 60 | AWS_ACCESS_KEY_ID= 61 | AWS_SECRET_ACCESS_KEY= 62 | AWS_DEFAULT_REGION=us-east-1 63 | AWS_BUCKET= 64 | AWS_USE_PATH_STYLE_ENDPOINT=false 65 | 66 | PUSHER_APP_ID= 67 | PUSHER_APP_KEY= 68 | PUSHER_APP_SECRET= 69 | PUSHER_APP_CLUSTER=mt1 70 | 71 | FACEBOOK_CLIENT_ID= 72 | FACEBOOK_CLIENT_SECRET= 73 | FACEBOOK_REDIRECT_URI= 74 | 75 | GOOGLE_CLIENT_ID= 76 | GOOGLE_CLIENT_SECRET= 77 | GOOGLE_REDIRECT_URI= 78 | 79 | GITHUB_CLIENT_ID= 80 | GITHUB_CLIENT_SECRET= 81 | GITHUB_REDIRECT_URI= 82 | 83 | STRIPE_KEY=XXX 84 | STRIPE_SECRET=XXX 85 | 86 | SENTRY_DSN= 87 | 88 | TELESCOPE_ENABLED=0 89 | 90 | ENSO_API_TOKEN= 91 | 92 | WIKITREE_API= 93 | 94 | TINY_MCE_API_KEY= 95 | 96 | SANCTUM_STATEFUL_DOMAINS=localhost,127.0.0.1,127.0.0.1:8000,127.0.0.1:3000,localhost:3000,localhost:8080,::1 97 | 98 | GELATO_API_KEY=x 99 | 100 | LARAVEL_ECHO_SERVER_REDIS_HOST=127.0.0.1 101 | LARAVEL_ECHO_SERVER_REDIS_PORT=6379 102 | LARAVEL_ECHO_SERVER_DEBUG=true 103 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: laravel-liberu 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/sweep-template.yml: -------------------------------------------------------------------------------- 1 | name: Sweep Issue 2 | title: 'Sweep: ' 3 | description: For small bugs, features, refactors, and tests to be handled by Sweep, an AI-powered junior developer. 4 | labels: sweep 5 | body: 6 | - type: textarea 7 | id: description 8 | attributes: 9 | label: Details 10 | description: Tell Sweep where and what to edit and provide enough context for a new developer to the codebase 11 | placeholder: | 12 | Unit Tests: Write unit tests for . Test each function in the file. Make sure to test edge cases. 13 | Bugs: The bug might be in . Here are the logs: ... 14 | Features: the new endpoint should use the ... class from because it contains ... logic. 15 | Refactors: We are migrating this function to ... version because ... -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "composer" 4 | # Files stored in repository root 5 | directory: "/" 6 | schedule: 7 | interval: "daily" 8 | - package-ecosystem: "npm" 9 | # Files stored in repository root 10 | directory: "/" 11 | schedule: 12 | interval: "daily" 13 | - package-ecosystem: "github-actions" 14 | # Workflow files stored in the 15 | # default location of `.github/workflows` 16 | directory: "/" 17 | schedule: 18 | interval: "daily" 19 | -------------------------------------------------------------------------------- /.github/issue_template.md: -------------------------------------------------------------------------------- 1 | 2 | This is a **bug | feature request**. 3 | 4 | 5 | ### Prerequisites 6 | * [ ] Are you running the latest version? 7 | * [ ] Are you reporting to the correct repository? 8 | * [ ] Did you check the documentation? 9 | * [ ] Did you perform a cursory search? 10 | 11 | ### Description 12 | 13 | 14 | ### Steps to Reproduce 15 | 20 | 21 | ### Expected behavior 22 | 23 | 24 | ### Actual behavior 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor/ 2 | node_modules/ 3 | npm-debug.log 4 | yarn-error.log 5 | 6 | # Laravel 4 specific 7 | bootstrap/compiled.php 8 | app/storage/ 9 | 10 | # Laravel 5 & Lumen specific 11 | public/storage 12 | public/hot 13 | 14 | # Laravel 5 & Lumen specific with changed public path 15 | public_html/storage 16 | public_html/hot 17 | 18 | storage/*.key 19 | .env 20 | Homestead.yaml 21 | Homestead.json 22 | /.vagrant 23 | .phpunit.result.cache 24 | rr 25 | .rr.yaml 26 | -------------------------------------------------------------------------------- /app/Console/Kernel.php: -------------------------------------------------------------------------------- 1 | command('inspire')->hourly(); 16 | } 17 | 18 | /** 19 | * Register the commands for the application. 20 | */ 21 | protected function commands(): void 22 | { 23 | $this->load(__DIR__.'/Commands'); 24 | 25 | require base_path('routes/console.php'); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /app/Exceptions/Handler.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | protected $dontFlash = [ 16 | 'current_password', 17 | 'password', 18 | 'password_confirmation', 19 | ]; 20 | 21 | /** 22 | * Register the exception handling callbacks for the application. 23 | */ 24 | public function register(): void 25 | { 26 | $this->reportable(function (Throwable $e) { 27 | // 28 | }); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /app/Filament/Admin/Resources/CartItemResource/Pages/CreateCartItem.php: -------------------------------------------------------------------------------- 1 | schema([ 26 | // 27 | ]); 28 | } 29 | 30 | public static function table(Table $table): Table 31 | { 32 | return $table 33 | ->columns([ 34 | // 35 | ]) 36 | ->filters([ 37 | // 38 | ]) 39 | ->actions([ 40 | Tables\Actions\EditAction::make(), 41 | ]) 42 | ->bulkActions([ 43 | Tables\Actions\BulkActionGroup::make([ 44 | Tables\Actions\DeleteBulkAction::make(), 45 | ]), 46 | ]); 47 | } 48 | 49 | public static function getRelations(): array 50 | { 51 | return [ 52 | // 53 | ]; 54 | } 55 | 56 | public static function getPages(): array 57 | { 58 | return [ 59 | 'index' => Pages\ListGroups::route('/'), 60 | 'create' => Pages\CreateGroup::route('/create'), 61 | 'edit' => Pages\EditGroup::route('/{record}/edit'), 62 | ]; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /app/Filament/Admin/Resources/GroupResource/Pages/CreateGroup.php: -------------------------------------------------------------------------------- 1 | schema([ 26 | Forms\Components\TextInput::make('name') 27 | ->required() 28 | ->maxLength(255), 29 | ]); 30 | } 31 | 32 | public static function table(Table $table): Table 33 | { 34 | return $table 35 | ->columns([ 36 | Tables\Columns\TextColumn::make('name') 37 | ->label('Name'), 38 | ]) 39 | ->filters([ 40 | // 41 | ]) 42 | ->actions([ 43 | Tables\Actions\EditAction::make(), 44 | ]) 45 | ->bulkActions([ 46 | Tables\Actions\BulkActionGroup::make([ 47 | Tables\Actions\DeleteBulkAction::make(), 48 | ]), 49 | ]); 50 | } 51 | 52 | public static function getRelations(): array 53 | { 54 | return [ 55 | // 56 | ]; 57 | } 58 | 59 | public static function getPages(): array 60 | { 61 | return [ 62 | 'index' => Pages\ListProductCategories::route('/'), 63 | 'create' => Pages\CreateProductCategory::route('/create'), 64 | 'edit' => Pages\EditProductCategory::route('/{record}/edit'), 65 | ]; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /app/Filament/Admin/Resources/ProductCategoryResource/Pages/CreateProductCategory.php: -------------------------------------------------------------------------------- 1 | schema([ 27 | Forms\Components\Select::make('product_id') 28 | ->label('Product') 29 | ->required() 30 | ->options(Product::pluck('name', 'id')) 31 | ->reactive(), 32 | 33 | Forms\Components\Select::make('tag_id') 34 | ->label('Tag') 35 | ->required() 36 | ->options(ProductTag::pluck('name', 'id')) 37 | ->reactive() 38 | ]); 39 | } 40 | 41 | public static function table(Table $table): Table 42 | { 43 | return $table 44 | ->columns([ 45 | Tables\Columns\TextColumn::make('product.name')->label('Product'), 46 | Tables\Columns\TextColumn::make('tag.name')->label('Tag'), 47 | ]) 48 | ->filters([ 49 | // 50 | ]) 51 | ->actions([ 52 | Tables\Actions\EditAction::make(), 53 | ]) 54 | ->bulkActions([ 55 | Tables\Actions\BulkActionGroup::make([ 56 | Tables\Actions\DeleteBulkAction::make(), 57 | ]), 58 | ]); 59 | } 60 | 61 | public static function getRelations(): array 62 | { 63 | return [ 64 | // 65 | ]; 66 | } 67 | 68 | public static function getPages(): array 69 | { 70 | return [ 71 | 'index' => Pages\ListProductTags::route('/'), 72 | 'create' => Pages\CreateProductTag::route('/create'), 73 | 'edit' => Pages\EditProductTag::route('/{record}/edit'), 74 | ]; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /app/Filament/Admin/Resources/ProductTagResource/Pages/CreateProductTag.php: -------------------------------------------------------------------------------- 1 | schema([ 26 | Forms\Components\TextInput::make('quantity') 27 | ->required(), 28 | Forms\Components\TextInput::make('price') 29 | ->numeric() 30 | ->required(), 31 | Forms\Components\Select::make('product_id') 32 | ->relationship('product', 'name') 33 | ->required(), 34 | ]); 35 | } 36 | 37 | public static function table(Table $table): Table 38 | { 39 | return $table 40 | ->columns([ 41 | Tables\Columns\TextColumn::make('quantity') 42 | ->sortable(), 43 | Tables\Columns\TextColumn::make('price') 44 | ->sortable() 45 | ->searchable(), 46 | Tables\Columns\TextColumn::make('product.name') 47 | ->sortable() 48 | ->searchable(), 49 | ]) 50 | ->filters([ 51 | // 52 | ]) 53 | ->actions([ 54 | Tables\Actions\EditAction::make(), 55 | ]) 56 | ->bulkActions([ 57 | Tables\Actions\BulkActionGroup::make([ 58 | Tables\Actions\DeleteBulkAction::make(), 59 | ]), 60 | ]); 61 | } 62 | 63 | public static function getRelations(): array 64 | { 65 | return [ 66 | // 67 | ]; 68 | } 69 | 70 | public static function getPages(): array 71 | { 72 | return [ 73 | 'index' => Pages\ListSimpleProducts::route('/'), 74 | 'create' => Pages\CreateSimpleProduct::route('/create'), 75 | 'edit' => Pages\EditSimpleProduct::route('/{record}/edit'), 76 | ]; 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /app/Filament/Admin/Resources/SimpleProductResource/Pages/CreateSimpleProduct.php: -------------------------------------------------------------------------------- 1 | session()->get('cart', []); 14 | $request->session()->put('guest_cart', $cart); 15 | $request->session()->put('is_guest', true); 16 | } 17 | 18 | public function checkout(Request $request) 19 | { 20 | if ($request->session()->get('is_guest', false)) { 21 | $this->guestCheckout($request); 22 | } 23 | 24 | // Streamline checkout steps 25 | $checkoutData = $request->only(['email', 'shipping_address', 'payment_method']); 26 | $this->verifyPaymentAndShippingInfo($checkoutData); 27 | 28 | // Proceed with checkout logic... 29 | } 30 | 31 | protected function verifyPaymentAndShippingInfo(array $data) 32 | { 33 | $validator = Validator::make($data, [ 34 | 'email' => 'required|email', 35 | 'shipping_address' => 'required|string|max:255', 36 | 'payment_method' => 'required|string|max:255', 37 | ]); 38 | 39 | if ($validator->fails()) { 40 | throw new \Illuminate\Validation\ValidationException($validator); 41 | } 42 | 43 | // Verification logic... 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /app/Http/Controllers/Controller.php: -------------------------------------------------------------------------------- 1 | firstOrFail(); 15 | if (!$this->checkDownloadLimits($downloadableProduct) || !$this->authorizeDownload($request->user(), $downloadableProduct)) { 16 | abort(403, 'Download limit reached or not authorized.'); 17 | } 18 | $temporaryUrl = Storage::disk('local')->temporaryUrl( 19 | $downloadableProduct->file_url, now()->addMinutes(5) 20 | ); 21 | return response()->json(['url' => $temporaryUrl]); 22 | } 23 | 24 | private function checkDownloadLimits(DownloadableProduct $downloadableProduct) 25 | { 26 | return $downloadableProduct->download_limit > $downloadableProduct->downloads_count && (!$downloadableProduct->expiration_time || $downloadableProduct->expiration_time->isFuture()); 27 | } 28 | 29 | public function serveFile(Request $request, $productId) 30 | { 31 | $downloadableProduct = DownloadableProduct::where('product_id', $productId)->firstOrFail(); 32 | if (!$this->checkDownloadLimits($downloadableProduct) || !$this->authorizeDownload($request->user(), $downloadableProduct)) { 33 | abort(403, 'Download limit reached or not authorized.'); 34 | } 35 | $downloadableProduct->increment('downloads_count'); 36 | return Storage::disk('local')->download($downloadableProduct->file_url); 37 | } 38 | 39 | private function authorizeDownload($user, DownloadableProduct $downloadableProduct) 40 | { 41 | // Implement logic to verify user's purchase 42 | return true; // Placeholder for purchase verification logic 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /app/Http/Controllers/HomeController.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | namespace App\Http\Controllers; 4 | 5 | use App\Models\Product; 6 | use Illuminate\Http\Request; 7 | 8 | class HomeController extends Controller 9 | { 10 | public function index() 11 | { 12 | $featuredProducts = Product::where('is_featured', true)->get(); 13 | // Assuming 'specialOffers' is a method or scope on the Product model that retrieves special offers 14 | $specialOffers = Product::specialOffers()->get(); 15 | 16 | return view('home', [ 17 | 'products' => $featuredProducts, 18 | 'specialOffers' => $specialOffers, 19 | ]); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /app/Http/Controllers/InventoryController.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | namespace App\Http\Controllers; 4 | 5 | use App\Models\InventoryLog; 6 | use App\Models\Product; 7 | use Illuminate\Http\Request; 8 | use Illuminate\Http\Response; 9 | 10 | class InventoryController extends Controller 11 | { 12 | public function adjustInventory(Request $request) 13 | { 14 | $validatedData = $request->validate([ 15 | 'product_id' => 'required|exists:products,id', 16 | 'quantity_change' => 'required|integer', 17 | 'reason' => 'required|string|max:255', 18 | ]); 19 | 20 | $product = Product::find($validatedData['product_id']); 21 | $newInventoryCount = $product->inventory_count + $validatedData['quantity_change']; 22 | 23 | if ($newInventoryCount < 0) { 24 | return response()->json(['message' => 'Invalid quantity change. Inventory count cannot be negative.'], Response::HTTP_BAD_REQUEST); 25 | } 26 | 27 | $product->inventory_count = $newInventoryCount; 28 | $product->save(); 29 | 30 | $inventoryLog = new InventoryLog([ 31 | 'product_id' => $validatedData['product_id'], 32 | 'quantity_change' => $validatedData['quantity_change'], 33 | 'reason' => $validatedData['reason'], 34 | ]); 35 | $inventoryLog->save(); 36 | 37 | return response()->json(['message' => 'Inventory adjusted successfully', 'product' => $product], Response::HTTP_OK); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /app/Http/Controllers/InvoiceController.php: -------------------------------------------------------------------------------- 1 | $order->id, 19 | 'invoice_date' => now(), 20 | 'total_amount' => $order->total, 21 | ]); 22 | foreach ($order->products as $product) { 23 | $invoice->products()->attach($product->id, ['quantity' => $product->pivot->quantity, 'price' => $product->price]); 24 | } 25 | return $invoice; 26 | } 27 | 28 | public function index(Request $request) 29 | { 30 | $query = Invoice::query(); 31 | if ($request->has('date')) { 32 | $query->whereDate('invoice_date', $request->date); 33 | } 34 | if ($request->has('status')) { 35 | $query->where('payment_status', $request->status); 36 | } 37 | $invoices = $query->paginate(10); 38 | return response()->json($invoices); 39 | } 40 | 41 | public function show($id) 42 | { 43 | $invoice = Invoice::with(['order', 'order.products', 'customer'])->findOrFail($id); 44 | return response()->json($invoice); 45 | } 46 | 47 | public function downloadInvoiceAsPDF($id) 48 | { 49 | $invoice = Invoice::findOrFail($id); 50 | $pdf = PDF::loadView('invoices.pdf', compact('invoice')); 51 | return $pdf->download("invoice-{$id}.pdf"); 52 | } 53 | 54 | public function sendInvoiceToCustomer($id) 55 | { 56 | $invoice = Invoice::with('customer')->findOrFail($id); 57 | $pdf = \Livewire\Livewire::mount('invoice-pdf', ['invoiceId' => $id])->httpResponse->getContent(); 58 | Mail::to($invoice->customer->email)->send(new InvoiceMail($invoice, $pdf)); 59 | return response()->json(['message' => 'Invoice sent to customer successfully.']); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /app/Http/Controllers/PaypalPaymentController.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | namespace App\Http\Controllers; 4 | 5 | use App\Services\PaymentGatewayService; 6 | use App\Services\SubscriptionService; 7 | use Illuminate\Http\Request; 8 | 9 | class PaypalPaymentController extends Controller 10 | { 11 | private $paymentGatewayService; 12 | private $subscriptionService; 13 | 14 | public function __construct(PaymentGatewayService $paymentGatewayService, SubscriptionService $subscriptionService) 15 | { 16 | $this->paymentGatewayService = $paymentGatewayService; 17 | $this->subscriptionService = $subscriptionService; 18 | } 19 | 20 | public function createOneTimePayment(Request $request) 21 | { 22 | $paymentMethodId = $request->input('paymentMethodId'); 23 | $amount = $request->input('amount'); 24 | 25 | $result = $this->paymentGatewayService->processPaypalPayment($paymentMethodId, $amount); 26 | 27 | return response()->json($result); 28 | } 29 | 30 | public function createSubscription(Request $request) 31 | { 32 | $paymentMethodId = $request->input('paymentMethodId'); 33 | $planId = $request->input('planId'); 34 | $userDetails = $request->only(['email', 'address']); 35 | 36 | $result = $this->subscriptionService->createSubscription($paymentMethodId, $planId, $userDetails); 37 | 38 | return response()->json($result); 39 | } 40 | 41 | public function updateSubscription(Request $request) 42 | { 43 | $subscriptionId = $request->input('subscriptionId'); 44 | $planId = $request->input('planId'); 45 | 46 | $result = $this->subscriptionService->updateSubscription($subscriptionId, $planId); 47 | 48 | return response()->json($result); 49 | } 50 | 51 | public function cancelSubscription(Request $request) 52 | { 53 | $subscriptionId = $request->input('subscriptionId'); 54 | 55 | $result = $this->subscriptionService->cancelSubscription($subscriptionId); 56 | 57 | return response()->json($result); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /app/Http/Controllers/RatingController.php: -------------------------------------------------------------------------------- 1 | user_id = auth()->id(); 15 | $rating->product_id = $request->product_id; 16 | $rating->rating = $request->rating; 17 | $rating->save(); 18 | 19 | return response()->json(['message' => 'Rating submitted successfully', 'rating' => $rating], 201); 20 | } 21 | 22 | public function calculateAverageRating($productId) 23 | { 24 | $averageRating = Rating::calculateAverageRating($productId); 25 | 26 | return response()->json(['averageRating' => $averageRating]); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /app/Http/Controllers/ReviewController.php: -------------------------------------------------------------------------------- 1 | user_id = auth()->id(); 15 | $review->product_id = $request->product_id; 16 | $review->rating = $request->rating; 17 | $review->review = $request->review; 18 | $review->approved = false; // Reviews are not approved by default 19 | $review->save(); 20 | 21 | return response()->json(['message' => 'Review submitted successfully', 'review' => $review], 201); 22 | } 23 | 24 | public function approve($id) 25 | { 26 | $review = Review::find($id); 27 | if (!$review) { 28 | return response()->json(['message' => 'Review not found'], 404); 29 | } 30 | 31 | $review->approve(); 32 | return response()->json(['message' => 'Review approved successfully']); 33 | } 34 | 35 | public function show($productId) 36 | { 37 | $reviews = Review::where('product_id', $productId)->where('approved', true)->get(); 38 | return response()->json($reviews); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /app/Http/Controllers/SiteSettingController.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | namespace App\Http\Controllers; 4 | 5 | use App\Models\SiteSetting; 6 | use Illuminate\Http\Request; 7 | use Illuminate\Support\Facades\Validator; 8 | 9 | class SiteSettingController extends Controller 10 | { 11 | public function index() 12 | { 13 | $settings = SiteSetting::all(); 14 | return response()->json($settings); 15 | } 16 | 17 | public function edit($id) 18 | { 19 | $setting = SiteSetting::findOrFail($id); 20 | return response()->json($setting); 21 | } 22 | 23 | public function update(Request $request, $id) 24 | { 25 | $validator = Validator::make($request->all(), [ 26 | 'name' => 'required|string|max:255', 27 | 'value' => 'required|string', 28 | 'description' => 'nullable|string', 29 | ]); 30 | 31 | if ($validator->fails()) { 32 | return response()->json($validator->errors(), 422); 33 | } 34 | 35 | $setting = SiteSetting::findOrFail($id); 36 | $setting->update($request->all()); 37 | return response()->json($setting); 38 | } 39 | 40 | public function store(Request $request) 41 | { 42 | $validator = Validator::make($request->all(), [ 43 | 'name' => 'required|string|max:255|unique:site_settings', 44 | 'value' => 'required|string', 45 | 'description' => 'nullable|string', 46 | ]); 47 | 48 | if ($validator->fails()) { 49 | return response()->json($validator->errors(), 422); 50 | } 51 | 52 | $setting = SiteSetting::create($request->all()); 53 | return response()->json($setting, 201); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /app/Http/Livewire/InvoicePdf.php: -------------------------------------------------------------------------------- 1 | invoiceId = $invoiceId; 16 | } 17 | 18 | public function render() 19 | { 20 | $invoice = Invoice::findOrFail($this->invoiceId); 21 | $pdf = PDF::loadView('invoices.pdf', ['invoice' => $invoice]); 22 | return response()->streamDownload(function () use ($pdf) { 23 | echo $pdf->stream(); 24 | }, "invoice-{$this->invoiceId}.pdf"); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /app/Http/Livewire/ShoppingCart.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | namespace App\Http\Livewire; 4 | 5 | use Livewire\Component; 6 | use Illuminate\Support\Facades\Session; 7 | 8 | class ShoppingCart extends Component 9 | { 10 | public $items = []; 11 | 12 | public function mount() 13 | { 14 | $this->items = Session::get('cart', []); 15 | } 16 | 17 | public function render() 18 | { 19 | return view('livewire.shopping-cart', ['items' => $this->items]); 20 | } 21 | 22 | public function addToCart($productId, $name, $price, $quantity = 1) 23 | { 24 | if (isset($this->items[$productId])) { 25 | $this->items[$productId]['quantity'] += $quantity; 26 | } else { 27 | $this->items[$productId] = [ 28 | 'name' => $name, 29 | 'price' => $price, 30 | 'quantity' => $quantity, 31 | ]; 32 | } 33 | 34 | Session::put('cart', $this->items); 35 | } 36 | 37 | public function updateQuantity($productId, $quantity) 38 | { 39 | if (isset($this->items[$productId]) && $quantity > 0) { 40 | $this->items[$productId]['quantity'] = $quantity; 41 | Session::put('cart', $this->items); 42 | } 43 | } 44 | 45 | public function removeItem($productId) 46 | { 47 | if (isset($this->items[$productId])) { 48 | unset($this->items[$productId]); 49 | Session::put('cart', $this->items); 50 | } 51 | } 52 | 53 | public function clearCart() 54 | { 55 | $this->items = []; 56 | Session::forget('cart'); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /app/Http/Middleware/Authenticate.php: -------------------------------------------------------------------------------- 1 | expectsJson() ? null : route('login'); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /app/Http/Middleware/EncryptCookies.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | protected $except = [ 15 | // 16 | ]; 17 | } 18 | -------------------------------------------------------------------------------- /app/Http/Middleware/PreventRequestsDuringMaintenance.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | protected $except = [ 15 | // 16 | ]; 17 | } 18 | -------------------------------------------------------------------------------- /app/Http/Middleware/RedirectIfAuthenticated.php: -------------------------------------------------------------------------------- 1 | check()) { 24 | return redirect(RouteServiceProvider::HOME); 25 | } 26 | } 27 | 28 | return $next($request); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /app/Http/Middleware/TrimStrings.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | protected $except = [ 15 | 'current_password', 16 | 'password', 17 | 'password_confirmation', 18 | ]; 19 | } 20 | -------------------------------------------------------------------------------- /app/Http/Middleware/TrustHosts.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | public function hosts(): array 15 | { 16 | return [ 17 | $this->allSubdomainsOfApplicationUrl(), 18 | ]; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /app/Http/Middleware/TrustProxies.php: -------------------------------------------------------------------------------- 1 | |string|null 14 | */ 15 | protected $proxies; 16 | 17 | /** 18 | * The headers that should be used to detect proxies. 19 | * 20 | * @var int 21 | */ 22 | protected $headers = 23 | Request::HEADER_X_FORWARDED_FOR | 24 | Request::HEADER_X_FORWARDED_HOST | 25 | Request::HEADER_X_FORWARDED_PORT | 26 | Request::HEADER_X_FORWARDED_PROTO | 27 | Request::HEADER_X_FORWARDED_AWS_ELB; 28 | } 29 | -------------------------------------------------------------------------------- /app/Http/Middleware/ValidateSignature.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | protected $except = [ 15 | // 'fbclid', 16 | // 'utm_campaign', 17 | // 'utm_content', 18 | // 'utm_medium', 19 | // 'utm_source', 20 | // 'utm_term', 21 | ]; 22 | } 23 | -------------------------------------------------------------------------------- /app/Http/Middleware/VerifyCsrfToken.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | protected $except = [ 15 | // 16 | ]; 17 | } 18 | -------------------------------------------------------------------------------- /app/Mail/InvoiceMail.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | namespace App\Mail; 4 | 5 | use App\Models\Invoice; 6 | use Illuminate\Bus\Queueable; 7 | use Illuminate\Mail\Mailable; 8 | use Illuminate\Queue\SerializesModels; 9 | use Barryvdh\DomPDF\Facade as PDF; 10 | 11 | class InvoiceMail extends Mailable 12 | { 13 | use Queueable, SerializesModels; 14 | 15 | public $invoice; 16 | 17 | public function __construct(Invoice $invoice) 18 | { 19 | $this->invoice = $invoice; 20 | } 21 | 22 | public function build() 23 | { 24 | $pdf = PDF::loadView('invoices.pdf', ['invoice' => $this->invoice]); 25 | 26 | return $this->subject('Your Invoice from Ecommerce') 27 | ->view('emails.invoice') 28 | ->attachData($pdf->output(), "invoice-{$this->invoice->id}.pdf", [ 29 | 'mime' => 'application/pdf', 30 | ]); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /app/Models/CartItem.php: -------------------------------------------------------------------------------- 1 | belongsTo(Product::class); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /app/Models/Customer.php: -------------------------------------------------------------------------------- 1 | hasMany(Order::class); 28 | } 29 | 30 | public function invoices() 31 | { 32 | return $this->hasMany(Invoice::class); 33 | } 34 | 35 | public function review() 36 | { 37 | return $this->hasMany(ProductReview::class); 38 | } 39 | 40 | public function rating() 41 | { 42 | return $this->hasMany(ProductRating::class); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /app/Models/DownloadableProduct.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | namespace App\Models; 4 | 5 | use Illuminate\Database\Eloquent\Model; 6 | use Illuminate\Database\Eloquent\Relations\BelongsTo; 7 | 8 | class DownloadableProduct extends Model 9 | { 10 | protected $fillable = [ 11 | 'product_id', 12 | 'file_url', 13 | 'download_limit', 14 | 'expiration_time', 15 | ]; 16 | 17 | public function product(): BelongsTo 18 | { 19 | return $this->belongsTo(Product::class, 'product_id'); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /app/Models/Group.php: -------------------------------------------------------------------------------- 1 | belongsTo(Product::class); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /app/Models/Invoice.php: -------------------------------------------------------------------------------- 1 | belongsTo(Order::class); 24 | } 25 | 26 | public function customer() 27 | { 28 | return $this->belongsTo(Customer::class); 29 | } 30 | } 31 | protected $casts = [ 32 | 'invoice_date' => 'datetime', 33 | ]; 34 | 35 | public function products() 36 | { 37 | return $this->belongsToMany(Product::class)->withPivot('quantity', 'price'); 38 | } 39 | -------------------------------------------------------------------------------- /app/Models/Order.php: -------------------------------------------------------------------------------- 1 | belongsTo(Customer::class); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /app/Models/OrderItem.php: -------------------------------------------------------------------------------- 1 | belongsTo(Product::class); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /app/Models/PaymentMethod.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | namespace App\Models; 4 | 5 | use Illuminate\Database\Eloquent\Model; 6 | 7 | class PaymentMethod extends Model 8 | { 9 | protected $table = 'payment_methods'; 10 | 11 | protected $fillable = ['user_id', 'name', 'details', 'is_default']; 12 | 13 | public function user() 14 | { 15 | return $this->belongsTo(User::class, 'user_id'); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /app/Models/Product.php: -------------------------------------------------------------------------------- 1 | belongsTo(ProductCategory::class); 25 | } 26 | 27 | public function cartItems() 28 | { 29 | return $this->hasMany(CartItem::class); 30 | } 31 | 32 | public function orderItems() 33 | { 34 | return $this->hasMany(OrderItem::class); 35 | } 36 | 37 | public function review() 38 | { 39 | return $this->hasMany(ProductReview::class); 40 | } 41 | 42 | public function rating() 43 | { 44 | return $this->hasMany(ProductRating::class); 45 | } 46 | 47 | 48 | } 49 | public function downloadable() 50 | { 51 | return $this->hasMany(DownloadableProduct::class); 52 | } 53 | -------------------------------------------------------------------------------- /app/Models/ProductCategory.php: -------------------------------------------------------------------------------- 1 | hasMany(Product::class); 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /app/Models/ProductRating.php: -------------------------------------------------------------------------------- 1 | belongsTo(Product::class); 23 | } 24 | 25 | public function customer() 26 | { 27 | return $this->belongsTo(Customer::class); 28 | } 29 | } 30 | 31 | 32 | -------------------------------------------------------------------------------- /app/Models/ProductReview.php: -------------------------------------------------------------------------------- 1 | belongsTo(Product::class); 23 | } 24 | 25 | public function customer() 26 | { 27 | return $this->belongsTo(Customer::class); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /app/Models/ProductTag.php: -------------------------------------------------------------------------------- 1 | belongsTo(Product::class); 22 | } 23 | 24 | public function tag() 25 | { 26 | return $this->belongsTo(ProductTag::class); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /app/Models/Rating.php: -------------------------------------------------------------------------------- 1 | belongsTo(User::class); 18 | } 19 | 20 | public function product() 21 | { 22 | return $this->belongsTo(Product::class); 23 | } 24 | 25 | public static function calculateAverageRating($productId) 26 | { 27 | return self::where('product_id', $productId) 28 | ->avg('rating'); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /app/Models/Review.php: -------------------------------------------------------------------------------- 1 | belongsTo(User::class); 18 | } 19 | 20 | public function product() 21 | { 22 | return $this->belongsTo(Product::class); 23 | } 24 | 25 | public function approve() 26 | { 27 | $this->approved = true; 28 | $this->save(); 29 | } 30 | 31 | public function reject() 32 | { 33 | $this->approved = false; 34 | $this->save(); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /app/Models/SimpleProduct.php: -------------------------------------------------------------------------------- 1 | belongsTo(Product::class); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /app/Models/SiteSetting.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | namespace App\Models; 4 | 5 | use Illuminate\Database\Eloquent\Model; 6 | 7 | class SiteSetting extends Model 8 | { 9 | protected $fillable = ['name', 'value', 'description']; 10 | 11 | protected $casts = [ 12 | 'value' => 'string', 13 | ]; 14 | } 15 | -------------------------------------------------------------------------------- /app/Models/Subscription.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | namespace App\Models; 4 | 5 | use Illuminate\Database\Eloquent\Model; 6 | use Laravel\Cashier\Billable; 7 | 8 | class Subscription extends Model 9 | { 10 | use Billable; 11 | 12 | protected $fillable = [ 13 | 'name', 'stripe_id', 'stripe_status', 'stripe_plan', 'quantity', 'trial_ends_at', 'ends_at', 14 | ]; 15 | 16 | public function user() 17 | { 18 | return $this->belongsTo(User::class); 19 | } 20 | 21 | public function isActive() 22 | { 23 | return $this->stripe_status === 'active'; 24 | } 25 | 26 | public function cancel() 27 | { 28 | $this->subscription('default')->cancel(); 29 | } 30 | 31 | public function renew() 32 | { 33 | if ($this->onGracePeriod()) { 34 | $this->subscription('default')->resume(); 35 | } else { 36 | // Handle logic for subscriptions that are not in grace period 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /app/Models/User.php: -------------------------------------------------------------------------------- 1 | 19 | */ 20 | protected $fillable = [ 21 | 'name', 22 | 'email', 23 | 'password', 24 | ]; 25 | 26 | /** 27 | * The attributes that should be hidden for serialization. 28 | * 29 | * @var array 30 | */ 31 | protected $hidden = [ 32 | 'password', 33 | 'remember_token', 34 | ]; 35 | 36 | /** 37 | * The attributes that should be cast. 38 | * 39 | * @var array 40 | */ 41 | protected $casts = [ 42 | 'email_verified_at' => 'datetime', 43 | 'password' => 'hashed', 44 | ]; 45 | } 46 | -------------------------------------------------------------------------------- /app/Notifications/PaypalTransactionNotification.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | namespace App\Notifications; 4 | 5 | use Illuminate\Bus\Queueable; 6 | use Illuminate\Contracts\Queue\ShouldQueue; 7 | use Illuminate\Notifications\Messages\MailMessage; 8 | use Illuminate\Notifications\Notification; 9 | use Illuminate\Notifications\Messages\NexmoMessage; 10 | 11 | class PaypalTransactionNotification extends Notification implements ShouldQueue 12 | { 13 | use Queueable; 14 | 15 | protected $transactionDetails; 16 | 17 | public function __construct($transactionDetails) 18 | { 19 | $this->transactionDetails = $transactionDetails; 20 | } 21 | 22 | public function via($notifiable) 23 | { 24 | return ['mail', 'database']; // Add 'nexmo' for SMS notifications if required 25 | } 26 | 27 | public function toMail($notifiable) 28 | { 29 | $mailMessage = (new MailMessage) 30 | ->subject('PayPal Transaction Notification') 31 | ->line('This is a notification regarding your recent PayPal transaction.') 32 | ->line('Transaction Type: ' . $this->transactionDetails['type']) 33 | ->line('Amount: ' . $this->transactionDetails['amount']); 34 | 35 | if ($this->transactionDetails['type'] === 'subscription_renewal') { 36 | $mailMessage->line('Your subscription has been successfully renewed.'); 37 | } elseif ($this->transactionDetails['type'] === 'upcoming_charge') { 38 | $mailMessage->line('You have an upcoming charge for your subscription.'); 39 | } elseif ($this->transactionDetails['type'] === 'subscription_cancellation') { 40 | $mailMessage->line('Your subscription has been cancelled.'); 41 | } else { 42 | $mailMessage->line('Your payment was successful.'); 43 | } 44 | 45 | return $mailMessage->action('View Details', url('/transactions')); 46 | } 47 | 48 | public function toNexmo($notifiable) 49 | { 50 | $message = new NexmoMessage(); 51 | $message->content('Your PayPal transaction was successful. Amount: ' . $this->transactionDetails['amount']); 52 | return $message; 53 | } 54 | 55 | public function toArray($notifiable) 56 | { 57 | return [ 58 | 'transaction_type' => $this->transactionDetails['type'], 59 | 'amount' => $this->transactionDetails['amount'], 60 | 'message' => 'Your PayPal transaction was successful.' 61 | ]; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /app/Notifications/SubscriptionUpdatedNotification.php: -------------------------------------------------------------------------------- 1 | subscriptionDetails = $subscriptionDetails; 19 | } 20 | 21 | public function via($notifiable) 22 | { 23 | return ['mail']; 24 | } 25 | 26 | public function toMail($notifiable) 27 | { 28 | $mailMessage = (new MailMessage) 29 | ->subject('Subscription Update') 30 | ->greeting('Hello!'); 31 | 32 | if ($this->subscriptionDetails['type'] === 'plan_change') { 33 | $mailMessage->line('Your subscription plan has been successfully updated.') 34 | ->line('Old Plan: ' . $this->subscriptionDetails['old_plan']) 35 | ->line('New Plan: ' . $this->subscriptionDetails['new_plan']) 36 | ->line('Effective Date: ' . $this->subscriptionDetails['effective_date']); 37 | } elseif ($this->subscriptionDetails['type'] === 'cancellation') { 38 | $mailMessage->line('Your subscription has been cancelled.') 39 | ->line('Cancellation Date: ' . $this->subscriptionDetails['cancellation_date']) 40 | ->line('Your subscription will not renew.'); 41 | } 42 | 43 | $mailMessage->action('View Subscription', url('/subscription')) 44 | ->line('Thank you for using our application!'); 45 | 46 | return $mailMessage; 47 | } 48 | 49 | public function toArray($notifiable) 50 | { 51 | return [ 52 | 'subscription_details' => $this->subscriptionDetails, 53 | ]; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /app/Notifications/TransactionSuccessNotification.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | namespace App\Notifications; 4 | 5 | use Illuminate\Bus\Queueable; 6 | use Illuminate\Contracts\Queue\ShouldQueue; 7 | use Illuminate\Notifications\Messages\MailMessage; 8 | use Illuminate\Notifications\Notification; 9 | 10 | class TransactionSuccessNotification extends Notification implements ShouldQueue 11 | { 12 | use Queueable; 13 | 14 | protected $transactionDetails; 15 | 16 | public function __construct(array $transactionDetails) 17 | { 18 | $this->transactionDetails = $transactionDetails; 19 | } 20 | 21 | public function via($notifiable) 22 | { 23 | return ['mail']; 24 | } 25 | 26 | public function toMail($notifiable) 27 | { 28 | return (new MailMessage) 29 | ->subject('Transaction Successful') 30 | ->greeting('Hello!') 31 | ->line('Your transaction has been successfully processed.') 32 | ->line('Transaction ID: ' . $this->transactionDetails['transaction_id']) 33 | ->line('Amount: $' . number_format($this->transactionDetails['amount'], 2)) 34 | ->action('View Transaction', url('/transactions/' . $this->transactionDetails['transaction_id'])) 35 | ->line('Thank you for using our application!'); 36 | } 37 | 38 | public function toArray($notifiable) 39 | { 40 | return [ 41 | 'transaction_id' => $this->transactionDetails['transaction_id'], 42 | 'amount' => $this->transactionDetails['amount'], 43 | ]; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /app/Providers/AppServiceProvider.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | protected $policies = [ 16 | // 17 | ]; 18 | 19 | /** 20 | * Register any authentication / authorization services. 21 | */ 22 | public function boot(): void 23 | { 24 | // 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /app/Providers/BroadcastServiceProvider.php: -------------------------------------------------------------------------------- 1 | > 16 | */ 17 | protected $listen = [ 18 | Registered::class => [ 19 | SendEmailVerificationNotification::class, 20 | ], 21 | ]; 22 | 23 | /** 24 | * Register any events for your application. 25 | */ 26 | public function boot(): void 27 | { 28 | // 29 | } 30 | 31 | /** 32 | * Determine if events and listeners should be automatically discovered. 33 | */ 34 | public function shouldDiscoverEvents(): bool 35 | { 36 | return false; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /app/Providers/Filament/AdminPanelProvider.php: -------------------------------------------------------------------------------- 1 | id('admin') 27 | ->path('admin') 28 | ->login() 29 | ->registration() 30 | ->passwordReset() 31 | ->emailVerification() 32 | ->profile() 33 | ->colors([ 34 | 'primary' => Color::Amber, 35 | ]) 36 | ->discoverResources(in: app_path('Filament/Admin/Resources'), for: 'App\\Filament\\Admin\\Resources') 37 | ->discoverPages(in: app_path('Filament/Admin/Pages'), for: 'App\\Filament\\Admin\\Pages') 38 | ->pages([ 39 | Pages\Dashboard::class, 40 | ]) 41 | ->discoverWidgets(in: app_path('Filament/Admin/Widgets'), for: 'App\\Filament\\Admin\\Widgets') 42 | ->widgets([ 43 | Widgets\AccountWidget::class, 44 | Widgets\FilamentInfoWidget::class, 45 | ]) 46 | ->middleware([ 47 | EncryptCookies::class, 48 | AddQueuedCookiesToResponse::class, 49 | StartSession::class, 50 | AuthenticateSession::class, 51 | ShareErrorsFromSession::class, 52 | VerifyCsrfToken::class, 53 | SubstituteBindings::class, 54 | DisableBladeIconComponents::class, 55 | DispatchServingFilamentEvent::class, 56 | ]) 57 | ->authMiddleware([ 58 | Authenticate::class, 59 | ]); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /app/Providers/RouteServiceProvider.php: -------------------------------------------------------------------------------- 1 | by($request->user()?->id ?: $request->ip()); 29 | }); 30 | 31 | $this->routes(function () { 32 | Route::middleware('api') 33 | ->prefix('api') 34 | ->group(base_path('routes/api.php')); 35 | 36 | Route::middleware('web') 37 | ->group(base_path('routes/web.php')); 38 | }); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /artisan: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | make(Illuminate\Contracts\Console\Kernel::class); 34 | 35 | $status = $kernel->handle( 36 | $input = new Symfony\Component\Console\Input\ArgvInput, 37 | new Symfony\Component\Console\Output\ConsoleOutput 38 | ); 39 | 40 | /* 41 | |-------------------------------------------------------------------------- 42 | | Shutdown The Application 43 | |-------------------------------------------------------------------------- 44 | | 45 | | Once Artisan has finished running, we will fire off the shutdown events 46 | | so that any final work may be done by the application before we shut 47 | | down the process. This is the last thing to happen to the request. 48 | | 49 | */ 50 | 51 | $kernel->terminate($input, $status); 52 | 53 | exit($status); 54 | -------------------------------------------------------------------------------- /bootstrap/app.php: -------------------------------------------------------------------------------- 1 | singleton( 30 | Illuminate\Contracts\Http\Kernel::class, 31 | App\Http\Kernel::class 32 | ); 33 | 34 | $app->singleton( 35 | Illuminate\Contracts\Console\Kernel::class, 36 | App\Console\Kernel::class 37 | ); 38 | 39 | $app->singleton( 40 | Illuminate\Contracts\Debug\ExceptionHandler::class, 41 | App\Exceptions\Handler::class 42 | ); 43 | 44 | /* 45 | |-------------------------------------------------------------------------- 46 | | Return The Application 47 | |-------------------------------------------------------------------------- 48 | | 49 | | This script returns the application instance. The instance is given to 50 | | the calling script so we can separate the building of the instances 51 | | from the actual running of the application and sending responses. 52 | | 53 | */ 54 | 55 | return $app; 56 | -------------------------------------------------------------------------------- /bootstrap/cache/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "laravel/laravel", 3 | "type": "project", 4 | "description": "The skeleton application for the Laravel framework.", 5 | "keywords": [ 6 | "laravel", 7 | "framework" 8 | ], 9 | "license": "MIT", 10 | "require": { 11 | "php": "^8.2", 12 | "filament/filament": "^3.2", 13 | "guzzlehttp/guzzle": "^7.8", 14 | "laravel/framework": "dev-master", 15 | "laravel/octane": "^2.3", 16 | "laravel/sanctum": "dev-master", 17 | "laravel/tinker": "^2.9" 18 | }, 19 | "require-dev": { 20 | "fakerphp/faker": "^1.23", 21 | "laravel/pint": "^1.14", 22 | "laravel/sail": "^1.28", 23 | "mockery/mockery": "^1.6", 24 | "nunomaduro/collision": "^8.0", 25 | "phpunit/phpunit": "^11.0", 26 | "spatie/laravel-ignition": "^2.4" 27 | }, 28 | "autoload": { 29 | "psr-4": { 30 | "App\\": "app/", 31 | "Database\\Factories\\": "database/factories/", 32 | "Database\\Seeders\\": "database/seeders/" 33 | } 34 | }, 35 | "autoload-dev": { 36 | "psr-4": { 37 | "Tests\\": "tests/" 38 | } 39 | }, 40 | "scripts": { 41 | "post-autoload-dump": [ 42 | "Illuminate\\Foundation\\ComposerScripts::postAutoloadDump", 43 | "@php artisan package:discover --ansi", 44 | "@php artisan filament:upgrade" 45 | ], 46 | "post-update-cmd": [ 47 | "@php artisan vendor:publish --tag=laravel-assets --ansi --force" 48 | ], 49 | "post-root-package-install": [ 50 | "@php -r \"file_exists('.env') || copy('.env.example', '.env');\"" 51 | ], 52 | "post-create-project-cmd": [ 53 | "@php artisan key:generate --ansi" 54 | ] 55 | }, 56 | "extra": { 57 | "laravel": { 58 | "dont-discover": [] 59 | } 60 | }, 61 | "config": { 62 | "optimize-autoloader": true, 63 | "preferred-install": "dist", 64 | "sort-packages": true, 65 | "allow-plugins": { 66 | "pestphp/pest-plugin": true, 67 | "php-http/discovery": true 68 | } 69 | }, 70 | "minimum-stability": "stable", 71 | "prefer-stable": true 72 | } 73 | -------------------------------------------------------------------------------- /config/broadcasting.php: -------------------------------------------------------------------------------- 1 | env('BROADCAST_DRIVER', 'null'), 19 | 20 | /* 21 | |-------------------------------------------------------------------------- 22 | | Broadcast Connections 23 | |-------------------------------------------------------------------------- 24 | | 25 | | Here you may define all of the broadcast connections that will be used 26 | | to broadcast events to other systems or over websockets. Samples of 27 | | each available type of connection are provided inside this array. 28 | | 29 | */ 30 | 31 | 'connections' => [ 32 | 33 | 'pusher' => [ 34 | 'driver' => 'pusher', 35 | 'key' => env('PUSHER_APP_KEY'), 36 | 'secret' => env('PUSHER_APP_SECRET'), 37 | 'app_id' => env('PUSHER_APP_ID'), 38 | 'options' => [ 39 | 'cluster' => env('PUSHER_APP_CLUSTER'), 40 | 'host' => env('PUSHER_HOST') ?: 'api-'.env('PUSHER_APP_CLUSTER', 'mt1').'.pusher.com', 41 | 'port' => env('PUSHER_PORT', 443), 42 | 'scheme' => env('PUSHER_SCHEME', 'https'), 43 | 'encrypted' => true, 44 | 'useTLS' => env('PUSHER_SCHEME', 'https') === 'https', 45 | ], 46 | 'client_options' => [ 47 | // Guzzle client options: https://docs.guzzlephp.org/en/stable/request-options.html 48 | ], 49 | ], 50 | 51 | 'ably' => [ 52 | 'driver' => 'ably', 53 | 'key' => env('ABLY_KEY'), 54 | ], 55 | 56 | 'redis' => [ 57 | 'driver' => 'redis', 58 | 'connection' => 'default', 59 | ], 60 | 61 | 'log' => [ 62 | 'driver' => 'log', 63 | ], 64 | 65 | 'null' => [ 66 | 'driver' => 'null', 67 | ], 68 | 69 | ], 70 | 71 | ]; 72 | -------------------------------------------------------------------------------- /config/cors.php: -------------------------------------------------------------------------------- 1 | ['api/*', 'sanctum/csrf-cookie'], 19 | 20 | 'allowed_methods' => ['*'], 21 | 22 | 'allowed_origins' => ['*'], 23 | 24 | 'allowed_origins_patterns' => [], 25 | 26 | 'allowed_headers' => ['*'], 27 | 28 | 'exposed_headers' => [], 29 | 30 | 'max_age' => 0, 31 | 32 | 'supports_credentials' => false, 33 | 34 | ]; 35 | -------------------------------------------------------------------------------- /config/filesystems.php: -------------------------------------------------------------------------------- 1 | env('FILESYSTEM_DISK', 'local'), 17 | 18 | /* 19 | |-------------------------------------------------------------------------- 20 | | Filesystem Disks 21 | |-------------------------------------------------------------------------- 22 | | 23 | | Here you may configure as many filesystem "disks" as you wish, and you 24 | | may even configure multiple disks of the same driver. Defaults have 25 | | been set up for each driver as an example of the required values. 26 | | 27 | | Supported Drivers: "local", "ftp", "sftp", "s3" 28 | | 29 | */ 30 | 31 | 'disks' => [ 32 | 33 | 'local' => [ 34 | 'driver' => 'local', 35 | 'root' => storage_path('app'), 36 | 'throw' => false, 37 | ], 38 | 39 | 'public' => [ 40 | 'driver' => 'local', 41 | 'root' => storage_path('app/public'), 42 | 'url' => env('APP_URL').'/storage', 43 | 'visibility' => 'public', 44 | 'throw' => false, 45 | ], 46 | 47 | 's3' => [ 48 | 'driver' => 's3', 49 | 'key' => env('AWS_ACCESS_KEY_ID'), 50 | 'secret' => env('AWS_SECRET_ACCESS_KEY'), 51 | 'region' => env('AWS_DEFAULT_REGION'), 52 | 'bucket' => env('AWS_BUCKET'), 53 | 'url' => env('AWS_URL'), 54 | 'endpoint' => env('AWS_ENDPOINT'), 55 | 'use_path_style_endpoint' => env('AWS_USE_PATH_STYLE_ENDPOINT', false), 56 | 'throw' => false, 57 | ], 58 | 59 | ], 60 | 61 | /* 62 | |-------------------------------------------------------------------------- 63 | | Symbolic Links 64 | |-------------------------------------------------------------------------- 65 | | 66 | | Here you may configure the symbolic links that will be created when the 67 | | `storage:link` Artisan command is executed. The array keys should be 68 | | the locations of the links and the values should be their targets. 69 | | 70 | */ 71 | 72 | 'links' => [ 73 | public_path('storage') => storage_path('app/public'), 74 | ], 75 | 76 | ]; 77 | -------------------------------------------------------------------------------- /config/hashing.php: -------------------------------------------------------------------------------- 1 | 'bcrypt', 19 | 20 | /* 21 | |-------------------------------------------------------------------------- 22 | | Bcrypt Options 23 | |-------------------------------------------------------------------------- 24 | | 25 | | Here you may specify the configuration options that should be used when 26 | | passwords are hashed using the Bcrypt algorithm. This will allow you 27 | | to control the amount of time it takes to hash the given password. 28 | | 29 | */ 30 | 31 | 'bcrypt' => [ 32 | 'rounds' => env('BCRYPT_ROUNDS', 12), 33 | 'verify' => true, 34 | ], 35 | 36 | /* 37 | |-------------------------------------------------------------------------- 38 | | Argon Options 39 | |-------------------------------------------------------------------------- 40 | | 41 | | Here you may specify the configuration options that should be used when 42 | | passwords are hashed using the Argon algorithm. These will allow you 43 | | to control the amount of time it takes to hash the given password. 44 | | 45 | */ 46 | 47 | 'argon' => [ 48 | 'memory' => 65536, 49 | 'threads' => 1, 50 | 'time' => 4, 51 | 'verify' => true, 52 | ], 53 | 54 | ]; 55 | -------------------------------------------------------------------------------- /config/services.php: -------------------------------------------------------------------------------- 1 | [ 18 | 'domain' => env('MAILGUN_DOMAIN'), 19 | 'secret' => env('MAILGUN_SECRET'), 20 | 'endpoint' => env('MAILGUN_ENDPOINT', 'api.mailgun.net'), 21 | 'scheme' => 'https', 22 | ], 23 | 24 | 'postmark' => [ 25 | 'token' => env('POSTMARK_TOKEN'), 26 | ], 27 | 28 | 'ses' => [ 29 | 'key' => env('AWS_ACCESS_KEY_ID'), 30 | 'secret' => env('AWS_SECRET_ACCESS_KEY'), 31 | 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'), 32 | ], 33 | 34 | 'paypal' => [ 35 | 'client_id' => env('PAYPAL_CLIENT_ID'), 36 | 'secret' => env('PAYPAL_SECRET'), 37 | 'settings' => [ 38 | 'mode' => env('PAYPAL_MODE', 'sandbox'), // Can be 'sandbox' or 'live' 39 | 'http.ConnectionTimeOut' => 30, 40 | 'log.LogEnabled' => true, 41 | 'log.FileName' => storage_path('/logs/paypal.log'), 42 | 'log.LogLevel' => 'ERROR', 43 | ], 44 | ], 45 | ]; 46 | 47 | 48 | 49 | 50 | 51 | 'paypal' => [ 52 | 'client_id' => env('PAYPAL_CLIENT_ID'), 53 | 'secret' => env('PAYPAL_SECRET'), 54 | 'settings' => [ 55 | 'mode' => env('PAYPAL_MODE', 'sandbox'), // Can be 'sandbox' or 'live' 56 | 'http.ConnectionTimeOut' => 30, 57 | 'log.LogEnabled' => true, 58 | 'log.FileName' => storage_path('/logs/paypal.log'), 59 | 'log.LogLevel' => 'ERROR', 60 | ], 61 | ], 62 | ]; 63 | 64 | ''stripe' => [ 65 | 'model' => App\Models\User::class, 66 | 'key' => env('STRIPE_KEY'), 67 | 'secret' => env('STRIPE_SECRET'), 68 | 'webhook' => [ 69 | 'secret' => env('STRIPE_WEBHOOK_SECRET'), 70 | ], 71 | ], 72 | 73 | -------------------------------------------------------------------------------- /config/view.php: -------------------------------------------------------------------------------- 1 | [ 17 | resource_path('views'), 18 | ], 19 | 20 | /* 21 | |-------------------------------------------------------------------------- 22 | | Compiled View Path 23 | |-------------------------------------------------------------------------- 24 | | 25 | | This option determines where all the compiled Blade templates will be 26 | | stored for your application. Typically, this is within the storage 27 | | directory. However, as usual, you are free to change this value. 28 | | 29 | */ 30 | 31 | 'compiled' => env( 32 | 'VIEW_COMPILED_PATH', 33 | realpath(storage_path('framework/views')) 34 | ), 35 | 36 | ]; 37 | -------------------------------------------------------------------------------- /database/.gitignore: -------------------------------------------------------------------------------- 1 | *.sqlite* 2 | -------------------------------------------------------------------------------- /database/factories/UserFactory.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | class UserFactory extends Factory 13 | { 14 | /** 15 | * The current password being used by the factory. 16 | */ 17 | protected static ?string $password; 18 | 19 | /** 20 | * Define the model's default state. 21 | * 22 | * @return array 23 | */ 24 | public function definition(): array 25 | { 26 | return [ 27 | 'name' => fake()->name(), 28 | 'email' => fake()->unique()->safeEmail(), 29 | 'email_verified_at' => now(), 30 | 'password' => static::$password ??= Hash::make('password'), 31 | 'remember_token' => Str::random(10), 32 | ]; 33 | } 34 | 35 | /** 36 | * Indicate that the model's email address should be unverified. 37 | */ 38 | public function unverified(): static 39 | { 40 | return $this->state(fn (array $attributes) => [ 41 | 'email_verified_at' => null, 42 | ]); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /database/migrations/2014_10_12_000000_create_users_table.php: -------------------------------------------------------------------------------- 1 | id(); 16 | $table->string('name'); 17 | $table->string('email')->unique(); 18 | $table->timestamp('email_verified_at')->nullable(); 19 | $table->string('password'); 20 | $table->rememberToken(); 21 | $table->timestamps(); 22 | }); 23 | } 24 | 25 | /** 26 | * Reverse the migrations. 27 | */ 28 | public function down(): void 29 | { 30 | Schema::dropIfExists('users'); 31 | } 32 | }; 33 | -------------------------------------------------------------------------------- /database/migrations/2014_10_12_100000_create_password_reset_tokens_table.php: -------------------------------------------------------------------------------- 1 | string('email')->primary(); 16 | $table->string('token'); 17 | $table->timestamp('created_at')->nullable(); 18 | }); 19 | } 20 | 21 | /** 22 | * Reverse the migrations. 23 | */ 24 | public function down(): void 25 | { 26 | Schema::dropIfExists('password_reset_tokens'); 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /database/migrations/2019_08_19_000000_create_failed_jobs_table.php: -------------------------------------------------------------------------------- 1 | id(); 16 | $table->string('uuid')->unique(); 17 | $table->text('connection'); 18 | $table->text('queue'); 19 | $table->longText('payload'); 20 | $table->longText('exception'); 21 | $table->timestamp('failed_at')->useCurrent(); 22 | }); 23 | } 24 | 25 | /** 26 | * Reverse the migrations. 27 | */ 28 | public function down(): void 29 | { 30 | Schema::dropIfExists('failed_jobs'); 31 | } 32 | }; 33 | -------------------------------------------------------------------------------- /database/migrations/2019_12_14_000001_create_personal_access_tokens_table.php: -------------------------------------------------------------------------------- 1 | id(); 16 | $table->morphs('tokenable'); 17 | $table->string('name'); 18 | $table->string('token', 64)->unique(); 19 | $table->text('abilities')->nullable(); 20 | $table->timestamp('last_used_at')->nullable(); 21 | $table->timestamp('expires_at')->nullable(); 22 | $table->timestamps(); 23 | }); 24 | } 25 | 26 | /** 27 | * Reverse the migrations. 28 | */ 29 | public function down(): void 30 | { 31 | Schema::dropIfExists('personal_access_tokens'); 32 | } 33 | }; 34 | -------------------------------------------------------------------------------- /database/migrations/2023_04_01_000000_create_downloadable_products_table.php: -------------------------------------------------------------------------------- 1 | id(); 16 | $table->foreignId('product_id')->constrained()->onDelete('cascade'); 17 | $table->string('file_url'); 18 | $table->integer('download_limit'); 19 | $table->dateTime('expiration_time')->nullable(); 20 | $table->timestamps(); 21 | }); 22 | } 23 | 24 | /** 25 | * Reverse the migrations. 26 | */ 27 | public function down() 28 | { 29 | Schema::dropIfExists('downloadable_products'); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /database/migrations/2023_04_01_000000_create_payment_methods_table.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | use Illuminate\Support\Facades\Schema; 4 | use Illuminate\Database\Schema\Blueprint; 5 | use Illuminate\Database\Migrations\Migration; 6 | 7 | class CreatePaymentMethodsTable extends Migration 8 | { 9 | /** 10 | * Run the migrations. 11 | */ 12 | public function up() 13 | { 14 | Schema::create('payment_methods', function (Blueprint $table) { 15 | $table->id(); 16 | $table->unsignedBigInteger('user_id'); 17 | $table->string('name'); 18 | $table->text('details'); 19 | $table->boolean('is_default')->default(false); 20 | 21 | $table->foreign('user_id')->references('id')->on('users')->onDelete('cascade'); 22 | $table->timestamps(); 23 | }); 24 | } 25 | 26 | /** 27 | * Reverse the migrations. 28 | */ 29 | public function down() 30 | { 31 | Schema::table('payment_methods', function (Blueprint $table) { 32 | $table->dropForeign(['user_id']); 33 | }); 34 | Schema::dropIfExists('payment_methods'); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /database/migrations/2023_04_01_000000_create_products_table.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | use Illuminate\Support\Facades\Schema; 4 | use Illuminate\Database\Schema\Blueprint; 5 | use Illuminate\Database\Migrations\Migration; 6 | 7 | class CreateProductsTable extends Migration 8 | { 9 | /** 10 | * Run the migrations. 11 | */ 12 | public function up() 13 | { 14 | Schema::create('products', function (Blueprint $table) { 15 | $table->id(); 16 | $table->string('name'); 17 | $table->string('description'); 18 | $table->decimal('price', 8, 2); 19 | $table->string('category'); 20 | $table->integer('inventory_count'); 21 | $table->timestamps(); 22 | }); 23 | } 24 | 25 | /** 26 | * Reverse the migrations. 27 | */ 28 | public function down() 29 | { 30 | Schema::dropIfExists('products'); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /database/migrations/2023_04_01_000000_create_reviews_table.php: -------------------------------------------------------------------------------- 1 | id(); 13 | $table->unsignedBigInteger('user_id'); 14 | $table->unsignedBigInteger('product_id'); 15 | $table->integer('rating'); 16 | $table->text('review'); 17 | $table->boolean('approved')->default(false); 18 | $table->timestamps(); 19 | 20 | $table->foreign('user_id')->references('id')->on('users')->onDelete('cascade'); 21 | $table->foreign('product_id')->references('id')->on('products')->onDelete('cascade'); 22 | }); 23 | } 24 | 25 | public function down() 26 | { 27 | Schema::dropIfExists('reviews'); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /database/migrations/2023_04_01_000000_create_site_settings_table.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | use Illuminate\Support\Facades\Schema; 4 | use Illuminate\Database\Schema\Blueprint; 5 | use Illuminate\Database\Migrations\Migration; 6 | 7 | class CreateSiteSettingsTable extends Migration 8 | { 9 | /** 10 | * Run the migrations. 11 | */ 12 | public function up() 13 | { 14 | Schema::create('site_settings', function (Blueprint $table) { 15 | $table->id(); 16 | $table->string('name')->unique(); 17 | $table->text('value'); 18 | $table->text('description')->nullable(); 19 | $table->timestamps(); 20 | }); 21 | } 22 | 23 | /** 24 | * Reverse the migrations. 25 | */ 26 | public function down() 27 | { 28 | Schema::dropIfExists('site_settings'); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /database/migrations/2023_04_01_000001_create_ratings_table.php: -------------------------------------------------------------------------------- 1 | id(); 13 | $table->unsignedBigInteger('user_id'); 14 | $table->unsignedBigInteger('product_id'); 15 | $table->integer('rating'); 16 | $table->timestamps(); 17 | 18 | $table->foreign('user_id')->references('id')->on('users')->onDelete('cascade'); 19 | $table->foreign('product_id')->references('id')->on('products')->onDelete('cascade'); 20 | }); 21 | } 22 | 23 | public function down() 24 | { 25 | Schema::dropIfExists('ratings'); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /database/migrations/2023_09_26_113707_create_product_categories_table.php: -------------------------------------------------------------------------------- 1 | table, function (Blueprint $table) { 16 | $table->id(); 17 | $table->string('name'); 18 | $table->boolean('is_child')->default(0); 19 | $table->unsignedBigInteger('child_of')->nullable(); 20 | $table->timestamps(); 21 | }); 22 | } 23 | 24 | /** 25 | * Reverse the migrations. 26 | */ 27 | public function down(): void 28 | { 29 | Schema::dropIfExists($this->table); 30 | } 31 | }; 32 | -------------------------------------------------------------------------------- /database/migrations/2023_09_26_113708_create_products_table.php: -------------------------------------------------------------------------------- 1 | table, function (Blueprint $table) { 16 | $table->id(); 17 | $table->string('name'); 18 | $table->text('short_description'); 19 | $table->text('long_description'); 20 | $table->foreignId('category_id')->constrained('product_categories')->onUpdate('cascade')->onDelete('cascade');; 21 | $table->boolean('is_variable')->default(0); 22 | $table->boolean('is_grouped')->default(0); 23 | $table->boolean('is_simple')->default(1); 24 | $table->string('featured_image'); 25 | $table->timestamps(); 26 | }); 27 | } 28 | 29 | /** 30 | * Reverse the migrations. 31 | */ 32 | public function down(): void 33 | { 34 | Schema::dropIfExists($this->table); 35 | } 36 | }; 37 | -------------------------------------------------------------------------------- /database/migrations/2023_09_26_113743_create_customers_table.php: -------------------------------------------------------------------------------- 1 | table, function (Blueprint $table) { 16 | $table->id(); 17 | $table->string('first_name'); 18 | $table->string('last_name'); 19 | $table->string('email'); 20 | $table->integer('phone_number'); 21 | $table->string('address'); 22 | $table->string('city'); 23 | $table->string('state'); 24 | $table->string('postal_code'); 25 | $table->timestamps(); 26 | }); 27 | } 28 | 29 | /** 30 | * Reverse the migrations. 31 | */ 32 | public function down(): void 33 | { 34 | Schema::dropIfExists($this->table); 35 | } 36 | }; 37 | -------------------------------------------------------------------------------- /database/migrations/2023_09_26_113752_create_orders_table.php: -------------------------------------------------------------------------------- 1 | table, function (Blueprint $table) { 17 | $table->id(); 18 | $table->foreignId('customer_id')->constrained()->onUpdate('cascade')->onDelete('cascade'); 19 | $table->string('order_date'); 20 | $table->integer('total_amount'); 21 | $table->integer('payment_status'); 22 | $table->integer('shipping_status'); 23 | $table->timestamps(); 24 | }); 25 | } 26 | 27 | /** 28 | * Reverse the migrations. 29 | */ 30 | public function down(): void 31 | { 32 | Schema::dropIfExists($this->table); 33 | } 34 | }; 35 | -------------------------------------------------------------------------------- /database/migrations/2023_09_27_130936_create_cart_items_table.php: -------------------------------------------------------------------------------- 1 | table, function (Blueprint $table) { 17 | $table->id(); 18 | $table->foreignId('user_id')->constrained()->onDelete('cascade')->onUpdate('cascade'); 19 | $table->foreignId('product_id')->constrained()->onDelete('cascade')->onUpdate('cascade'); 20 | $table->integer('quantity'); 21 | $table->decimal('price', 10, 2); 22 | $table->timestamps(); 23 | }); 24 | } 25 | 26 | /** 27 | * Reverse the migrations. 28 | */ 29 | public function down(): void 30 | { 31 | Schema::dropIfExists($this->table); 32 | } 33 | }; 34 | -------------------------------------------------------------------------------- /database/migrations/2023_09_27_132432_create_order_items_table.php: -------------------------------------------------------------------------------- 1 | table, function (Blueprint $table) { 17 | $table->id(); 18 | $table->foreignId('order_id')->constrained()->onUpdate('cascade')->onDelete('cascade'); 19 | $table->foreignId('product_id')->constrained()->onUpdate('cascade')->onDelete('cascade'); 20 | $table->integer('quantity'); 21 | $table->decimal('price, 10, 2'); 22 | $table->timestamps(); 23 | }); 24 | } 25 | 26 | /** 27 | * Reverse the migrations. 28 | */ 29 | public function down(): void 30 | { 31 | Schema::dropIfExists($this->table); 32 | } 33 | }; 34 | -------------------------------------------------------------------------------- /database/migrations/2023_09_28_010654_create_table_tags.php: -------------------------------------------------------------------------------- 1 | table, function (Blueprint $table) { 16 | $table->id(); 17 | $table->string('name'); 18 | $table->timestamps(); 19 | }); 20 | } 21 | 22 | /** 23 | * Reverse the migrations. 24 | */ 25 | public function down(): void 26 | { 27 | Schema::dropIfExists('table_tags'); 28 | } 29 | }; 30 | -------------------------------------------------------------------------------- /database/migrations/2023_09_28_010821_create_images_table.php: -------------------------------------------------------------------------------- 1 | table, function (Blueprint $table) { 16 | $table->id(); 17 | $table->string('path'); 18 | $table->string('url'); 19 | $table->timestamps(); 20 | }); 21 | } 22 | 23 | /** 24 | * Reverse the migrations. 25 | */ 26 | public function down(): void 27 | { 28 | Schema::dropIfExists('images'); 29 | } 30 | }; 31 | -------------------------------------------------------------------------------- /database/migrations/2023_09_28_010935_create_variations_table.php: -------------------------------------------------------------------------------- 1 | table, function (Blueprint $table) { 16 | $table->id(); 17 | $table->string('name'); 18 | $table->string('value'); 19 | $table->timestamps(); 20 | }); 21 | } 22 | 23 | /** 24 | * Reverse the migrations. 25 | */ 26 | public function down(): void 27 | { 28 | Schema::dropIfExists('variations'); 29 | } 30 | }; 31 | -------------------------------------------------------------------------------- /database/migrations/2023_09_28_011302_create_product_tag_table.php: -------------------------------------------------------------------------------- 1 | table, function (Blueprint $table) { 16 | $table->id(); 17 | $table->foreignId('product_id')->constrained(); 18 | $table->foreignId('tag_id')->constrained(); 19 | $table->timestamps(); 20 | }); 21 | } 22 | 23 | /** 24 | * Reverse the migrations. 25 | */ 26 | public function down(): void 27 | { 28 | Schema::dropIfExists('product_tag'); 29 | } 30 | }; 31 | -------------------------------------------------------------------------------- /database/migrations/2023_09_28_011516_create_product_image_table.php: -------------------------------------------------------------------------------- 1 | table, function (Blueprint $table) { 16 | $table->id(); 17 | $table->foreignId('product_id')->constrained(); 18 | $table->foreignId('image_id')->constrained(); 19 | $table->timestamps(); 20 | }); 21 | } 22 | 23 | /** 24 | * Reverse the migrations. 25 | */ 26 | public function down(): void 27 | { 28 | Schema::dropIfExists('product_image'); 29 | } 30 | }; 31 | -------------------------------------------------------------------------------- /database/migrations/2023_09_28_013747_create_product_variation_table.php: -------------------------------------------------------------------------------- 1 | table, function (Blueprint $table) { 16 | $table->id(); 17 | $table->unsignedBigInteger('variation_id')->nullable(); 18 | $table->integer('quantity'); 19 | $table->integer('price'); 20 | $table->foreignId('product_id')->constrained(); 21 | $table->timestamps(); 22 | }); 23 | } 24 | 25 | /** 26 | * Reverse the migrations. 27 | */ 28 | public function down(): void 29 | { 30 | Schema::dropIfExists('product_variation'); 31 | } 32 | }; 33 | -------------------------------------------------------------------------------- /database/migrations/2023_09_28_014231_create_group_table.php: -------------------------------------------------------------------------------- 1 | table, function (Blueprint $table) { 16 | $table->id(); 17 | $table->string('name'); 18 | $table->integer('discount')->nullable(); 19 | $table->timestamps(); 20 | }); 21 | } 22 | 23 | /** 24 | * Reverse the migrations. 25 | */ 26 | public function down(): void 27 | { 28 | Schema::dropIfExists('group'); 29 | } 30 | }; 31 | -------------------------------------------------------------------------------- /database/migrations/2023_09_28_014549_create_product_group_table.php: -------------------------------------------------------------------------------- 1 | table, function (Blueprint $table) { 16 | $table->id(); 17 | $table->foreignId('product_id')->constrained(); 18 | $table->foreignId('group_id')->constrained(); 19 | $table->timestamps(); 20 | }); 21 | } 22 | 23 | /** 24 | * Reverse the migrations. 25 | */ 26 | public function down(): void 27 | { 28 | Schema::dropIfExists('product_group'); 29 | } 30 | }; 31 | -------------------------------------------------------------------------------- /database/migrations/2023_09_28_015116_create_simple_product_table.php: -------------------------------------------------------------------------------- 1 | table, function (Blueprint $table) { 16 | $table->id(); 17 | $table->integer('quantity'); 18 | $table->integer('price'); 19 | $table->foreignId('product_id')->constrained(); 20 | $table->timestamps(); 21 | }); 22 | } 23 | 24 | /** 25 | * Reverse the migrations. 26 | */ 27 | public function down(): void 28 | { 29 | Schema::dropIfExists('simple_product'); 30 | } 31 | }; 32 | -------------------------------------------------------------------------------- /database/migrations/2023_09_28_021219_create_product_rating_table.php: -------------------------------------------------------------------------------- 1 | table, function (Blueprint $table) { 16 | $table->id(); 17 | $table->integer('rating'); 18 | $table->unsignedBigInteger('product_id'); 19 | $table->unsignedBigInteger('customer_id'); 20 | $table->timestamps(); 21 | 22 | $table->foreign('product_id')->references('id')->on('products'); 23 | $table->foreign('customer_id')->references('id')->on('customers'); 24 | 25 | }); 26 | } 27 | 28 | /** 29 | * Reverse the migrations. 30 | */ 31 | public function down(): void 32 | { 33 | Schema::dropIfExists('product_rating'); 34 | } 35 | }; 36 | -------------------------------------------------------------------------------- /database/migrations/2023_09_28_021229_create_product_reviews_table.php: -------------------------------------------------------------------------------- 1 | table, function (Blueprint $table) { 16 | $table->id(); 17 | $table->text('comments'); 18 | $table->unsignedBigInteger('product_id'); 19 | $table->unsignedBigInteger('customer_id'); 20 | $table->timestamps(); 21 | 22 | $table->foreign('product_id')->references('id')->on('products'); 23 | $table->foreign('customer_id')->references('id')->on('customers'); 24 | 25 | 26 | 27 | }); 28 | } 29 | 30 | /** 31 | * Reverse the migrations. 32 | */ 33 | public function down(): void 34 | { 35 | Schema::dropIfExists('product_reviews'); 36 | } 37 | }; 38 | -------------------------------------------------------------------------------- /database/migrations/2023_09_29_151612_create_invoices_table.php: -------------------------------------------------------------------------------- 1 | table, function (Blueprint $table) { 16 | $table->id(); 17 | $table->foreignId('customer_id')->constrained(); 18 | $table->foreignId('order_id')->constrained(); 19 | $table->timestamp('invoice_date'); 20 | $table->decimal('total_amount', 10, 2); 21 | $table->string('payment_status', 50); 22 | $table->timestamps(); 23 | }); 24 | } 25 | 26 | /** 27 | * Reverse the migrations. 28 | */ 29 | public function down(): void 30 | { 31 | Schema::dropIfExists('invoice'); 32 | } 33 | }; 34 | -------------------------------------------------------------------------------- /database/migrations/2023_10_01_000000_create_inventory_logs_table.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | use Illuminate\Support\Facades\Schema; 4 | use Illuminate\Database\Schema\Blueprint; 5 | use Illuminate\Database\Migrations\Migration; 6 | 7 | class CreateInventoryLogsTable extends Migration 8 | { 9 | public function up() 10 | { 11 | Schema::create('inventory_logs', function (Blueprint $table) { 12 | $table->id(); 13 | $table->unsignedBigInteger('product_id'); 14 | $table->integer('quantity_change'); 15 | $table->text('reason'); 16 | $table->timestamps(); 17 | 18 | $table->foreign('product_id')->references('id')->on('products')->onDelete('cascade'); 19 | }); 20 | } 21 | 22 | public function down() 23 | { 24 | Schema::dropIfExists('inventory_logs'); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /database/migrations/xxxx_xx_xx_create_cart_items_table.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | use Illuminate\Database\Migrations\Migration; 4 | use Illuminate\Database\Schema\Blueprint; 5 | use Illuminate\Support\Facades\Schema; 6 | 7 | class CreateCartItemsTable extends Migration 8 | { 9 | public function up() 10 | { 11 | Schema::create('cart_items', function (Blueprint $table) { 12 | $table->id(); 13 | $table->string('session_id'); 14 | $table->foreignId('product_id')->constrained()->onDelete('cascade'); 15 | $table->integer('quantity'); 16 | $table->timestamps(); 17 | }); 18 | } 19 | 20 | public function down() 21 | { 22 | Schema::dropIfExists('cart_items'); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /database/migrations/xxxx_xx_xx_xxxxxx_create_invoices_table.php: -------------------------------------------------------------------------------- 1 | id(); 13 | $table->unsignedBigInteger('order_id'); 14 | $table->unsignedBigInteger('customer_id'); 15 | $table->timestamp('invoice_date'); 16 | $table->decimal('total_amount', 10, 2); 17 | $table->string('payment_status'); 18 | $table->timestamps(); 19 | 20 | $table->foreign('order_id')->references('id')->on('orders')->onDelete('cascade'); 21 | $table->foreign('customer_id')->references('id')->on('customers')->onDelete('cascade'); 22 | }); 23 | } 24 | 25 | public function down() 26 | { 27 | Schema::dropIfExists('invoices'); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /database/seeders/DatabaseSeeder.php: -------------------------------------------------------------------------------- 1 | create(); 16 | 17 | // \App\Models\User::factory()->create([ 18 | // 'name' => 'Test User', 19 | // 'email' => 'test@example.com', 20 | // ]); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "type": "module", 4 | "scripts": { 5 | "dev": "vite", 6 | "build": "vite build" 7 | }, 8 | "devDependencies": { 9 | "@tailwindcss/forms": "^0.5.7", 10 | "@tailwindcss/typography": "^0.5.13", 11 | "autoprefixer": "^10.4.18", 12 | "laravel-vite-plugin": "^1.0.2", 13 | "postcss": "^8.4.33", 14 | "postcss-nesting": "^12.1.0", 15 | "tailwindcss": "^3.4.1", 16 | "vite": "^5.2.11" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | tests/Unit 10 | 11 | 12 | tests/Feature 13 | 14 | 15 | 16 | 17 | app 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /postcss.config.cjs: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | 'tailwindcss/nesting': 'postcss-nesting', 4 | tailwindcss: {}, 5 | autoprefixer: {}, 6 | }, 7 | } 8 | -------------------------------------------------------------------------------- /public/.htaccess: -------------------------------------------------------------------------------- 1 | 2 | 3 | Options -MultiViews -Indexes 4 | 5 | 6 | RewriteEngine On 7 | 8 | # Handle Authorization Header 9 | RewriteCond %{HTTP:Authorization} . 10 | RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}] 11 | 12 | # Redirect Trailing Slashes If Not A Folder... 13 | RewriteCond %{REQUEST_FILENAME} !-d 14 | RewriteCond %{REQUEST_URI} (.+)/$ 15 | RewriteRule ^ %1 [L,R=301] 16 | 17 | # Send Requests To Front Controller... 18 | RewriteCond %{REQUEST_FILENAME} !-d 19 | RewriteCond %{REQUEST_FILENAME} !-f 20 | RewriteRule ^ index.php [L] 21 | 22 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zinping/Ecommerce-Laravel/50eaea6e05199cc9a649a49637fcd210dbcbbded/public/favicon.ico -------------------------------------------------------------------------------- /public/index.php: -------------------------------------------------------------------------------- 1 | make(Kernel::class); 50 | 51 | $response = $kernel->handle( 52 | $request = Request::capture() 53 | )->send(); 54 | 55 | $kernel->terminate($request, $response); 56 | -------------------------------------------------------------------------------- /public/js/filament/forms/components/key-value.js: -------------------------------------------------------------------------------- 1 | function r({state:i}){return{state:i,rows:[],shouldUpdateRows:!0,init:function(){this.updateRows(),this.rows.length<=0?this.rows.push({key:"",value:""}):this.updateState(),this.$watch("state",(t,e)=>{let s=o=>o===null?0:Array.isArray(o)?o.length:typeof o!="object"?0:Object.keys(o).length;s(t)===0&&s(e)===0||this.updateRows()})},addRow:function(){this.rows.push({key:"",value:""}),this.updateState()},deleteRow:function(t){this.rows.splice(t,1),this.rows.length<=0&&this.addRow(),this.updateState()},reorderRows:function(t){let e=Alpine.raw(this.rows),s=e.splice(t.oldIndex,1)[0];e.splice(t.newIndex,0,s),this.rows=e,this.updateState()},updateRows:function(){if(!this.shouldUpdateRows){this.shouldUpdateRows=!0;return}let t=[];for(let[e,s]of Object.entries(this.state??{}))t.push({key:e,value:s});this.rows=t},updateState:function(){let t={};this.rows.forEach(e=>{e.key===""||e.key===null||(t[e.key]=e.value)}),this.shouldUpdateRows=!1,this.state=t}}}export{r as default}; 2 | -------------------------------------------------------------------------------- /public/js/filament/forms/components/tags-input.js: -------------------------------------------------------------------------------- 1 | function i({state:a,splitKeys:n}){return{newTag:"",state:a,createTag:function(){if(this.newTag=this.newTag.trim(),this.newTag!==""){if(this.state.includes(this.newTag)){this.newTag="";return}this.state.push(this.newTag),this.newTag=""}},deleteTag:function(t){this.state=this.state.filter(e=>e!==t)},reorderTags:function(t){let e=this.state.splice(t.oldIndex,1)[0];this.state.splice(t.newIndex,0,e),this.state=[...this.state]},input:{["x-on:blur"]:"createTag()",["x-model"]:"newTag",["x-on:keydown"](t){["Enter",...n].includes(t.key)&&(t.preventDefault(),t.stopPropagation(),this.createTag())},["x-on:paste"](){this.$nextTick(()=>{if(n.length===0){this.createTag();return}let t=n.map(e=>e.replace(/[/\-\\^$*+?.()|[\]{}]/g,"\\$&")).join("|");this.newTag.split(new RegExp(t,"g")).forEach(e=>{this.newTag=e,this.createTag()})})}}}}export{i as default}; 2 | -------------------------------------------------------------------------------- /public/js/filament/forms/components/textarea.js: -------------------------------------------------------------------------------- 1 | function t({initialHeight:e}){return{init:function(){this.render()},render:function(){this.$el.scrollHeight>0&&(this.$el.style.height=e+"rem",this.$el.style.height=this.$el.scrollHeight+"px")}}}export{t as default}; 2 | -------------------------------------------------------------------------------- /public/js/filament/tables/components/table.js: -------------------------------------------------------------------------------- 1 | function c(){return{collapsedGroups:[],isLoading:!1,selectedRecords:[],shouldCheckUniqueSelection:!0,init:function(){this.$wire.$on("deselectAllTableRecords",()=>this.deselectAllRecords()),this.$watch("selectedRecords",()=>{if(!this.shouldCheckUniqueSelection){this.shouldCheckUniqueSelection=!0;return}this.selectedRecords=[...new Set(this.selectedRecords)],this.shouldCheckUniqueSelection=!1})},mountAction:function(e,s=null){this.$wire.set("selectedTableRecords",this.selectedRecords,!1),this.$wire.mountTableAction(e,s)},mountBulkAction:function(e){this.$wire.set("selectedTableRecords",this.selectedRecords,!1),this.$wire.mountTableBulkAction(e)},toggleSelectRecordsOnPage:function(){let e=this.getRecordsOnPage();if(this.areRecordsSelected(e)){this.deselectRecords(e);return}this.selectRecords(e)},toggleSelectRecordsInGroup:async function(e){if(this.isLoading=!0,this.areRecordsSelected(this.getRecordsInGroupOnPage(e))){this.deselectRecords(await this.$wire.getGroupedSelectableTableRecordKeys(e));return}this.selectRecords(await this.$wire.getGroupedSelectableTableRecordKeys(e)),this.isLoading=!1},getRecordsInGroupOnPage:function(e){let s=[];for(let t of this.$root?.getElementsByClassName("fi-ta-record-checkbox")??[])t.dataset.group===e&&s.push(t.value);return s},getRecordsOnPage:function(){let e=[];for(let s of this.$root?.getElementsByClassName("fi-ta-record-checkbox")??[])e.push(s.value);return e},selectRecords:function(e){for(let s of e)this.isRecordSelected(s)||this.selectedRecords.push(s)},deselectRecords:function(e){for(let s of e){let t=this.selectedRecords.indexOf(s);t!==-1&&this.selectedRecords.splice(t,1)}},selectAllRecords:async function(){this.isLoading=!0,this.selectedRecords=await this.$wire.getAllSelectableTableRecordKeys(),this.isLoading=!1},deselectAllRecords:function(){this.selectedRecords=[]},isRecordSelected:function(e){return this.selectedRecords.includes(e)},areRecordsSelected:function(e){return e.every(s=>this.isRecordSelected(s))},toggleCollapseGroup:function(e){if(this.isGroupCollapsed(e)){this.collapsedGroups.splice(this.collapsedGroups.indexOf(e),1);return}this.collapsedGroups.push(e)},isGroupCollapsed:function(e){return this.collapsedGroups.includes(e)},resetCollapsedGroups:function(){this.collapsedGroups=[]}}}export{c as default}; 2 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: 3 | -------------------------------------------------------------------------------- /resources/css/app.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | -------------------------------------------------------------------------------- /resources/js/app.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zinping/Ecommerce-Laravel/50eaea6e05199cc9a649a49637fcd210dbcbbded/resources/js/app.js -------------------------------------------------------------------------------- /resources/views/checkout/address_autofill.blade.php: -------------------------------------------------------------------------------- 1 | <div> 2 | <input type="text" id="address-input" wire:model="address" placeholder="Enter your address" autocomplete="off"> 3 | <div id="address-suggestions" style="display: none;"> 4 | <ul> 5 | <template x-for="address in addresses" :key="address"> 6 | <li @click="selectAddress(address)"><x-text x-text="address"></li> 7 | </template> 8 | </ul> 9 | </div> 10 | </div> 11 | 12 | <script> 13 | document.addEventListener('livewire:load', function () { 14 | const input = document.getElementById('address-input'); 15 | let autocomplete; 16 | 17 | function initAutocomplete() { 18 | autocomplete = new google.maps.places.Autocomplete(input, {types: ['geocode']}); 19 | autocomplete.addListener('place_changed', fillInAddress); 20 | } 21 | 22 | function fillInAddress() { 23 | const place = autocomplete.getPlace(); 24 | Livewire.emit('setAddress', place.formatted_address); 25 | } 26 | 27 | initAutocomplete(); 28 | 29 | Livewire.on('setAddress', address => { 30 | input.value = address; 31 | document.getElementById('address-suggestions').style.display = 'none'; 32 | }); 33 | }); 34 | </script> 35 | 36 | @livewireStyles 37 | @livewireScripts 38 | -------------------------------------------------------------------------------- /resources/views/checkout/checkout.blade.php: -------------------------------------------------------------------------------- 1 | <div> 2 | @livewire('checkout.address_autofill') 3 | 4 | <form wire:submit.prevent="submitCheckout"> 5 | <div class="form-group"> 6 | <label for="email">Email Address</label> 7 | <input type="email" class="form-control" id="email" wire:model.defer="email" required> 8 | </div> 9 | 10 | <div class="form-group"> 11 | <div class="custom-control custom-checkbox"> 12 | <input type="checkbox" class="custom-control-input" id="guestCheckout" wire:model="guestCheckout"> 13 | <label class="custom-control-label" for="guestCheckout">Checkout as Guest</label> 14 | </div> 15 | </div> 16 | 17 | <div class="form-group" wire:ignore> 18 | <label for="shipping_address">Shipping Address</label> 19 | @livewire('address_autofill', key('address_autofill')) 20 | </div> 21 | 22 | <div class="form-group"> 23 | <label for="paymentMethod">Payment Method</label> 24 | <select class="form-control" id="paymentMethod" wire:model.defer="paymentMethod" required> 25 | <option value="">Select a payment method</option> 26 | <option value="credit_card">Credit Card</option> 27 | <option value="paypal">PayPal</option> 28 | </select> 29 | </div> 30 | 31 | <button type="submit" class="btn btn-primary">Complete Checkout</button> 32 | </form> 33 | </div> 34 | 35 | @livewireStyles 36 | @livewireScripts 37 | -------------------------------------------------------------------------------- /resources/views/checkout/order_summary.blade.php: -------------------------------------------------------------------------------- 1 | <div class="container mt-4"> 2 | <h2>Order Summary</h2> 3 | <div class="row"> 4 | <div class="col-12 col-md-8"> 5 | <h4>Items</h4> 6 | <ul class="list-group mb-3"> 7 | @foreach($items as $item) 8 | <li class="list-group-item d-flex justify-content-between lh-condensed"> 9 | <div> 10 | <h6 class="my-0">{{ $item->name }}</h6> 11 | <small class="text-muted">Quantity: {{ $item->quantity }}</small> 12 | </div> 13 | <span class="text-muted">${{ number_format($item->price, 2) }}</span> 14 | </li> 15 | @endforeach 16 | </ul> 17 | <h4>Costs</h4> 18 | <ul class="list-group mb-3"> 19 | <li class="list-group-item d-flex justify-content-between"> 20 | <span>Total (USD)</span> 21 | <strong>${{ number_format($totalCost, 2) }}</strong> 22 | </li> 23 | </ul> 24 | </div> 25 | <div class="col-12 col-md-4"> 26 | <h4>Shipping Information</h4> 27 | <p>{{ $shippingInfo->name }}<br> 28 | {{ $shippingInfo->address }}<br> 29 | {{ $shippingInfo->city }}, {{ $shippingInfo->state }} {{ $shippingInfo->zip }}<br> 30 | {{ $shippingInfo->country }}<br> 31 | Phone: {{ $shippingInfo->phone }} 32 | </p> 33 | <h4>Estimated Delivery Date</h4> 34 | <p>{{ $estimatedDeliveryDate->format('F j, Y') }}</p> 35 | </div> 36 | </div> 37 | <div class="row"> 38 | <div class="col-12"> 39 | <a href="{{ route('finalizePurchase') }}" class="btn btn-primary btn-lg btn-block">Confirm and Place Order</a> 40 | </div> 41 | </div> 42 | </div> 43 | -------------------------------------------------------------------------------- /resources/views/components/layouts/app.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | {{ config('app.name') }} 11 | 12 | 17 | 18 | @filamentStyles 19 | @vite('resources/css/app.css') 20 | 21 | 22 | 23 | {{ $slot }} 24 | 25 | @livewire('notifications') 26 | 27 | @filamentScripts 28 | @vite('resources/js/app.js') 29 | 30 | 31 | -------------------------------------------------------------------------------- /resources/views/invoices/index.blade.php: -------------------------------------------------------------------------------- 1 | @extends('layouts.app') 2 | 3 | @section('content') 4 |
5 | @livewire('invoices.index') 6 |
7 | @endsection 8 | 9 | @push('scripts') 10 | 13 | @endpush 14 | -------------------------------------------------------------------------------- /resources/views/invoices/show.blade.php: -------------------------------------------------------------------------------- 1 | @extends('layouts.app') 2 | 3 | @section('content') 4 |
5 |

Invoice Details

6 |
7 |

Invoice ID: {{ $invoice->id }}

8 |

Invoice Date: {{ $invoice->invoice_date->format('Y-m-d') }}

9 |

Total Amount: ${{ number_format($invoice->total_amount, 2) }}

10 |

Customer Information

11 |

Name: {{ $invoice->customer->name }}

12 |

Email: {{ $invoice->customer->email }}

13 |
14 |

Products

15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | @foreach($invoice->products as $product) 26 | 27 | 28 | 29 | 30 | 31 | 32 | @endforeach 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 |
Product NameQuantityPriceTotal
{{ $product->name }}{{ $product->pivot->quantity }}${{ number_format($product->pivot->price, 2) }}${{ number_format($product->pivot->quantity * $product->pivot->price, 2) }}
Total Amount${{ number_format($invoice->total_amount, 2) }}
41 | Back to Invoices 42 |
43 | @endsection 44 | -------------------------------------------------------------------------------- /resources/views/livewire/shopping-cart.blade.php: -------------------------------------------------------------------------------- 1 |
2 |

Shopping Cart

3 | @if(count($items) > 0) 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | @foreach($items as $id => $item) 16 | 17 | 18 | 19 | 22 | 23 | 26 | 27 | @endforeach 28 | 29 |
ProductPriceQuantityTotalAction
{{ $item['name'] }}${{ number_format($item['price'], 2) }} 20 | 21 | ${{ number_format($item['price'] * $item['quantity'], 2) }} 24 | 25 |
30 |
31 | Total: ${{ number_format(array_reduce($items, function ($carry, $item) { 32 | return $carry + ($item['price'] * $item['quantity']); 33 | }, 0), 2) }} 34 |
35 |
36 | 37 |
38 | @else 39 |

Your cart is empty.

40 | @endif 41 |
42 | -------------------------------------------------------------------------------- /resources/views/payment_methods/index.blade.php: -------------------------------------------------------------------------------- 1 | @extends('layouts.app') 2 | 3 | @section('content') 4 |
5 |

Manage Payment Methods

6 |
7 |
8 |
9 | @csrf 10 |
11 | 12 | 13 |
14 |
15 | 16 | 17 |
18 | 19 |
20 |
21 |
22 |
23 |
24 | @foreach ($paymentMethods as $method) 25 |
26 |
27 |
28 |
{{ $method->name }}
29 |

{{ $method->details }}

30 | Edit 31 |
32 | @csrf 33 | @method('DELETE') 34 | 35 |
36 |
37 | @csrf 38 |
39 | is_default ? 'checked' : '' }}> 40 | 43 |
44 | 45 |
46 |
47 |
48 |
49 | @endforeach 50 |
51 |
52 | @endsection 53 | 54 | @push('scripts') 55 | 58 | @endpush 59 | -------------------------------------------------------------------------------- /resources/views/product.blade.php: -------------------------------------------------------------------------------- 1 | @extends('layouts.app') 2 | 3 | @section('content') 4 |
5 |
6 |

{{ $product->name }}

7 |

{{ $product->description }}

8 |

Price: ${{ number_format($product->price, 2) }}

9 | 10 |
11 |

Customer Ratings

12 |

Average Rating: {{ number_format($averageRating, 1) }} / 5

13 |

Total Reviews: {{ $reviews->count() }}

14 |
15 |
16 | 17 | 18 | @include('reviews', ['reviews' => $reviews]) 19 |
20 | @endsection 21 | -------------------------------------------------------------------------------- /resources/views/products/create.blade.php: -------------------------------------------------------------------------------- 1 | @extends('layouts.app') 2 | 3 | @section('content') 4 |
5 |

Create Product

6 |
7 | @csrf 8 |
9 | 10 | 11 | @error('name') 12 |
{{ $message }}
13 | @enderror 14 |
15 |
16 | 17 | 18 | @error('description') 19 |
{{ $message }}
20 | @enderror 21 |
22 |
23 | 24 | 25 | @error('price') 26 |
{{ $message }}
27 | @enderror 28 |
29 |
30 | 31 | 32 | @error('category') 33 |
{{ $message }}
34 | @enderror 35 |
36 |
37 | 38 | 39 | @error('inventory_count') 40 |
{{ $message }}
41 | @enderror 42 |
43 | 44 |
45 |
46 | @endsection 47 | -------------------------------------------------------------------------------- /resources/views/products/index.blade.php: -------------------------------------------------------------------------------- 1 | @extends('layouts.app') 2 | 3 | @section('content') 4 |
5 |
6 | @if($products->count() > 0) 7 | @foreach($products as $product) 8 |
9 |
10 |
11 |
{{ $product->name }}
12 |

{{ $product->description }}

13 |

Price: ${{ number_format($product->price, 2) }}

14 |
15 |
16 |
17 | @endforeach 18 | @else 19 |
20 |

No products available.

21 |
22 | @endif 23 |
24 |
25 |
26 | {{ $products->links() }} 27 |
28 |
29 |
30 | @endsection 31 | -------------------------------------------------------------------------------- /resources/views/products/show.blade.php: -------------------------------------------------------------------------------- 1 | @extends('layouts.app') 2 | 3 | @section('content') 4 |
5 |
6 |
7 |
8 |
{{ $product->name }}
9 |
10 | {{ $product->name }} 11 |

Description: {{ $product->description }}

12 |

Price: ${{ number_format($product->price, 2) }}

13 |

Category: {{ $product->category }}

14 |

Inventory Count: {{ $product->inventory_count }}

15 |
16 |
17 | Back to Products 18 |
19 |
20 |
21 | @endsection 22 | 23 | @if($product->downloadable->count() > 0 && auth()->user() && auth()->user()->hasPurchased($product)) 24 | Download 25 | @endif 26 | 27 |
28 |
Inventory Logs
29 |
30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | @foreach ($product->inventoryLogs as $log) 40 | 41 | 42 | 43 | 44 | 45 | @endforeach 46 | 47 |
DateQuantity ChangeReason
{{ $log->created_at->format('Y-m-d H:i:s') }}{{ $log->quantity_change }}{{ $log->reason }}
48 |
49 |
50 | 51 | -------------------------------------------------------------------------------- /resources/views/reviews.blade.php: -------------------------------------------------------------------------------- 1 | @extends('layouts.app') 2 | 3 | @section('content') 4 |
5 |

Product Reviews

6 |
7 |
8 |
9 | 10 | 17 |
18 | 19 |
20 |
21 | @if($reviews->isEmpty()) 22 |

No reviews yet.

23 | @else 24 | @foreach($reviews as $review) 25 |
26 |

{{ $review->author }}

27 |

{{ $review->content }}

28 |

Rating: {{ $review->rating }} / 5

29 |

Date: {{ $review->created_at->toFormattedDateString() }}

30 |
31 | @endforeach 32 | {{ $reviews->links() }} 33 | @endif 34 |
35 | @endsection 36 | -------------------------------------------------------------------------------- /resources/views/site_settings/edit.blade.php: -------------------------------------------------------------------------------- 1 | @extends('layouts.app') 2 | 3 | @section('content') 4 |
5 |

Edit Site Setting

6 |
7 | @csrf 8 | @method('PUT') 9 | 10 |
11 | 12 | 13 |
14 | 15 |
16 | 17 | 18 |
19 | 20 |
21 | 22 | 23 |
24 | 25 | 26 | Back 27 |
28 |
29 | @endsection 30 | -------------------------------------------------------------------------------- /resources/views/site_settings/index.blade.php: -------------------------------------------------------------------------------- 1 | @extends('layouts.app') 2 | 3 | @section('content') 4 |
5 |

Site Settings

6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | @forelse($settings as $setting) 17 | 18 | 19 | 20 | 21 | 24 | 25 | @empty 26 | 27 | 28 | 29 | @endforelse 30 | 31 |
NameValueDescriptionActions
{{ $setting->name }}{{ $setting->value }}{{ $setting->description }} 22 | Edit 23 |
No settings found.
32 |
33 | @endsection 34 | -------------------------------------------------------------------------------- /resources/views/subscriptions/manage.blade.php: -------------------------------------------------------------------------------- 1 | @extends('layouts.app') 2 | 3 | @section('content') 4 |
5 |

Manage Subscription

6 |
7 |

Current Subscription

8 |

Plan: {{ $subscription->stripe_plan }}

9 |

Status: {{ $subscription->stripe_status }}

10 |

Renewal Date: {{ $subscription->ends_at ? $subscription->ends_at->toFormattedDateString() : 'N/A' }}

11 |
12 | 13 |
14 |

Change Plan

15 |
16 | @csrf 17 |
18 | 19 | 24 |
25 | 26 |
27 |
28 | 29 |
30 |

Cancel Subscription

31 |
32 | @csrf 33 | 34 |
35 |
36 |
37 | @endsection 38 | 39 | @section('scripts') 40 | 63 | @endsection 64 | -------------------------------------------------------------------------------- /routes/api.php: -------------------------------------------------------------------------------- 1 | get('/user', function (Request $request) { 18 | return $request->user(); 19 | }); 20 | -------------------------------------------------------------------------------- /routes/channels.php: -------------------------------------------------------------------------------- 1 | id === (int) $id; 18 | }); 19 | -------------------------------------------------------------------------------- /routes/console.php: -------------------------------------------------------------------------------- 1 | comment(Inspiring::quote()); 19 | })->purpose('Display an inspiring quote'); 20 | -------------------------------------------------------------------------------- /storage/app/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !public/ 3 | !.gitignore 4 | -------------------------------------------------------------------------------- /storage/app/public/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /storage/framework/.gitignore: -------------------------------------------------------------------------------- 1 | compiled.php 2 | config.php 3 | down 4 | events.scanned.php 5 | maintenance.php 6 | routes.php 7 | routes.scanned.php 8 | schedule-* 9 | services.json 10 | -------------------------------------------------------------------------------- /storage/framework/cache/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !data/ 3 | !.gitignore 4 | -------------------------------------------------------------------------------- /storage/framework/cache/data/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /storage/framework/sessions/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /storage/framework/testing/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /storage/framework/views/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /storage/logs/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /sweep.yaml: -------------------------------------------------------------------------------- 1 | # Sweep AI turns bugs & feature requests into code changes (https://sweep.dev) 2 | # For details on our config file, check out our docs at https://docs.sweep.dev/usage/config 3 | 4 | # This setting contains a list of rules that Sweep will check for. If any of these rules are broken in a new commit, Sweep will create an pull request to fix the broken rule. 5 | rules: 6 | - "All new business logic should have corresponding unit tests." 7 | - "Refactor large functions to be more modular." 8 | - "Add docstrings to all functions and file headers." 9 | 10 | # This is the branch that Sweep will develop from and make pull requests to. Most people use 'main' or 'master' but some users also use 'dev' or 'staging'. 11 | branch: 'main' 12 | 13 | # By default Sweep will read the logs and outputs from your existing Github Actions. To disable this, set this to false. 14 | gha_enabled: True 15 | 16 | # This is the description of your project. It will be used by sweep when creating PRs. You can tell Sweep what's unique about your project, what frameworks you use, or anything else you want. 17 | # 18 | # Example: 19 | # 20 | # description: sweepai/sweep is a python project. The main api endpoints are in sweepai/api.py. Write code that adheres to PEP8. 21 | description: '' 22 | 23 | # This sets whether to create pull requests as drafts. If this is set to True, then all pull requests will be created as drafts and GitHub Actions will not be triggered. 24 | draft: False 25 | 26 | # This is a list of directories that Sweep will not be able to edit. 27 | blocked_dirs: [] 28 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | import preset from './vendor/filament/support/tailwind.config.preset' 2 | 3 | export default { 4 | presets: [preset], 5 | content: [ 6 | './app/Filament/**/*.php', 7 | './resources/views/**/*.blade.php', 8 | './vendor/filament/**/*.blade.php', 9 | ], 10 | } 11 | -------------------------------------------------------------------------------- /tests/CreatesApplication.php: -------------------------------------------------------------------------------- 1 | make(Kernel::class)->bootstrap(); 18 | 19 | return $app; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /tests/Feature/ExampleTest.php: -------------------------------------------------------------------------------- 1 | get('/'); 16 | 17 | $response->assertStatus(200); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /tests/Feature/InvoiceTest.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | namespace Tests\Feature; 4 | 5 | use Tests\TestCase; 6 | use Illuminate\Foundation\Testing\RefreshDatabase; 7 | use Illuminate\Support\Facades\Mail; 8 | use App\Models\Order; 9 | use App\Models\Invoice; 10 | use App\Mail\InvoiceMail; 11 | 12 | class InvoiceTest extends TestCase 13 | { 14 | use RefreshDatabase; 15 | 16 | public function testAutomaticInvoiceGeneration() 17 | { 18 | // Arrange: Simulate order completion 19 | $order = Order::factory()->create(); 20 | 21 | // Act: Trigger invoice generation 22 | $response = $this->postJson('/api/orders/'.$order->id.'/complete'); 23 | 24 | // Assert: Invoice is automatically generated with correct details 25 | $this->assertDatabaseHas('invoices', [ 26 | 'order_id' => $order->id, 27 | ]); 28 | } 29 | 30 | public function testInvoiceManagementInterfaceAccessibility() 31 | { 32 | // Act: Access the invoice management interface 33 | $response = $this->get('/invoices'); 34 | 35 | // Assert: Interface is accessible 36 | $response->assertStatus(200); 37 | } 38 | 39 | public function testCorrectnessOfDetailedInvoiceView() 40 | { 41 | // Arrange: Create an invoice 42 | $invoice = Invoice::factory()->create(); 43 | 44 | // Act: View the invoice 45 | $response = $this->get('/invoices/'.$invoice->id); 46 | 47 | // Assert: Response contains correct invoice details 48 | $response->assertSee($invoice->total_amount); 49 | } 50 | 51 | public function testSuccessfulPDFGeneration() 52 | { 53 | // Arrange: Create an invoice 54 | $invoice = Invoice::factory()->create(); 55 | 56 | // Act: Request PDF generation 57 | $response = $this->get('/invoices/'.$invoice->id.'/pdf'); 58 | 59 | // Assert: PDF is successfully generated 60 | $response->assertHeader('Content-Type', 'application/pdf'); 61 | } 62 | 63 | public function testEmailSendingWithInvoiceAttached() 64 | { 65 | // Arrange: Mock the mailer 66 | Mail::fake(); 67 | $invoice = Invoice::factory()->create(); 68 | 69 | // Act: Trigger email sending 70 | $this->post('/invoices/'.$invoice->id.'/send'); 71 | 72 | // Assert: Email is sent with the correct attachment 73 | Mail::assertSent(InvoiceMail::class, function ($mail) use ($invoice) { 74 | return $mail->hasTo($invoice->customer->email) && 75 | $mail->attachments->contains(function ($attachment) use ($invoice) { 76 | return $attachment->as == "invoice-{$invoice->id}.pdf"; 77 | }); 78 | }); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /tests/Feature/ProductControllerTest.php: -------------------------------------------------------------------------------- 1 | get('/products/create'); 17 | $response->assertOk(); 18 | $response->assertViewIs('products.create'); 19 | } 20 | 21 | public function testList() 22 | { 23 | $response = $this->get('/products'); 24 | $response->assertOk(); 25 | $response->assertViewIs('products.list'); 26 | } 27 | 28 | public function testShow() 29 | { 30 | $product = \App\Models\Product::factory()->create(); 31 | $response = $this->get("/products/{$product->id}"); 32 | $response->assertOk(); 33 | $response->assertViewIs('products.show'); 34 | $response->assertViewHas('product', $product); 35 | } 36 | 37 | public function testUpdate() 38 | { 39 | $product = \App\Models\Product::factory()->create(); 40 | $updatedData = ['name' => 'Updated Product Name', 'description' => 'Updated Description']; 41 | $response = $this->put("/products/{$product->id}", $updatedData); 42 | $response->assertRedirect('/products'); 43 | $this->assertDatabaseHas('products', $updatedData); 44 | } 45 | 46 | public function testDelete() 47 | { 48 | $product = \App\Models\Product::factory()->create(); 49 | $response = $this->delete("/products/{$product->id}"); 50 | $response->assertRedirect('/products'); 51 | $this->assertDatabaseMissing('products', ['id' => $product->id]); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /tests/TestCase.php: -------------------------------------------------------------------------------- 1 | mock(Request::class); 18 | $request->shouldReceive('validate')->andReturn([ 19 | 'name' => 'Test Product', 20 | 'description' => 'Test Description', 21 | 'price' => 10.99, 22 | 'category' => 'Test Category', 23 | 'inventory_count' => 100, 24 | ]); 25 | 26 | // Create a mock product 27 | $product = $this->mock(Product::class); 28 | $product->shouldReceive('create')->once()->andReturn($product); 29 | 30 | // Call the create method on the ProductController 31 | $controller = new ProductController(); 32 | $response = $controller->create($request); 33 | 34 | // Assert that the inventory log is created 35 | $this->assertDatabaseHas('inventory_logs', [ 36 | 'product_id' => $product->id, 37 | 'quantity_change' => 100, 38 | 'reason' => 'Inventory adjustment', 39 | ]); 40 | } 41 | 42 | public function testUpdate() 43 | { 44 | $product = Product::factory()->create(); 45 | 46 | $payload = [ 47 | 'name' => $this->faker->name, 48 | 'description' => $this->faker->sentence, 49 | 'price' => $this->faker->randomFloat(2, 0, 100), 50 | 'category' => $this->faker->word, 51 | 'inventory_count' => $this->faker->numberBetween(0, 100), 52 | ]; 53 | 54 | $response = $this->put("/products/{$product->id}", $payload); 55 | 56 | $response->assertStatus(200); 57 | $this->assertDatabaseHas('products', $payload); 58 | $this->assertDatabaseHas('inventory_logs', [ 59 | 'quantity_change' => $payload['inventory_count'] - $product->inventory_count, 60 | 'reason' => 'Inventory adjustment', 61 | ]); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /tests/Unit/ExampleTest.php: -------------------------------------------------------------------------------- 1 | assertTrue(true); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /tests/Unit/RatingControllerTest.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | namespace Tests\Unit; 4 | 5 | use App\Http\Controllers\RatingController; 6 | use App\Http\Requests\RatingRequest; 7 | use App\Models\Rating; 8 | use Illuminate\Foundation\Testing\RefreshDatabase; 9 | use Illuminate\Support\Facades\Auth; 10 | use Mockery; 11 | use Tests\TestCase; 12 | 13 | class RatingControllerTest extends TestCase 14 | { 15 | use RefreshDatabase; 16 | 17 | public function testStore() 18 | { 19 | $mockRequest = Mockery::mock(RatingRequest::class); 20 | $mockRequest->shouldReceive('product_id')->andReturn(1); 21 | $mockRequest->shouldReceive('rating')->andReturn(5); 22 | 23 | Auth::shouldReceive('id')->once()->andReturn(1); 24 | 25 | $response = app(RatingController::class)->store($mockRequest); 26 | 27 | $response->assertStatus(201); 28 | $this->assertDatabaseHas('ratings', [ 29 | 'user_id' => 1, 30 | 'product_id' => 1, 31 | 'rating' => 5, 32 | ]); 33 | } 34 | 35 | public function testCalculateAverageRating() 36 | { 37 | Rating::shouldReceive('calculateAverageRating') 38 | ->with(1) 39 | ->once() 40 | ->andReturn(4.5); 41 | 42 | $response = app(RatingController::class)->calculateAverageRating(1); 43 | 44 | $response->assertJson(['averageRating' => 4.5]); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /tests/Unit/RatingTest.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | namespace Tests\Unit; 4 | 5 | use App\Models\Rating; 6 | use Illuminate\Foundation\Testing\RefreshDatabase; 7 | use Tests\TestCase; 8 | 9 | class RatingTest extends TestCase 10 | { 11 | use RefreshDatabase; 12 | 13 | public function testCalculateAverageRating() 14 | { 15 | $productId = 1; 16 | $expectedAverage = 4.5; 17 | 18 | Rating::unguard(); 19 | Rating::insert([ 20 | ['product_id' => $productId, 'rating' => 5], 21 | ['product_id' => $productId, 'rating' => 4], 22 | ]); 23 | Rating::reguard(); 24 | 25 | $averageRating = Rating::calculateAverageRating($productId); 26 | 27 | $this->assertEquals($expectedAverage, $averageRating); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /tests/Unit/ReviewControllerTest.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | namespace Tests\Unit; 4 | 5 | use App\Http\Controllers\ReviewController; 6 | use App\Http\Requests\ReviewRequest; 7 | use App\Models\Review; 8 | use Illuminate\Foundation\Testing\RefreshDatabase; 9 | use Illuminate\Support\Facades\Auth; 10 | use Mockery; 11 | use Tests\TestCase; 12 | 13 | class ReviewControllerTest extends TestCase 14 | { 15 | use RefreshDatabase; 16 | 17 | public function testStore() 18 | { 19 | $mockRequest = Mockery::mock(ReviewRequest::class); 20 | $mockRequest->shouldReceive('product_id')->andReturn(1); 21 | $mockRequest->shouldReceive('rating')->andReturn(5); 22 | $mockRequest->shouldReceive('review')->andReturn('Great product!'); 23 | 24 | Auth::shouldReceive('id')->once()->andReturn(1); 25 | 26 | $response = app(ReviewController::class)->store($mockRequest); 27 | 28 | $response->assertStatus(201); 29 | $this->assertDatabaseHas('reviews', [ 30 | 'user_id' => 1, 31 | 'product_id' => 1, 32 | 'rating' => 5, 33 | 'review' => 'Great product!', 34 | 'approved' => false, 35 | ]); 36 | } 37 | 38 | public function testApprove() 39 | { 40 | $review = Review::create([ 41 | 'user_id' => 1, 42 | 'product_id' => 1, 43 | 'rating' => 5, 44 | 'review' => 'Great product!', 45 | 'approved' => false, 46 | ]); 47 | 48 | $response = app(ReviewController::class)->approve($review->id); 49 | 50 | $this->assertDatabaseHas('reviews', [ 51 | 'id' => $review->id, 52 | 'approved' => true, 53 | ]); 54 | } 55 | 56 | public function testShow() 57 | { 58 | $review = Review::create([ 59 | 'user_id' => 1, 60 | 'product_id' => 1, 61 | 'rating' => 5, 62 | 'review' => 'Great product!', 63 | 'approved' => true, 64 | ]); 65 | 66 | $response = app(ReviewController::class)->show(1); 67 | 68 | $response->assertJson([$review->toArray()]); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /tests/Unit/ReviewTest.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | namespace Tests\Unit; 4 | 5 | use App\Models\Review; 6 | use Illuminate\Foundation\Testing\RefreshDatabase; 7 | use Tests\TestCase; 8 | 9 | class ReviewTest extends TestCase 10 | { 11 | use RefreshDatabase; 12 | 13 | public function testApprove() 14 | { 15 | $review = Review::create([ 16 | 'user_id' => 1, 17 | 'product_id' => 1, 18 | 'rating' => 5, 19 | 'review' => 'Excellent product!', 20 | 'approved' => false, 21 | ]); 22 | 23 | $review->approve(); 24 | 25 | $this->assertTrue($review->approved); 26 | $this->assertDatabaseHas('reviews', [ 27 | 'id' => $review->id, 28 | 'approved' => true, 29 | ]); 30 | } 31 | 32 | public function testReject() 33 | { 34 | $review = Review::create([ 35 | 'user_id' => 2, 36 | 'product_id' => 2, 37 | 'rating' => 4, 38 | 'review' => 'Good product, but has some issues.', 39 | 'approved' => true, 40 | ]); 41 | 42 | $review->reject(); 43 | 44 | $this->assertFalse($review->approved); 45 | $this->assertDatabaseHas('reviews', [ 46 | 'id' => $review->id, 47 | 'approved' => false, 48 | ]); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import laravel, { refreshPaths } from 'laravel-vite-plugin' 3 | 4 | export default defineConfig({ 5 | plugins: [ 6 | laravel({ 7 | input: ['resources/css/app.css', 'resources/js/app.js'], 8 | refresh: [ 9 | ...refreshPaths, 10 | 'app/Filament/**', 11 | 'app/Forms/Components/**', 12 | 'app/Livewire/**', 13 | 'app/Infolists/Components/**', 14 | 'app/Providers/Filament/**', 15 | 'app/Tables/Columns/**', 16 | ], 17 | }), 18 | ], 19 | }) 20 | --------------------------------------------------------------------------------