├── public ├── favicon.ico ├── robots.txt ├── .htaccess └── index.php ├── resources ├── css │ └── app.css ├── views │ ├── sales │ │ ├── edit.blade.php │ │ ├── show.blade.php │ │ └── dashboard.blade.php │ ├── stores │ │ ├── edit.blade.php │ │ ├── index.blade.php │ │ └── create.blade.php │ ├── components │ │ └── search.blade.php │ ├── manufacturers │ │ ├── index.blade.php │ │ ├── show.blade.php │ │ └── create.blade.php │ ├── suppliers │ │ ├── index.blade.php │ │ ├── create.blade.php │ │ └── show.blade.php │ ├── store_products │ │ └── create.blade.php │ ├── reports │ │ └── index.blade.php │ └── products │ │ ├── index.blade.php │ │ ├── create.blade.php │ │ ├── bulk.blade.php │ │ └── show.blade.php └── js │ ├── app.js │ └── bootstrap.js ├── database ├── .gitignore ├── seeders │ ├── DatabaseSeeder.php │ └── StoreSeeder.php ├── migrations │ ├── 2025_02_19_082559_add_qr_code_to_products_table.php │ ├── 2014_10_12_100000_create_password_resets_table.php │ ├── 2025_09_08_160851_add_low_stock_threshold_to_stores_table.php │ ├── 2025_02_18_085429_create_stores_table.php │ ├── 2014_10_12_000000_create_users_table.php │ ├── 2025_09_06_210641_create_manufacturers_table.php │ ├── 2019_08_19_000000_create_failed_jobs_table.php │ ├── 2025_02_19_070701_create_suppliers_table.php │ ├── 2025_09_08_163418_create_expiry_alerts_table.php │ ├── 2025_09_09_165042_create_reports_table.php │ ├── 2025_09_06_223600_create_store_product_table.php │ ├── 2019_12_14_000001_create_personal_access_tokens_table.php │ ├── 2025_09_16_210742_create_sales_predictions_table.php │ ├── 2025_09_16_210749_create_inventory_forecasts_table.php │ ├── 2025_09_09_221237_create_sales_table.php │ └── 2025_02_19_074807_create_products_table.php └── factories │ └── UserFactory.php ├── bootstrap ├── cache │ └── .gitignore └── app.php ├── storage ├── logs │ └── .gitignore ├── app │ ├── public │ │ └── .gitignore │ └── .gitignore └── framework │ ├── testing │ └── .gitignore │ ├── views │ └── .gitignore │ ├── cache │ ├── data │ │ └── .gitignore │ └── .gitignore │ ├── sessions │ └── .gitignore │ └── .gitignore ├── isses.PNG ├── Kanban.PNG ├── use_case.png ├── tests ├── TestCase.php ├── Unit │ └── ExampleTest.php ├── Feature │ └── ExampleTest.php └── CreatesApplication.php ├── .gitattributes ├── app ├── Models │ ├── Report.php │ ├── Manufacturer.php │ ├── ExpiryAlert.php │ ├── Store.php │ ├── SalesPrediction.php │ ├── InventoryForecast.php │ ├── Product.php │ ├── User.php │ ├── Sale.php │ └── Supplier.php ├── Http │ ├── Middleware │ │ ├── EncryptCookies.php │ │ ├── VerifyCsrfToken.php │ │ ├── PreventRequestsDuringMaintenance.php │ │ ├── TrustHosts.php │ │ ├── TrimStrings.php │ │ ├── Authenticate.php │ │ ├── ValidateSignature.php │ │ ├── TrustProxies.php │ │ └── RedirectIfAuthenticated.php │ ├── Controllers │ │ ├── Controller.php │ │ ├── SupplierController.php │ │ ├── StoreProductController.php │ │ ├── DashboardController.php │ │ ├── StoreController.php │ │ ├── ManufacturerController.php │ │ ├── ProductController.php │ │ ├── ReportController.php │ │ └── SaleController.php │ └── Kernel.php ├── Providers │ ├── BroadcastServiceProvider.php │ ├── AppServiceProvider.php │ ├── AuthServiceProvider.php │ ├── EventServiceProvider.php │ └── RouteServiceProvider.php ├── Console │ ├── Kernel.php │ └── Commands │ │ ├── CheckProductExpiry.php │ │ └── TestPredictions.php └── Exceptions │ └── Handler.php ├── .gitignore ├── vite.config.js ├── .editorconfig ├── package.json ├── lang └── en │ ├── pagination.php │ ├── auth.php │ └── passwords.php ├── routes ├── channels.php ├── api.php ├── console.php └── web.php ├── config ├── cors.php ├── services.php ├── view.php ├── hashing.php ├── broadcasting.php ├── sanctum.php ├── filesystems.php ├── queue.php ├── cache.php ├── mail.php ├── auth.php ├── logging.php └── database.php ├── phpunit.xml ├── artisan ├── Dockerfile ├── composer.json ├── README.md ├── SPECIFICATION.md ├── TEST_CASE.md ├── Reflection_on_Challenges_test_cases.md ├── Template_Analysis.md ├── class-diagram-counterfeit-system.md ├── Kanban_Board_Customization.md ├── Kanban_Board_Explanation.md ├── ARCHITECTURE.md ├── User_Stories.md ├── Domain_Model_Counterfeit_System.md └── STAKEHOLDER_ANALYSIS.md /public/favicon.ico: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /resources/css/app.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /database/.gitignore: -------------------------------------------------------------------------------- 1 | *.sqlite* 2 | -------------------------------------------------------------------------------- /resources/views/sales/edit.blade.php: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /bootstrap/cache/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /resources/js/app.js: -------------------------------------------------------------------------------- 1 | import './bootstrap'; 2 | -------------------------------------------------------------------------------- /storage/logs/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: 3 | -------------------------------------------------------------------------------- /storage/app/public/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /storage/app/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !public/ 3 | !.gitignore 4 | -------------------------------------------------------------------------------- /storage/framework/testing/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /storage/framework/views/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /storage/framework/cache/data/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /storage/framework/sessions/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /storage/framework/cache/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !data/ 3 | !.gitignore 4 | -------------------------------------------------------------------------------- /isses.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OkuhleNdlebe/spaza_shop/HEAD/isses.PNG -------------------------------------------------------------------------------- /Kanban.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OkuhleNdlebe/spaza_shop/HEAD/Kanban.PNG -------------------------------------------------------------------------------- /use_case.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OkuhleNdlebe/spaza_shop/HEAD/use_case.png -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /tests/TestCase.php: -------------------------------------------------------------------------------- 1 | assertTrue(true); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /resources/views/stores/edit.blade.php: -------------------------------------------------------------------------------- 1 |
2 | 3 | 5 |
Get alerted when product quantity falls below this number
6 |
-------------------------------------------------------------------------------- /app/Http/Middleware/EncryptCookies.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | protected $except = [ 15 | // 16 | ]; 17 | } 18 | -------------------------------------------------------------------------------- /app/Http/Middleware/VerifyCsrfToken.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | protected $except = [ 15 | // 16 | ]; 17 | } 18 | -------------------------------------------------------------------------------- /app/Http/Controllers/Controller.php: -------------------------------------------------------------------------------- 1 | hasMany(Product::class); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /app/Http/Middleware/PreventRequestsDuringMaintenance.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | protected $except = [ 15 | // 16 | ]; 17 | } 18 | -------------------------------------------------------------------------------- /app/Http/Middleware/TrustHosts.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | public function hosts() 15 | { 16 | return [ 17 | $this->allSubdomainsOfApplicationUrl(), 18 | ]; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /app/Http/Middleware/TrimStrings.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | protected $except = [ 15 | 'current_password', 16 | 'password', 17 | 'password_confirmation', 18 | ]; 19 | } 20 | -------------------------------------------------------------------------------- /tests/Feature/ExampleTest.php: -------------------------------------------------------------------------------- 1 | get('/'); 18 | 19 | $response->assertStatus(200); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /tests/CreatesApplication.php: -------------------------------------------------------------------------------- 1 | make(Kernel::class)->bootstrap(); 19 | 20 | return $app; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /app/Providers/BroadcastServiceProvider.php: -------------------------------------------------------------------------------- 1 | belongsTo(Product::class); 17 | } 18 | 19 | public function store() 20 | { 21 | return $this->belongsTo(Store::class); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /app/Http/Middleware/Authenticate.php: -------------------------------------------------------------------------------- 1 | expectsJson()) { 18 | return route('login'); 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /database/seeders/DatabaseSeeder.php: -------------------------------------------------------------------------------- 1 | create(); 18 | 19 | // \App\Models\User::factory()->create([ 20 | // 'name' => 'Test User', 21 | // 'email' => 'test@example.com', 22 | // ]); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /lang/en/pagination.php: -------------------------------------------------------------------------------- 1 | '« Previous', 17 | 'next' => 'Next »', 18 | 19 | ]; 20 | -------------------------------------------------------------------------------- /routes/channels.php: -------------------------------------------------------------------------------- 1 | id === (int) $id; 18 | }); 19 | -------------------------------------------------------------------------------- /routes/api.php: -------------------------------------------------------------------------------- 1 | get('/user', function (Request $request) { 18 | return $request->user(); 19 | }); 20 | 21 | -------------------------------------------------------------------------------- /routes/console.php: -------------------------------------------------------------------------------- 1 | comment(Inspiring::quote()); 19 | })->purpose('Display an inspiring quote'); 20 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /database/migrations/2025_02_19_082559_add_qr_code_to_products_table.php: -------------------------------------------------------------------------------- 1 | text('qr_code')->nullable(); // Add the qr_code column 18 | }); 19 | } 20 | 21 | public function down() 22 | { 23 | Schema::table('products', function (Blueprint $table) { 24 | $table->dropColumn('qr_code'); 25 | }); 26 | } 27 | 28 | }; 29 | -------------------------------------------------------------------------------- /resources/views/components/search.blade.php: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 |
6 | 8 |
9 |
10 | 13 |
14 |
15 |
16 |
17 |
-------------------------------------------------------------------------------- /lang/en/auth.php: -------------------------------------------------------------------------------- 1 | 'These credentials do not match our records.', 17 | 'password' => 'The provided password is incorrect.', 18 | 'throttle' => 'Too many login attempts. Please try again in :seconds seconds.', 19 | 20 | ]; 21 | -------------------------------------------------------------------------------- /app/Providers/AuthServiceProvider.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | protected $policies = [ 16 | // 'App\Models\Model' => 'App\Policies\ModelPolicy', 17 | ]; 18 | 19 | /** 20 | * Register any authentication / authorization services. 21 | * 22 | * @return void 23 | */ 24 | public function boot() 25 | { 26 | $this->registerPolicies(); 27 | 28 | // 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /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/Models/Store.php: -------------------------------------------------------------------------------- 1 | belongsToMany(Product::class, 'store_product') 17 | ->withPivot('quantity', 'delivered_at', 'expire_date') 18 | ->withTimestamps(); 19 | } 20 | public function lowStockProducts() 21 | { 22 | return $this->belongsToMany(Product::class, 'store_product') 23 | ->wherePivot('quantity', '<=', $this->low_stock_threshold) 24 | ->withPivot('quantity'); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /lang/en/passwords.php: -------------------------------------------------------------------------------- 1 | 'Your password has been reset!', 17 | 'sent' => 'We have emailed your password reset link!', 18 | 'throttled' => 'Please wait before retrying.', 19 | 'token' => 'This password reset token is invalid.', 20 | 'user' => "We can't find a user with that email address.", 21 | 22 | ]; 23 | -------------------------------------------------------------------------------- /app/Console/Kernel.php: -------------------------------------------------------------------------------- 1 | command('products:check-expiry')->daily(); 20 | } 21 | 22 | /** 23 | * Register the commands for the application. 24 | * 25 | * @return void 26 | */ 27 | protected function commands() 28 | { 29 | $this->load(__DIR__.'/Commands'); 30 | 31 | require base_path('routes/console.php'); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /database/migrations/2014_10_12_100000_create_password_resets_table.php: -------------------------------------------------------------------------------- 1 | string('email')->primary(); 18 | $table->string('token'); 19 | $table->timestamp('created_at')->nullable(); 20 | }); 21 | } 22 | 23 | /** 24 | * Reverse the migrations. 25 | * 26 | * @return void 27 | */ 28 | public function down() 29 | { 30 | Schema::dropIfExists('password_resets'); 31 | } 32 | }; 33 | -------------------------------------------------------------------------------- /app/Models/SalesPrediction.php: -------------------------------------------------------------------------------- 1 | 'date', 24 | 'for_date' => 'date', 25 | 'factors' => 'array' 26 | ]; 27 | public function product() 28 | { 29 | return $this->belongsTo(Product::class); 30 | } 31 | 32 | public function store() 33 | { 34 | return $this->belongsTo(Store::class); 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /database/migrations/2025_09_08_160851_add_low_stock_threshold_to_stores_table.php: -------------------------------------------------------------------------------- 1 | integer('low_stock_threshold')->default(5)->after('contact_number'); 19 | 20 | }); 21 | } 22 | 23 | /** 24 | * Reverse the migrations. 25 | * 26 | * @return void 27 | */ 28 | public function down() 29 | { 30 | Schema::table('stores', function (Blueprint $table) { 31 | // 32 | }); 33 | } 34 | }; 35 | -------------------------------------------------------------------------------- /app/Models/InventoryForecast.php: -------------------------------------------------------------------------------- 1 | 'date', 24 | 'forecast_date' => 'date', 25 | 'for_date' => 'date' 26 | ]; 27 | public function product() 28 | { 29 | return $this->belongsTo(Product::class); 30 | } 31 | 32 | public function store() 33 | { 34 | return $this->belongsTo(Store::class); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /database/migrations/2025_02_18_085429_create_stores_table.php: -------------------------------------------------------------------------------- 1 | id(); 18 | $table->string('name'); 19 | $table->string('location'); 20 | $table->string('owner_name'); 21 | $table->string('contact_number'); 22 | $table->timestamps(); 23 | }); 24 | } 25 | 26 | /** 27 | * Reverse the migrations. 28 | * 29 | * @return void 30 | */ 31 | public function down() 32 | { 33 | Schema::dropIfExists('stores'); 34 | } 35 | }; 36 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /database/migrations/2014_10_12_000000_create_users_table.php: -------------------------------------------------------------------------------- 1 | id(); 18 | $table->string('name'); 19 | $table->string('email')->unique(); 20 | $table->timestamp('email_verified_at')->nullable(); 21 | $table->string('password'); 22 | $table->rememberToken(); 23 | $table->timestamps(); 24 | }); 25 | } 26 | 27 | /** 28 | * Reverse the migrations. 29 | * 30 | * @return void 31 | */ 32 | public function down() 33 | { 34 | Schema::dropIfExists('users'); 35 | } 36 | }; 37 | -------------------------------------------------------------------------------- /resources/views/manufacturers/index.blade.php: -------------------------------------------------------------------------------- 1 | @extends('layouts.app') 2 | 3 | @section('title', 'Manufacturers') 4 | 5 | @section('content') 6 |
7 |

Manufacturers

8 | Add Manufacturer 9 |
10 | @foreach($manufacturers as $manufacturer) 11 |
12 |
13 |
14 |
{{ $manufacturer->name }}
15 |

{{ $manufacturer->contact_email }}

16 | View Details 17 |
18 |
19 |
20 | @endforeach 21 |
22 |
23 | @endsection -------------------------------------------------------------------------------- /database/migrations/2025_09_06_210641_create_manufacturers_table.php: -------------------------------------------------------------------------------- 1 | id(); 13 | $table->string('name')->unique(); 14 | $table->string('contact_email')->nullable(); 15 | $table->string('website')->nullable(); 16 | $table->text('address')->nullable(); 17 | $table->timestamps(); 18 | }); 19 | 20 | 21 | } 22 | 23 | public function down() 24 | { 25 | Schema::table('products', function (Blueprint $table) { 26 | $table->dropForeign(['manufacturer_id']); 27 | $table->dropColumn('manufacturer_id'); 28 | }); 29 | Schema::dropIfExists('manufacturers'); 30 | } 31 | }; -------------------------------------------------------------------------------- /database/migrations/2019_08_19_000000_create_failed_jobs_table.php: -------------------------------------------------------------------------------- 1 | id(); 18 | $table->string('uuid')->unique(); 19 | $table->text('connection'); 20 | $table->text('queue'); 21 | $table->longText('payload'); 22 | $table->longText('exception'); 23 | $table->timestamp('failed_at')->useCurrent(); 24 | }); 25 | } 26 | 27 | /** 28 | * Reverse the migrations. 29 | * 30 | * @return void 31 | */ 32 | public function down() 33 | { 34 | Schema::dropIfExists('failed_jobs'); 35 | } 36 | }; 37 | -------------------------------------------------------------------------------- /database/migrations/2025_02_19_070701_create_suppliers_table.php: -------------------------------------------------------------------------------- 1 | id(); // supplier_id (auto-increment) 18 | $table->string('company_name'); 19 | $table->string('contact_person'); 20 | $table->string('phone_number', 20); 21 | $table->string('email')->unique(); 22 | $table->text('address'); 23 | $table->timestamps(); 24 | }); 25 | } 26 | 27 | /** 28 | * Reverse the migrations. 29 | * 30 | * @return void 31 | */ 32 | public function down() 33 | { 34 | Schema::dropIfExists('suppliers'); 35 | } 36 | }; 37 | -------------------------------------------------------------------------------- /database/migrations/2025_09_08_163418_create_expiry_alerts_table.php: -------------------------------------------------------------------------------- 1 | id(); 18 | $table->foreignId('product_id')->constrained(); 19 | $table->foreignId('store_id')->constrained(); 20 | $table->date('expiry_date'); 21 | $table->integer('days_until_expiry'); 22 | $table->boolean('notification_sent')->default(false); 23 | $table->timestamps(); 24 | }); 25 | } 26 | 27 | /** 28 | * Reverse the migrations. 29 | * 30 | * @return void 31 | */ 32 | public function down() 33 | { 34 | Schema::dropIfExists('expiry_alerts'); 35 | } 36 | }; 37 | -------------------------------------------------------------------------------- /app/Http/Middleware/RedirectIfAuthenticated.php: -------------------------------------------------------------------------------- 1 | check()) { 26 | return redirect(RouteServiceProvider::HOME); 27 | } 28 | } 29 | 30 | return $next($request); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /app/Models/Product.php: -------------------------------------------------------------------------------- 1 | belongsTo(Supplier::class); 17 | } 18 | 19 | public function generateQrCode() 20 | { 21 | $productUrl = route('products.show', $this->id); 22 | return QrCode::size(200)->generate($productUrl); 23 | } 24 | public function manufacturer() 25 | { 26 | return $this->belongsTo(Manufacturer::class); 27 | } 28 | 29 | public function stores() 30 | { 31 | return $this->belongsToMany(Store::class, 'store_product') 32 | ->withPivot('quantity', 'delivered_at', 'expire_date') 33 | ->withTimestamps(); 34 | } 35 | public function sales() 36 | { 37 | return $this->hasMany(Sale::class); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /database/migrations/2025_09_09_165042_create_reports_table.php: -------------------------------------------------------------------------------- 1 | id(); 19 | $table->string('name'); 20 | $table->string('type'); // 'sales', 'inventory', 'expiry', 'performance' 21 | $table->json('parameters'); // Store report filters as JSON 22 | $table->text('description')->nullable(); 23 | $table->foreignId('user_id')->constrained(); 24 | $table->timestamps(); 25 | }); 26 | } 27 | 28 | /** 29 | * Reverse the migrations. 30 | * 31 | * @return void 32 | */ 33 | public function down() 34 | { 35 | Schema::dropIfExists('reports'); 36 | } 37 | }; 38 | -------------------------------------------------------------------------------- /resources/views/stores/index.blade.php: -------------------------------------------------------------------------------- 1 | @extends('layouts.app') 2 | 3 | @section('title', 'Spaza Stores') 4 | 5 | @section('content') 6 |
7 |

Spaza Stores

8 | 9 |
10 | @foreach($stores as $store) 11 |
12 |
13 |
14 |
{{ $store->name }}
15 |

Location: {{ $store->location }}

16 |

Owner: {{ $store->owner_name }}

17 | View Details 18 |
19 |
20 |
21 | @endforeach 22 | 23 |
24 | {{ $stores->links() }} 25 |
26 |
27 |
28 | @endsection 29 | -------------------------------------------------------------------------------- /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 | ]; 44 | } 45 | -------------------------------------------------------------------------------- /database/migrations/2025_09_06_223600_create_store_product_table.php: -------------------------------------------------------------------------------- 1 | id(); 18 | $table->foreignId('store_id')->constrained()->onDelete('cascade'); 19 | $table->foreignId('product_id')->constrained()->onDelete('cascade'); 20 | $table->integer('quantity')->default(1); 21 | $table->date('delivered_at')->nullable(); 22 | $table->date('expire_date')->nullable(); 23 | $table->timestamps(); 24 | }); 25 | } 26 | 27 | /** 28 | * Reverse the migrations. 29 | * 30 | * @return void 31 | */ 32 | public function down() 33 | { 34 | Schema::dropIfExists('store_product'); 35 | } 36 | }; 37 | -------------------------------------------------------------------------------- /database/migrations/2019_12_14_000001_create_personal_access_tokens_table.php: -------------------------------------------------------------------------------- 1 | id(); 18 | $table->morphs('tokenable'); 19 | $table->string('name'); 20 | $table->string('token', 64)->unique(); 21 | $table->text('abilities')->nullable(); 22 | $table->timestamp('last_used_at')->nullable(); 23 | $table->timestamp('expires_at')->nullable(); 24 | $table->timestamps(); 25 | }); 26 | } 27 | 28 | /** 29 | * Reverse the migrations. 30 | * 31 | * @return void 32 | */ 33 | public function down() 34 | { 35 | Schema::dropIfExists('personal_access_tokens'); 36 | } 37 | }; 38 | -------------------------------------------------------------------------------- /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 | ]; 35 | -------------------------------------------------------------------------------- /app/Providers/EventServiceProvider.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 | * @return void 27 | */ 28 | public function boot() 29 | { 30 | // 31 | } 32 | 33 | /** 34 | * Determine if events and listeners should be automatically discovered. 35 | * 36 | * @return bool 37 | */ 38 | public function shouldDiscoverEvents() 39 | { 40 | return false; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /database/factories/UserFactory.php: -------------------------------------------------------------------------------- 1 | 10 | */ 11 | class UserFactory extends Factory 12 | { 13 | /** 14 | * Define the model's default state. 15 | * 16 | * @return array 17 | */ 18 | public function definition() 19 | { 20 | return [ 21 | 'name' => fake()->name(), 22 | 'email' => fake()->unique()->safeEmail(), 23 | 'email_verified_at' => now(), 24 | 'password' => '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', // password 25 | 'remember_token' => Str::random(10), 26 | ]; 27 | } 28 | 29 | /** 30 | * Indicate that the model's email address should be unverified. 31 | * 32 | * @return static 33 | */ 34 | public function unverified() 35 | { 36 | return $this->state(fn (array $attributes) => [ 37 | 'email_verified_at' => null, 38 | ]); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /resources/views/suppliers/index.blade.php: -------------------------------------------------------------------------------- 1 | @extends('layouts.app') 2 | 3 | @section('title', 'Suppliers') 4 | 5 | @section('content') 6 |
7 |

Suppliers

8 | Add Supplier 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | @foreach($suppliers as $supplier) 22 | 23 | 24 | 25 | 26 | 27 | 30 | 31 | @endforeach 32 | 33 |
Company NameContact PersonPhoneEmailActions
{{ $supplier->company_name }}{{ $supplier->contact_person }}{{ $supplier->phone_number }}{{ $supplier->email }} 28 | View 29 |
34 |
35 | @endsection 36 | -------------------------------------------------------------------------------- /database/migrations/2025_09_16_210742_create_sales_predictions_table.php: -------------------------------------------------------------------------------- 1 | id(); 18 | $table->foreignId('product_id')->constrained(); 19 | $table->foreignId('store_id')->constrained(); 20 | $table->integer('predicted_quantity'); 21 | $table->decimal('confidence_level', 5, 2); // 0.00 to 1.00 22 | $table->date('prediction_date'); 23 | $table->date('for_date'); 24 | $table->json('factors'); // Seasonality, trends, etc. 25 | $table->timestamps(); 26 | 27 | $table->index(['product_id', 'store_id', 'for_date']); 28 | }); 29 | } 30 | 31 | /** 32 | * Reverse the migrations. 33 | * 34 | * @return void 35 | */ 36 | public function down() 37 | { 38 | Schema::dropIfExists('sales_predictions'); 39 | } 40 | }; 41 | -------------------------------------------------------------------------------- /database/migrations/2025_09_16_210749_create_inventory_forecasts_table.php: -------------------------------------------------------------------------------- 1 | id(); 18 | $table->foreignId('product_id')->constrained(); 19 | $table->foreignId('store_id')->constrained(); 20 | $table->integer('current_stock'); 21 | $table->integer('predicted_demand'); 22 | $table->integer('recommended_order'); 23 | $table->date('stockout_risk_date')->nullable(); 24 | $table->decimal('stockout_probability', 5, 2); 25 | $table->date('forecast_date'); 26 | $table->date('for_date'); 27 | $table->timestamps(); 28 | }); 29 | } 30 | 31 | /** 32 | * Reverse the migrations. 33 | * 34 | * @return void 35 | */ 36 | public function down() 37 | { 38 | Schema::dropIfExists('inventory_forecasts'); 39 | } 40 | }; 41 | -------------------------------------------------------------------------------- /app/Models/Sale.php: -------------------------------------------------------------------------------- 1 | 'datetime', 23 | 'unit_price' => 'decimal:2', 24 | 'total_amount' => 'decimal:2' 25 | ]; 26 | 27 | public function store() 28 | { 29 | return $this->belongsTo(Store::class); 30 | } 31 | 32 | public function product() 33 | { 34 | return $this->belongsTo(Product::class); 35 | } 36 | 37 | // Scope for filtering sales by date range 38 | public function scopeDateRange($query, $startDate, $endDate) 39 | { 40 | return $query->whereBetween('sale_date', [$startDate, $endDate]); 41 | } 42 | 43 | // Scope for sales in the last X days 44 | public function scopeLastDays($query, $days) 45 | { 46 | return $query->where('sale_date', '>=', now()->subDays($days)); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /app/Exceptions/Handler.php: -------------------------------------------------------------------------------- 1 | , \Psr\Log\LogLevel::*> 14 | */ 15 | protected $levels = [ 16 | // 17 | ]; 18 | 19 | /** 20 | * A list of the exception types that are not reported. 21 | * 22 | * @var array> 23 | */ 24 | protected $dontReport = [ 25 | // 26 | ]; 27 | 28 | /** 29 | * A list of the inputs that are never flashed to the session on validation exceptions. 30 | * 31 | * @var array 32 | */ 33 | protected $dontFlash = [ 34 | 'current_password', 35 | 'password', 36 | 'password_confirmation', 37 | ]; 38 | 39 | /** 40 | * Register the exception handling callbacks for the application. 41 | * 42 | * @return void 43 | */ 44 | public function register() 45 | { 46 | $this->reportable(function (Throwable $e) { 47 | // 48 | }); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /database/migrations/2025_09_09_221237_create_sales_table.php: -------------------------------------------------------------------------------- 1 | id(); 18 | $table->foreignId('store_id')->constrained()->onDelete('cascade'); 19 | $table->foreignId('product_id')->constrained()->onDelete('cascade'); 20 | $table->integer('quantity')->default(1); 21 | $table->decimal('unit_price', 10, 2); 22 | $table->decimal('total_amount', 10, 2); 23 | $table->dateTime('sale_date')->default(now()); 24 | $table->text('notes')->nullable(); 25 | $table->timestamps(); 26 | 27 | $table->index('sale_date'); 28 | $table->index(['store_id', 'sale_date']); 29 | }); 30 | } 31 | 32 | /** 33 | * Reverse the migrations. 34 | * 35 | * @return void 36 | */ 37 | public function down() 38 | { 39 | Schema::dropIfExists('sales'); 40 | } 41 | }; 42 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /resources/views/manufacturers/show.blade.php: -------------------------------------------------------------------------------- 1 | @extends('layouts.app') 2 | 3 | @section('title', $manufacturer->name) 4 | 5 | @section('content') 6 |
7 |

{{ $manufacturer->name }}

8 |

Email: {{ $manufacturer->contact_email }}

9 |

Website: {{ $manufacturer->website }}

10 |

Address: {{ $manufacturer->address }}

11 |

QR Code

12 | {!! $qrcode !!} 13 |

Products

14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | @foreach($manufacturer->products as $product) 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | @endforeach 34 | 35 |
NameDescriptionExpiry DatePriceSupplier
{{ $product->name }}{{ $product->description }}{{ $product->expiry_date }}{{ $product->price }}{{ $product->supplier->company_name ?? '' }}
36 |
37 | @endsection -------------------------------------------------------------------------------- /app/Http/Controllers/SupplierController.php: -------------------------------------------------------------------------------- 1 | validate([ 25 | 'company_name' => 'required', 26 | 'contact_person' => 'required', 27 | 'phone_number' => 'required', 28 | 'email' => 'required|email|unique:suppliers,email', 29 | 'address' => 'required', 30 | ]); 31 | 32 | Supplier::create($request->all()); 33 | 34 | return redirect()->route('suppliers.index')->with('success', 'Supplier created successfully.'); 35 | } 36 | 37 | public function show($id) 38 | { 39 | $supplier = Supplier::findOrFail($id); 40 | $qrcode = QrCode::size(200)->generate(route('suppliers.show', $supplier->id)); 41 | 42 | return view('suppliers.show', compact('supplier', 'qrcode')); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /database/migrations/2025_02_19_074807_create_products_table.php: -------------------------------------------------------------------------------- 1 | id(); 18 | $table->string('name'); 19 | $table->text('description')->nullable(); 20 | // Remove old: $table->string('manufacturer'); 21 | $table->unsignedBigInteger('manufacturer_id'); // <-- Foreign key 22 | $table->date('expiry_date'); 23 | $table->decimal('price', 10, 2); 24 | $table->unsignedBigInteger('supplier_id'); 25 | $table->foreign('supplier_id')->references('id')->on('suppliers')->onDelete('cascade'); 26 | $table->foreign('manufacturer_id')->references('id')->on('manufacturers')->onDelete('cascade'); 27 | $table->timestamps(); 28 | }); 29 | } 30 | 31 | /** 32 | * Reverse the migrations. 33 | * 34 | * @return void 35 | */ 36 | public function down() 37 | { 38 | Schema::dropIfExists('products'); 39 | } 40 | }; -------------------------------------------------------------------------------- /app/Http/Controllers/StoreProductController.php: -------------------------------------------------------------------------------- 1 | validate([ 22 | 'product_id' => 'required|exists:products,id', 23 | 'quantity' => 'required|integer|min:1', 24 | 'delivered_at' => 'nullable|date', 25 | ]); 26 | 27 | $product = Product::findOrFail($data['product_id']); 28 | 29 | $store->products()->attach($product->id, [ 30 | 'quantity' => $data['quantity'], 31 | 'delivered_at' => $data['delivered_at'] ?? now(), 32 | 'expire_date' => $product->expire_date, // pulled from product 33 | ]); 34 | 35 | return redirect()->route('stores.show', $store) 36 | ->with('success', 'Product registered to store!'); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /app/Models/Supplier.php: -------------------------------------------------------------------------------- 1 | hasMany(Product::class); 21 | } 22 | 23 | public function performanceMetrics() 24 | { 25 | $products = $this->products()->withCount('stores')->get(); 26 | 27 | return [ 28 | 'total_products' => $products->count(), 29 | 'products_in_stock' => $products->sum('stores_count'), 30 | 'average_delivery_time' => $this->calculateAverageDeliveryTime(), 31 | 'quality_rating' => $this->calculateQualityRating() 32 | ]; 33 | } 34 | 35 | private function calculateAverageDeliveryTime() 36 | { 37 | // This would use actual delivery data 38 | return rand(1, 5); // Placeholder 39 | } 40 | 41 | private function calculateQualityRating() 42 | { 43 | // This would use product quality data 44 | return rand(3, 5); // Placeholder 3-5 star rating 45 | } 46 | 47 | // Add this to track performance 48 | public function performanceLogs() 49 | { 50 | return $this->hasMany(SupplierPerformanceLog::class); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /resources/js/bootstrap.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | window._ = _; 3 | 4 | /** 5 | * We'll load the axios HTTP library which allows us to easily issue requests 6 | * to our Laravel back-end. This library automatically handles sending the 7 | * CSRF token as a header based on the value of the "XSRF" token cookie. 8 | */ 9 | 10 | import axios from 'axios'; 11 | window.axios = axios; 12 | 13 | window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest'; 14 | 15 | /** 16 | * Echo exposes an expressive API for subscribing to channels and listening 17 | * for events that are broadcast by Laravel. Echo and event broadcasting 18 | * allows your team to easily build robust real-time web applications. 19 | */ 20 | 21 | // import Echo from 'laravel-echo'; 22 | 23 | // import Pusher from 'pusher-js'; 24 | // window.Pusher = Pusher; 25 | 26 | // window.Echo = new Echo({ 27 | // broadcaster: 'pusher', 28 | // key: import.meta.env.VITE_PUSHER_APP_KEY, 29 | // cluster: import.meta.env.VITE_PUSHER_APP_CLUSTER ?? 'mt1', 30 | // wsHost: import.meta.env.VITE_PUSHER_HOST ? import.meta.env.VITE_PUSHER_HOST : `ws-${import.meta.env.VITE_PUSHER_APP_CLUSTER}.pusher.com`, 31 | // wsPort: import.meta.env.VITE_PUSHER_PORT ?? 80, 32 | // wssPort: import.meta.env.VITE_PUSHER_PORT ?? 443, 33 | // forceTLS: (import.meta.env.VITE_PUSHER_SCHEME ?? 'https') === 'https', 34 | // enabledTransports: ['ws', 'wss'], 35 | // }); 36 | -------------------------------------------------------------------------------- /resources/views/suppliers/create.blade.php: -------------------------------------------------------------------------------- 1 | @extends('layouts.app') 2 | 3 | @section('title', 'Create Supplier') 4 | 5 | @section('content') 6 |
7 |

Add New Supplier

8 | 9 |
10 | @csrf 11 |
12 | 13 | 14 |
15 |
16 | 17 | 18 |
19 |
20 | 21 | 22 |
23 |
24 | 25 | 26 |
27 |
28 | 29 | 30 |
31 | 32 |
33 |
34 | @endsection 35 | -------------------------------------------------------------------------------- /app/Providers/RouteServiceProvider.php: -------------------------------------------------------------------------------- 1 | configureRateLimiting(); 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 | /** 42 | * Configure the rate limiters for the application. 43 | * 44 | * @return void 45 | */ 46 | protected function configureRateLimiting() 47 | { 48 | RateLimiter::for('api', function (Request $request) { 49 | return Limit::perMinute(60)->by($request->user()?->id ?: $request->ip()); 50 | }); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /resources/views/stores/create.blade.php: -------------------------------------------------------------------------------- 1 | @extends('layouts.app') 2 | 3 | @section('title', 'Add New Store') 4 | 5 | @section('content') 6 |
7 |

Add a New Spaza Store

8 | 9 |
10 |
11 |
12 | @csrf 13 |
14 | 15 | 16 |
17 | 18 |
19 | 20 | 21 |
22 | 23 |
24 | 25 | 26 |
27 | 28 |
29 | 30 | 31 |
32 | 33 | 34 |
35 |
36 |
37 |
38 | @endsection 39 | -------------------------------------------------------------------------------- /resources/views/manufacturers/create.blade.php: -------------------------------------------------------------------------------- 1 | @extends('layouts.app') 2 | 3 | @section('title', 'Add Manufacturer') 4 | 5 | @section('content') 6 |
7 |

Add a Manufacturer

8 |
9 |
10 |
11 | @csrf 12 |
13 | 14 | 15 |
16 |
17 | 18 | 19 |
20 |
21 | 22 | 23 |
24 |
25 | 26 | 27 |
28 | 29 | Cancel 30 |
31 |
32 |
33 |
34 | @endsection -------------------------------------------------------------------------------- /resources/views/store_products/create.blade.php: -------------------------------------------------------------------------------- 1 | @extends('layouts.app') 2 | @section('title', 'Register Product to Store') 3 | @section('content') 4 |
5 |

Add Product to {{ $store->name }}

6 |
7 | @csrf 8 |
9 | 10 | 18 |
19 |
20 | 21 | 22 |
23 |
24 | 25 | 26 |
27 | 28 | Back to Store 29 |
30 |
31 | @endsection -------------------------------------------------------------------------------- /app/Console/Commands/CheckProductExpiry.php: -------------------------------------------------------------------------------- 1 | addDays(7); // 7 days warning 35 | 36 | $expiringProducts = \DB::table('store_product') 37 | ->join('products', 'store_product.product_id', '=', 'products.id') 38 | ->where('store_product.expire_date', '<=', $threshold) 39 | ->where('store_product.expire_date', '>', now()) 40 | ->select('store_product.*', 'products.name') 41 | ->get(); 42 | 43 | foreach ($expiringProducts as $product) { 44 | ExpiryAlert::updateOrCreate( 45 | [ 46 | 'product_id' => $product->product_id, 47 | 'store_id' => $product->store_id 48 | ], 49 | [ 50 | 'expiry_date' => $product->expire_date, 51 | 'days_until_expiry' => now()->diffInDays($product->expire_date) 52 | ] 53 | ); 54 | } 55 | 56 | $this->info('Expiry check completed. ' . $expiringProducts->count() . ' products found.'); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /database/seeders/StoreSeeder.php: -------------------------------------------------------------------------------- 1 | 'Spaza Shop 1', 22 | 'location' => 'Cape Town', 23 | 'owner_name' => 'John Doe', 24 | 'contact_number' => '1234567890', 25 | 'created_at' => now(), 26 | 'updated_at' => now(), 27 | ], 28 | [ 29 | 'name' => 'Spaza Shop 2', 30 | 'location' => 'Johannesburg', 31 | 'owner_name' => 'Jane Smith', 32 | 'contact_number' => '0987654321', 33 | 'created_at' => now(), 34 | 'updated_at' => now(), 35 | ], 36 | [ 37 | 'name' => 'Spaza Shop 3', 38 | 'location' => 'Durban', 39 | 'owner_name' => 'David Johnson', 40 | 'contact_number' => '1122334455', 41 | 'created_at' => now(), 42 | 'updated_at' => now(), 43 | ], 44 | [ 45 | 'name' => 'Spaza Shop 4', 46 | 'location' => 'Pretoria', 47 | 'owner_name' => 'Sarah Adams', 48 | 'contact_number' => '5566778899', 49 | 'created_at' => now(), 50 | 'updated_at' => now(), 51 | ], 52 | ]); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /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', 10), 33 | ], 34 | 35 | /* 36 | |-------------------------------------------------------------------------- 37 | | Argon Options 38 | |-------------------------------------------------------------------------- 39 | | 40 | | Here you may specify the configuration options that should be used when 41 | | passwords are hashed using the Argon algorithm. These will allow you 42 | | to control the amount of time it takes to hash the given password. 43 | | 44 | */ 45 | 46 | 'argon' => [ 47 | 'memory' => 65536, 48 | 'threads' => 1, 49 | 'time' => 4, 50 | ], 51 | 52 | ]; 53 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /app/Http/Controllers/DashboardController.php: -------------------------------------------------------------------------------- 1 | Store::count(), 19 | 'total_products' => Product::count(), 20 | 'total_suppliers' => Supplier::count(), 21 | 'total_manufacturers' => Manufacturer::count(), 22 | 'recent_stores' => Store::latest()->take(5)->get(), 23 | 'recent_products' => Product::with('supplier', 'manufacturer')->latest()->take(5)->get(), 24 | 'stores_with_products' => Store::has('products')->count(), 25 | 'products_near_expiry' => Product::where('expiry_date', '<=', now()->addDays(30))->count(), 26 | 'total_store_products' => \DB::table('store_product')->count(), 27 | ]; 28 | $expiringSoon = ExpiryAlert::with(['product', 'store']) 29 | ->where('days_until_expiry', '<=', 7) 30 | ->where('notification_sent', false) 31 | ->get(); 32 | 33 | $salesData = [ 34 | 'today_sales' => Sale::whereDate('sale_date', today())->sum('total_amount'), 35 | 'week_sales' => Sale::whereBetween('sale_date', [now()->startOfWeek(), now()->endOfWeek()])->sum('total_amount'), 36 | 'top_selling' => Sale::select('product_id', \DB::raw('SUM(quantity) as total_sold')) 37 | ->with('product') 38 | ->groupBy('product_id') 39 | ->orderBy('total_sold', 'DESC') 40 | ->take(5) 41 | ->get() 42 | ]; 43 | 44 | return view('dashboard.dashboard', compact('stats', 'expiringSoon', 'salesData')); 45 | } 46 | } -------------------------------------------------------------------------------- /resources/views/reports/index.blade.php: -------------------------------------------------------------------------------- 1 | @extends('layouts.app') 2 | 3 | @section('title', 'Advanced Reports') 4 | 5 | @section('content') 6 |
7 |

Advanced Reporting

8 | 9 |
10 |
11 |
12 |
13 |
Sales Reports
14 |
15 |
16 |

Generate detailed sales reports by date range and store

17 | Generate Sales Report 18 |
19 |
20 |
21 | 22 |
23 |
24 |
25 |
Inventory Reports
26 |
27 |
28 |

Stock levels, valuation, and inventory health

29 | View Inventory Report 30 |
31 |
32 |
33 | 34 |
35 |
36 |
37 |
Supplier Performance
38 |
39 |
40 |

Analyze supplier delivery times and product quality

41 | Supplier Reports 42 |
43 |
44 |
45 |
46 |
47 | @endsection -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /resources/views/products/index.blade.php: -------------------------------------------------------------------------------- 1 | @extends('layouts.app') 2 | 3 | @section('title', 'Products') 4 | 5 | @section('content') 6 |
7 |

Products

8 | Add Product 9 |
10 |

Products

11 | 20 |
21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | @foreach($products as $product) 36 | 37 | 38 | 39 | 40 | 41 | 42 | 45 | 46 | @endforeach 47 | 48 |
Product NameDescriptionManufacturerExpiry DateSupplierActions
{{ $product->name }}{{ Str::limit($product->description, 50) }}{{ $product->manufacturer ? $product->manufacturer->name : 'N/A' }}{{ $product->expiry_date }}{{ $product->supplier ? $product->supplier->company_name : 'N/A' }} 43 | View Details 44 |
49 |
50 | @endsection -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # -------- Base PHP image -------- 2 | FROM php:8.2-fpm AS base 3 | 4 | # Set working directory 5 | WORKDIR /var/www/html 6 | 7 | # Install system dependencies and PHP extensions 8 | RUN apt-get update && apt-get install -y \ 9 | git unzip libzip-dev libpng-dev libjpeg-dev libfreetype6-dev zip curl cron rsync \ 10 | nodejs npm \ 11 | && docker-php-ext-configure gd --with-freetype --with-jpeg \ 12 | && docker-php-ext-install pdo_mysql zip gd \ 13 | && curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer \ 14 | && apt-get clean && rm -rf /var/lib/apt/lists/* 15 | 16 | # Copy application files 17 | COPY . . 18 | 19 | # Allow Composer to run as root 20 | ENV COMPOSER_ALLOW_SUPERUSER=1 21 | 22 | # Install PHP dependencies 23 | RUN composer install --optimize-autoloader --no-dev 24 | 25 | # Clear and cache Laravel configs 26 | RUN php artisan optimize:clear 27 | 28 | # Set permissions 29 | RUN chown -R www-data:www-data /var/www/html \ 30 | && chmod -R 755 /var/www/html/storage /var/www/html/bootstrap/cache 31 | 32 | # -------- Node build for assets -------- 33 | FROM node:18 AS node_build 34 | 35 | WORKDIR /app 36 | 37 | # Copy app files and vendor from base 38 | COPY --from=base /var/www/html /app 39 | 40 | # Install and build assets (Mix or Vite) 41 | RUN if [ -f "yarn.lock" ]; then \ 42 | yarn install --frozen-lockfile && yarn build; \ 43 | elif [ -f "pnpm-lock.yaml" ]; then \ 44 | corepack enable && corepack prepare pnpm@latest-8 --activate && pnpm install --frozen-lockfile && pnpm run build; \ 45 | elif [ -f "package-lock.json" ]; then \ 46 | npm ci --no-audit && npm run build; \ 47 | else \ 48 | npm install && npm run build; \ 49 | fi 50 | 51 | # Copy built assets back to PHP image 52 | FROM base AS final 53 | 54 | COPY --from=node_build /app/public /var/www/html/public 55 | 56 | # Expose port 8080 (Render default) 57 | EXPOSE 8080 58 | 59 | # Run Laravel using PHP-FPM 60 | CMD ["php", "-S", "0.0.0.0:8080", "-t", "public"] 61 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "laravel/laravel", 3 | "type": "project", 4 | "description": "The Laravel Framework.", 5 | "keywords": ["framework", "laravel"], 6 | "license": "MIT", 7 | "require": { 8 | "php": "^8.0.2", 9 | "guzzlehttp/guzzle": "^7.2", 10 | "laravel/framework": "^9.19", 11 | "laravel/sanctum": "^3.0", 12 | "laravel/tinker": "^2.7", 13 | "simplesoftwareio/simple-qrcode": "^4.2" 14 | }, 15 | "require-dev": { 16 | "fakerphp/faker": "^1.9.1", 17 | "laravel/pint": "^1.0", 18 | "laravel/sail": "^1.0.1", 19 | "mockery/mockery": "^1.4.4", 20 | "nunomaduro/collision": "^6.1", 21 | "phpunit/phpunit": "^9.5.10", 22 | "spatie/laravel-ignition": "^1.0" 23 | }, 24 | "autoload": { 25 | "psr-4": { 26 | "App\\": "app/", 27 | "Database\\Factories\\": "database/factories/", 28 | "Database\\Seeders\\": "database/seeders/" 29 | } 30 | }, 31 | "autoload-dev": { 32 | "psr-4": { 33 | "Tests\\": "tests/" 34 | } 35 | }, 36 | "scripts": { 37 | "post-autoload-dump": [ 38 | "Illuminate\\Foundation\\ComposerScripts::postAutoloadDump", 39 | "@php artisan package:discover --ansi" 40 | ], 41 | "post-update-cmd": [ 42 | "@php artisan vendor:publish --tag=laravel-assets --ansi --force" 43 | ], 44 | "post-root-package-install": [ 45 | "@php -r \"file_exists('.env') || copy('.env.example', '.env');\"" 46 | ], 47 | "post-create-project-cmd": [ 48 | "@php artisan key:generate --ansi" 49 | ] 50 | }, 51 | "extra": { 52 | "laravel": { 53 | "dont-discover": [] 54 | } 55 | }, 56 | "config": { 57 | "optimize-autoloader": true, 58 | "preferred-install": "dist", 59 | "sort-packages": true, 60 | "allow-plugins": { 61 | "pestphp/pest-plugin": true 62 | } 63 | }, 64 | "minimum-stability": "stable", 65 | "prefer-stable": true 66 | } 67 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SpazaSafe: Counterfeit & Expired Product Detection System 2 | 3 | ## Introduction 4 | SpazaSafe is a software system designed to combat the sale of counterfeit and expired products in Spaza shops across South Africa. The system leverages QR codes for product verification, store registration, and delivery tracking to ensure that only authentic and safe products reach consumers. By integrating suppliers, shop owners, and regulators, SpazaSafe enhances transparency and consumer safety in informal retail markets. 5 | 6 | ## Features 7 | - **Product Authentication**: Scan QR codes to verify product authenticity. 8 | - **Supplier & Store Registration**: Maintain a verified database of suppliers and Spaza shops. 9 | - **Delivery Tracking**: Monitor supply chain movements to prevent counterfeit goods. 10 | - **Regulatory Oversight**: Allow regulators to inspect and audit product authenticity. 11 | 12 | ## Project Documentation 13 | - [Specification Document](SPECIFICATION.md) 14 | - [Architecture Document](ARCHITECTURE.md) 15 | - [Reflection on Challenges](Reflection_on_Challenges.md) 16 | - [Stakeholder Analysis](STAKEHOLDER_ANALYSIS.md) 17 | - [System Requirements](SYSTEM_REQUIREMENTS.md) 18 | - [Test Cases](TEST_CASE.md) 19 | - [Use Case Diagram](USE_CASE_DIAGRAM.md) 20 | - [Use Case Specifications](Use_Case_Specifications.md) 21 | - [Challenges in Translating Requirements to Use Cases and Tests](Reflection_on_Challenges_test_cases.md) 22 | - [Agile Planning Document](Agile_Planning_Document.md) 23 | - [Template Analysis](Template_Analysis.md) 24 | - [Kanban Board Customization](Kanban_Board_Customization.md) 25 | - [Kanban Board Explaination](Kanban_Board_Explanation.md) 26 | - [Domain Model Counterfeit System](Domain_Model_Counterfeit_System.md) 27 | - [Class Diagram Counterfeit System](class-diagram-counterfeit-system.md) 28 | - [Reflection Counterfeit System](reflection-counterfeit-system.md) 29 | 30 | 31 | 32 | ## Technologies Used 33 | - **Backend**: Laravel (PHP) 34 | - **Frontend**: TBD (React, Vue, or another framework) 35 | - **Database**: MYSQL 36 | - **QR Code Integration**: Google Charts API or a similar service 37 | - **Hosting**: GitHub & Cloud Services (TBD) 38 | 39 | ## Getting Started 40 | 1. Clone the repository: 41 | ```sh 42 | git clone https://github.com/okuhlendlebe/SpazaSafe.git 43 | -------------------------------------------------------------------------------- /SPECIFICATION.md: -------------------------------------------------------------------------------- 1 | # SpazaSafe: System Specification Document 2 | 3 | ## 1. Introduction 4 | 5 | ### 1.1 Project Title 6 | **SpazaSafe: Counterfeit & Expired Product Detection System** 7 | 8 | ### 1.2 Domain 9 | **Retail & Consumer Safety** 10 | SpazaSafe operates within the **informal retail sector**, specifically targeting Spaza shops in South Africa. It aims to ensure product authenticity and safety through a digital verification system. 11 | 12 | ### 1.3 Problem Statement 13 | The proliferation of counterfeit and expired products in Spaza shops poses a serious health risk, particularly in low-income communities. Consumers, especially children, are vulnerable to unsafe goods, which can lead to severe health consequences, including fatal incidents. The lack of regulatory oversight and supply chain transparency allows fake products to infiltrate the market undetected. 14 | 15 | SpazaSafe addresses this issue by introducing a **QR code-based verification system** that enables: 16 | - **Consumers** to verify products before purchase. 17 | - **Shop owners** to ensure they source from verified suppliers. 18 | - **Suppliers** to track product distribution and prevent counterfeit infiltration. 19 | - **Regulators** to monitor compliance and conduct audits efficiently. 20 | 21 | ### 1.4 Individual Scope & Feasibility Justification 22 | #### **Scope** 23 | SpazaSafe will focus on: 24 | - **Product Authentication:** QR code scanning to verify product legitimacy. 25 | - **Supply Chain Transparency:** Tracking product movement from suppliers to Spaza shops. 26 | - **Regulatory Monitoring:** Providing oversight tools for compliance enforcement. 27 | - **User Management:** Enabling roles for consumers, shop owners, suppliers, and regulators. 28 | 29 | #### **Feasibility Justification** 30 | - **Technical Feasibility:** The system will use Laravel (PHP) for backend processing, MySQL for data storage, and QR code technology for product verification. 31 | - **Economic Feasibility:** The solution is cost-effective since it leverages widely available mobile technology and cloud hosting. 32 | - **Operational Feasibility:** The system is user-friendly, requiring only a smartphone to scan QR codes, making it easy for shop owners and consumers to adopt. 33 | 34 | --- 35 | 36 | ## Next Steps 37 | - Define functional and non-functional requirements. 38 | - Develop a C4 architecture diagram in `ARCHITECTURE.md`. 39 | 40 | -------------------------------------------------------------------------------- /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 | 'host' => env('PUSHER_HOST') ?: 'api-'.env('PUSHER_APP_CLUSTER', 'mt1').'.pusher.com', 40 | 'port' => env('PUSHER_PORT', 443), 41 | 'scheme' => env('PUSHER_SCHEME', 'https'), 42 | 'encrypted' => true, 43 | 'useTLS' => env('PUSHER_SCHEME', 'https') === 'https', 44 | ], 45 | 'client_options' => [ 46 | // Guzzle client options: https://docs.guzzlephp.org/en/stable/request-options.html 47 | ], 48 | ], 49 | 50 | 'ably' => [ 51 | 'driver' => 'ably', 52 | 'key' => env('ABLY_KEY'), 53 | ], 54 | 55 | 'redis' => [ 56 | 'driver' => 'redis', 57 | 'connection' => 'default', 58 | ], 59 | 60 | 'log' => [ 61 | 'driver' => 'log', 62 | ], 63 | 64 | 'null' => [ 65 | 'driver' => 'null', 66 | ], 67 | 68 | ], 69 | 70 | ]; 71 | -------------------------------------------------------------------------------- /resources/views/products/create.blade.php: -------------------------------------------------------------------------------- 1 | @extends('layouts.app') 2 | 3 | @section('title', 'Add Product') 4 | 5 | @section('content') 6 |
7 |

Add Product

8 | 9 |
10 | @csrf 11 | 12 | 13 |
14 | 15 | 21 |
22 | 23 | 24 |
25 | 26 | 27 |
28 | 29 | 30 |
31 | 32 | 33 |
34 | 35 | 36 |
37 | 38 | 44 |
45 | 46 | 47 |
48 | 49 | 50 |
51 | 52 | 53 |
54 | 55 | 56 |
57 | 58 | 59 |
60 |
61 | @endsection -------------------------------------------------------------------------------- /app/Http/Controllers/StoreController.php: -------------------------------------------------------------------------------- 1 | has('search')) { 17 | $search = $request->search; 18 | $query->where('name', 'like', "%{$search}%") 19 | ->orWhere('location', 'like', "%{$search}%") 20 | ->orWhere('owner_name', 'like', "%{$search}%"); 21 | } 22 | 23 | $stores = $query->paginate(10); 24 | return view('stores.index', compact('stores')); 25 | } 26 | 27 | // Show a single store 28 | public function show($id) 29 | { 30 | $store = Store::with(['products' => function($query) { 31 | $query->withPivot('quantity', 'delivered_at', 'expire_date'); 32 | }])->findOrFail($id); 33 | $lowStockProducts = $store->lowStockProducts; 34 | 35 | 36 | $qrCode = QrCode::size(200) 37 | ->backgroundColor(255, 255, 255) 38 | ->color(0, 0, 0) 39 | ->margin(1) 40 | ->generate(route('stores.show', $store->id)); 41 | 42 | return view('stores.show', compact('store', 'qrCode', 'lowStockProducts')); 43 | } 44 | 45 | // Show the create store form 46 | public function create() 47 | { 48 | return view('stores.create'); 49 | } 50 | 51 | // Store a new store in the database 52 | public function store(Request $request) 53 | { 54 | $request->validate([ 55 | 'name' => 'required|string|max:255', 56 | 'location' => 'required|string|max:255', 57 | 'owner_name' => 'required|string|max:255', 58 | 'contact_number' => 'required|string|max:15', 59 | ]); 60 | 61 | $store = Store::create($request->all()); 62 | 63 | return redirect()->route('stores.show', $store->id)->with('success', 'Store created successfully.'); 64 | } 65 | 66 | // Generate and display QR code for a store 67 | public function generateQRCode($id) 68 | { 69 | $store = Store::findOrFail($id); 70 | $qrcode = QrCode::size(200)->generate(route('stores.show', $store->id)); 71 | 72 | return response($qrcode)->header('Content-Type', 'image/png'); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /app/Http/Controllers/ManufacturerController.php: -------------------------------------------------------------------------------- 1 | findOrFail($id); 20 | $qrcode = QrCode::size(200)->generate(route('manufacturers.show', $manufacturer->id)); 21 | return view('manufacturers.show', compact('manufacturer', 'qrcode')); 22 | } 23 | 24 | public function create() 25 | { 26 | return view('manufacturers.create'); 27 | } 28 | 29 | public function store(Request $request) 30 | { 31 | $request->validate([ 32 | 'name' => 'required|unique:manufacturers,name', 33 | 'contact_email' => 'nullable|email', 34 | 'website' => 'nullable|url', 35 | 'address' => 'nullable|string', 36 | ]); 37 | $manufacturer = Manufacturer::create($request->all()); 38 | return redirect()->route('manufacturers.show', $manufacturer->id)->with('success', 'Manufacturer created!'); 39 | } 40 | 41 | public function edit($id) 42 | { 43 | $manufacturer = Manufacturer::findOrFail($id); 44 | return view('manufacturers.edit', compact('manufacturer')); 45 | } 46 | 47 | public function update(Request $request, $id) 48 | { 49 | $manufacturer = Manufacturer::findOrFail($id); 50 | $request->validate([ 51 | 'name' => 'required|unique:manufacturers,name,' . $manufacturer->id, 52 | 'contact_email' => 'nullable|email', 53 | 'website' => 'nullable|url', 54 | 'address' => 'nullable|string', 55 | ]); 56 | $manufacturer->update($request->all()); 57 | return redirect()->route('manufacturers.show', $manufacturer->id)->with('success', 'Manufacturer updated!'); 58 | } 59 | 60 | public function destroy($id) 61 | { 62 | $manufacturer = Manufacturer::findOrFail($id); 63 | $manufacturer->delete(); 64 | return redirect()->route('manufacturers.index')->with('success', 'Manufacturer deleted!'); 65 | } 66 | } -------------------------------------------------------------------------------- /TEST_CASE.md: -------------------------------------------------------------------------------- 1 | # Test Case Development 2 | 3 | ## Functional Test Cases 4 | | Test Case ID | Requirement ID | Description | Steps | Expected Result | Actual Result | Status (Pass/Fail) | 5 | |-------------|---------------|-------------|-------|----------------|--------------|------------------| 6 | | TC-001 | FR-001 | Verify product authenticity using QR code scanner | 1. Open QR scanner
2. Scan product QR code | Product authenticity status displayed within 1 second | | | 7 | | TC-002 | FR-002 | Alert shop owners about upcoming product expiry dates | 1. Add products with expiry dates
2. Wait for 30-day threshold | Expiry alert generated at least 30 days before expiry | | | 8 | | TC-003 | FR-003 | Report counterfeit product | 1. Enter product details
2. Attach photos
3. Submit report | Report successfully submitted and stored | | | 9 | | TC-004 | FR-004 | Customers provide feedback on product quality | 1. Open feedback form
2. Submit feedback | Acknowledgment within 24 hours | | | 10 | | TC-005 | FR-005 | Access educational resources on counterfeit products | 1. Open resources section
2. Click article link | Resource content displayed successfully | | | 11 | | TC-006 | FR-006 | Generate compliance reports | 1. Log in as regulator
2. Open compliance section | Monthly compliance report displayed | | | 12 | | TC-007 | FR-007 | Shop owner manages inventory | 1. Add product
2. Check inventory updates | Inventory updated in real-time | | | 13 | | TC-008 | FR-008 | Send alerts about counterfeit reports | 1. Detect counterfeit
2. Send alert | Alert sent via email/SMS within 5 minutes | | | 14 | 15 | ## Non-Functional Test Cases 16 | | Test Case ID | Requirement ID | Description | Steps | Expected Result | Actual Result | Status (Pass/Fail) | 17 | |-------------|---------------|-------------|-------|----------------|--------------|------------------| 18 | | NTC-001 | NFR-001 (Performance) | System handles 1,000 concurrent users verifying products | 1. Simulate 1,000 users scanning QR codes | Response time ≤ 2 seconds for each request | | | 19 | | NTC-002 | NFR-002 (Security) | Multi-factor authentication (MFA) for login | 1. Attempt login with username/password
2. Enter MFA code | Only authorized users gain access | | | 20 | 21 | ## Notes 22 | - "Actual Result" and "Status" columns will be updated after testing. 23 | - Non-functional tests include **performance testing** and **security validation**. 24 | 25 | --- 26 | 27 | **Author:** Okuhle Ndlebe 28 | **Date:** 13 March 2025 29 | -------------------------------------------------------------------------------- /resources/views/suppliers/show.blade.php: -------------------------------------------------------------------------------- 1 | @extends('layouts.app') 2 | 3 | @section('title', 'Supplier Details') 4 | 5 | @section('content') 6 |
7 |
8 |

{{ $supplier->company_name }}

9 |

Contact Person: {{ $supplier->contact_person }}

10 |

Phone: {{ $supplier->phone_number }}

11 |

Email: {{ $supplier->email }}

12 |

Address: {{ $supplier->address }}

13 | 14 |

QR Code

15 |
{!! $qrcode !!}
16 | 17 |

Products

18 |
19 | 20 | @if($supplier->products->count()) 21 |
22 |
23 |
Performance Metrics
24 |
25 |
26 | @php $metrics = $supplier->performanceMetrics(); @endphp 27 | 28 |
29 |
30 |
31 |

{{ $metrics['total_products'] }}

32 |

Total Products

33 |
34 |
35 |
36 |
37 |

{{ $metrics['products_in_stock'] }}

38 |

In Stock

39 |
40 |
41 |
42 |
43 |

{{ $metrics['average_delivery_time'] }} days

44 |

Avg. Delivery Time

45 |
46 |
47 |
48 |
49 |

50 | @for($i = 1; $i <= 5; $i++) 51 | 52 | @endfor 53 |

54 |

Quality Rating

55 |
56 |
57 |
58 | 59 |
60 |
Performance Over Time
61 | 62 |
63 |
64 |
65 | @endif 66 |
67 | @endsection 68 | -------------------------------------------------------------------------------- /Reflection_on_Challenges_test_cases.md: -------------------------------------------------------------------------------- 1 | # Reflection: Challenges in Translating Requirements to Use Cases and Tests 2 | 3 | ## Introduction 4 | Translating requirements into use cases and tests is a critical step in software development. It ensures that the system meets stakeholder expectations and functions correctly. However, this process is fraught with challenges that can impact the final product's quality and success. Below, I discuss the key challenges I faced during this translation process. 5 | 6 | ## Challenges Faced 7 | 8 | ### Understanding Stakeholder Requirements 9 | One of the primary challenges I faced was gaining a clear and comprehensive understanding of stakeholder requirements. Stakeholders often have diverse and sometimes conflicting needs, which can result in ambiguous or incomplete requirements. Misunderstanding these requirements can lead to incorrect or incomplete use cases, ultimately affecting the system's functionality. 10 | 11 | ### Defining Clear and Concise Use Cases 12 | Another significant challenge was defining use cases that were clear, concise, and comprehensive. Use cases must capture all possible interactions between the user and the system while remaining easy to understand. Overly complex use cases can be difficult to implement and test, while overly simplistic use cases may miss critical functionality. 13 | 14 | ### Identifying All Possible Scenarios 15 | Ensuring that all possible scenarios were identified and documented in the use cases was another significant challenge. Missing scenarios can lead to gaps in the system's functionality and unexpected behavior during operation. 16 | 17 | ### Writing Effective Test Cases 18 | Translating use cases into effective test cases was crucial for validating the system's functionality. However, writing test cases that covered all possible scenarios and edge cases was challenging. Incomplete test cases can result in undetected bugs and system failures. 19 | 20 | ### Balancing Functional and Non-Functional Requirements 21 | Balancing functional and non-functional requirements in use cases and tests was another challenge. While functional requirements define what the system should do, non-functional requirements specify how the system should perform. Both are equally important for the system's success. 22 | 23 | ### Maintaining Consistency and Traceability 24 | Maintaining consistency and traceability between requirements, use cases, and tests was crucial for successful project delivery. Inconsistencies can lead to misunderstandings, missed requirements, and defects. 25 | 26 | -------------------------------------------------------------------------------- /Template_Analysis.md: -------------------------------------------------------------------------------- 1 | # Template Analysis and Selection 2 | 3 | ## Comparison of GitHub Project Templates 4 | 5 | | Template Name | Columns | Workflows | Automation Features | Suitability for Agile Methodologies | 6 | |----------------------|--------------------------------|----------------------------|--------------------------------------------|--------------------------------------------------| 7 | | Basic Kanban | To Do, In Progress, Done | Manual | None | Good for simple tracking, but lacks automation | 8 | | Automated Kanban | To Do, In Progress, In Review, Done | Issues move automatically based on status changes | Auto-move issues, auto-assign reviewers | Excellent for Agile, supports sprint tracking and reduces manual work | 9 | | Bug Triage | New Issues, Triage, In Progress, Done | Manual, with focus on bug tracking | None | Suitable for bug-heavy projects, less general purpose | 10 | | Team Planning | Backlog, To Do, In Progress, Done | Manual | None | Good for high-level planning, lacks task automation | 11 | 12 | ## Justification for Chosen Template 13 | 14 | ### Chosen Template: Automated Kanban 15 | 16 | **Reasoning:** 17 | The Automated Kanban template is the most suitable for the SpazaSafe project due to the following reasons: 18 | 19 | 1. **Automation Features**: 20 | - The template includes automation features such as auto-moving issues based on status changes and auto-assigning reviewers. These features will significantly reduce manual effort and improve the efficiency of managing tasks. 21 | 22 | 2. **Columns and Workflows**: 23 | - The columns in the Automated Kanban template (To Do, In Progress, In Review, Done) align perfectly with the Agile methodology, allowing for clear tracking of task progress through different stages. 24 | 25 | 3. **Suitability for Agile Methodologies**: 26 | - The Automated Kanban template supports Agile methodologies by facilitating sprint tracking, visualizing workflows, and ensuring that no task is left untracked. The built-in automation helps in maintaining an organized and up-to-date board with minimal manual intervention. 27 | 28 | By using the Automated Kanban template, the project team can focus more on delivering value and less on administrative tasks, ensuring a smooth and efficient workflow throughout the project lifecycle. -------------------------------------------------------------------------------- /config/sanctum.php: -------------------------------------------------------------------------------- 1 | explode(',', env('SANCTUM_STATEFUL_DOMAINS', sprintf( 19 | '%s%s', 20 | 'localhost,localhost:3000,127.0.0.1,127.0.0.1:8000,::1', 21 | Sanctum::currentApplicationUrlWithPort() 22 | ))), 23 | 24 | /* 25 | |-------------------------------------------------------------------------- 26 | | Sanctum Guards 27 | |-------------------------------------------------------------------------- 28 | | 29 | | This array contains the authentication guards that will be checked when 30 | | Sanctum is trying to authenticate a request. If none of these guards 31 | | are able to authenticate the request, Sanctum will use the bearer 32 | | token that's present on an incoming request for authentication. 33 | | 34 | */ 35 | 36 | 'guard' => ['web'], 37 | 38 | /* 39 | |-------------------------------------------------------------------------- 40 | | Expiration Minutes 41 | |-------------------------------------------------------------------------- 42 | | 43 | | This value controls the number of minutes until an issued token will be 44 | | considered expired. If this value is null, personal access tokens do 45 | | not expire. This won't tweak the lifetime of first-party sessions. 46 | | 47 | */ 48 | 49 | 'expiration' => null, 50 | 51 | /* 52 | |-------------------------------------------------------------------------- 53 | | Sanctum Middleware 54 | |-------------------------------------------------------------------------- 55 | | 56 | | When authenticating your first-party SPA with Sanctum you may need to 57 | | customize some of the middleware Sanctum uses while processing the 58 | | request. You may change the middleware listed below as required. 59 | | 60 | */ 61 | 62 | 'middleware' => [ 63 | 'verify_csrf_token' => App\Http\Middleware\VerifyCsrfToken::class, 64 | 'encrypt_cookies' => App\Http\Middleware\EncryptCookies::class, 65 | ], 66 | 67 | ]; 68 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /class-diagram-counterfeit-system.md: -------------------------------------------------------------------------------- 1 | ```mermaid 2 | classDiagram 3 | %% Classes 4 | class User { 5 | -userId: String 6 | -name: String 7 | -email: String 8 | -role: String 9 | +submitReport() 10 | +viewAlerts() 11 | } 12 | 13 | class Product { 14 | -productId: String 15 | -name: String 16 | -category: String 17 | -authenticityScore: Number 18 | -status: String 19 | +verifyAuthenticity() 20 | +updateStatus() 21 | } 22 | 23 | class Report { 24 | -reportId: String 25 | -description: String 26 | -timestamp: Date 27 | -status: String 28 | +create() 29 | +updateStatus() 30 | } 31 | 32 | class Alert { 33 | -alertId: String 34 | -severity: String 35 | -timestamp: Date 36 | +generate() 37 | +resolve() 38 | } 39 | 40 | class ComplianceDashboard { 41 | -dashboardId: String 42 | -totalReports: Number 43 | -resolvedReports: Number 44 | -activeAlerts: Number 45 | +generateSummary() 46 | +viewDetails() 47 | } 48 | 49 | %% Relationships 50 | User "1" -- "0..*" Report : submits 51 | Report "1" -- "1" Product : associatedWith 52 | Product "1" -- "0..1" Alert : triggers 53 | User "1" -- "1" ComplianceDashboard : associatedWith 54 | ComplianceDashboard "1" -- "0..*" Alert : aggregates 55 | ComplianceDashboard "1" -- "0..*" Report : aggregates 56 | ``` 57 | 58 | ### Explanation of Key Design Decisions 59 | 60 | 1. **Classes and Attributes**: 61 | - The `User` class represents system users, with attributes such as `userId`, `name`, `email`, and `role`, which determine their permissions. 62 | - The `Product` class contains attributes like `authenticityScore` and `status` to handle counterfeit detection. 63 | - The `Report` class tracks user-submitted reports with details like `description` and `status`. 64 | - The `Alert` class is used for system-triggered notifications, with attributes like `severity` and `timestamp`. 65 | - The `ComplianceDashboard` class aggregates data for user analysis, such as active alerts and resolved reports. 66 | 67 | 2. **Relationships**: 68 | - A `User` can submit multiple `Reports` (`1` to `0..*`). 69 | - Each `Report` is associated with exactly one `Product` (`1` to `1`). 70 | - A `Product` can trigger at most one `Alert` (`1` to `0..1`). 71 | - Each `User` has one `ComplianceDashboard` (`1` to `1`). 72 | - The `ComplianceDashboard` aggregates multiple `Reports` and `Alerts` (`1` to `0..*`). 73 | 74 | 3. **Multiplicity**: 75 | - The multiplicity ensures a clear understanding of how many instances of one class can relate to another. 76 | 77 | 4. **Methods**: 78 | - Methods like `submitReport()` in `User` and `generateSummary()` in `ComplianceDashboard` reflect key functionalities of the system. 79 | 80 | This class diagram visually represents the counterfeit system and its data-driven relationships, ensuring clarity and alignment with functional requirements. -------------------------------------------------------------------------------- /app/Http/Kernel.php: -------------------------------------------------------------------------------- 1 | 15 | */ 16 | protected $middleware = [ 17 | // \App\Http\Middleware\TrustHosts::class, 18 | \App\Http\Middleware\TrustProxies::class, 19 | \Illuminate\Http\Middleware\HandleCors::class, 20 | \App\Http\Middleware\PreventRequestsDuringMaintenance::class, 21 | \Illuminate\Foundation\Http\Middleware\ValidatePostSize::class, 22 | \App\Http\Middleware\TrimStrings::class, 23 | \Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class, 24 | ]; 25 | 26 | /** 27 | * The application's route middleware groups. 28 | * 29 | * @var array> 30 | */ 31 | protected $middlewareGroups = [ 32 | 'web' => [ 33 | \App\Http\Middleware\EncryptCookies::class, 34 | \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class, 35 | \Illuminate\Session\Middleware\StartSession::class, 36 | \Illuminate\View\Middleware\ShareErrorsFromSession::class, 37 | \App\Http\Middleware\VerifyCsrfToken::class, 38 | \Illuminate\Routing\Middleware\SubstituteBindings::class, 39 | ], 40 | 41 | 'api' => [ 42 | // \Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class, 43 | 'throttle:api', 44 | \Illuminate\Routing\Middleware\SubstituteBindings::class, 45 | ], 46 | ]; 47 | 48 | /** 49 | * The application's route middleware. 50 | * 51 | * These middleware may be assigned to groups or used individually. 52 | * 53 | * @var array 54 | */ 55 | protected $routeMiddleware = [ 56 | 'auth' => \App\Http\Middleware\Authenticate::class, 57 | 'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class, 58 | 'auth.session' => \Illuminate\Session\Middleware\AuthenticateSession::class, 59 | 'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class, 60 | 'can' => \Illuminate\Auth\Middleware\Authorize::class, 61 | 'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class, 62 | 'password.confirm' => \Illuminate\Auth\Middleware\RequirePassword::class, 63 | 'signed' => \App\Http\Middleware\ValidateSignature::class, 64 | 'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class, 65 | 'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class, 66 | ]; 67 | } 68 | -------------------------------------------------------------------------------- /Kanban_Board_Customization.md: -------------------------------------------------------------------------------- 1 | # Custom Kanban Board Customization 2 | 3 | ## Custom Columns 4 | - **To Do** 5 | - **In Progress** 6 | - **In Review** 7 | - **Testing**: Added to align with QA requirements and ensure thorough testing of features before they are marked as done. 8 | - **Blocked**: Added to identify tasks that are currently blocked due to dependencies or other issues, allowing the team to address these blockers promptly. 9 | - **Done** 10 | 11 | ## Task Assignments and Statuses 12 | Each task from the sprint plan has been assigned to team members using @mentions and placed in the appropriate column based on its current status. This ensures clear visibility and accountability for all tasks in the sprint. 13 | 14 | ## Linked Issues 15 | Issues have been created for each user story and task, and linked to the Kanban board. Labels such as "feature" have been added to categorize the tasks and facilitate better tracking and reporting. 16 | 17 | ### Example of Task Assignments and Statuses 18 | 19 | | Task ID | Task Description | Assigned To | Status | Labels | 20 | |---------|------------------|-------------|----------------|----------| 21 | | T-001 | Develop QR code scanning API endpoint | @DevTeam | In Progress | feature | 22 | | T-002 | Implement product authenticity verification logic | @DevTeam | To Do | feature | 23 | | T-003 | Create UI for QR code scanning and product verification | @FrontendTeam | In Review | feature | 24 | | T-004 | Develop alert system for product expiry | @DevTeam | To Do | feature | 25 | | T-005 | Design UI for alert notifications | @FrontendTeam | To Do | feature | 26 | | T-006 | Implement counterfeit product reporting feature | @DevTeam | To Do | feature | 27 | | T-007 | Create UI for counterfeit product reporting | @FrontendTeam | Testing | feature | 28 | | T-008 | Develop compliance monitoring dashboard | @DevTeam | Blocked | feature | 29 | | T-009 | Design UI for compliance monitoring | @FrontendTeam | To Do | feature | 30 | | T-010 | Implement product registration feature via QR code scanning | @DevTeam | To Do | feature | 31 | | T-011 | Create UI for product registration | @FrontendTeam | To Do | feature | 32 | 33 | By following these steps, you can create a customized Kanban board on GitHub to effectively manage Agile workflows and track the progress of your project. 34 | 35 | ### Screenshot of Customized Kanban Board 36 | 37 | ![Automated Kanban Board](Kanban.PNG) 38 | ![Automated Kanban Board](isses.PNG) 39 | 40 | 41 | ## Conclusion 42 | This customized Kanban board facilitates the effective tracking and management of tasks, ensuring clear visibility and accountability for all team members. The addition of the "Testing" and "Blocked" columns helps in aligning with QA requirements and addressing blockers promptly, thereby improving the overall efficiency of the project management process. 43 | -------------------------------------------------------------------------------- /Kanban_Board_Explanation.md: -------------------------------------------------------------------------------- 1 | # Kanban Board Explanation 2 | 3 | ## What is a Kanban Board? 4 | A Kanban board is a visual tool used in Agile project management to track and manage tasks as they move through different stages of a workflow. It typically consists of columns that represent various phases of the work process, such as "To Do," "In Progress," and "Done." Tasks are represented as cards that move from one column to another as they progress. 5 | 6 | ## How My Board Visualizes Workflow 7 | My customized Kanban board visualizes the workflow by using the following columns: 8 | - **To Do**: This column contains tasks that are ready to be picked up and worked on. 9 | - **In Progress**: Tasks that are currently being worked on are placed in this column. 10 | - **In Review**: Tasks that have been completed and are awaiting review are moved to this column. 11 | - **Testing**: Tasks that are undergoing quality assurance testing are placed here. 12 | - **Blocked**: Tasks that are currently blocked due to dependencies or other issues are moved to this column. 13 | - **Done**: Completed tasks are placed in this column. 14 | 15 | By moving tasks from one column to another, team members can easily see the current status of each task and understand the overall progress of the project. 16 | 17 | ## Limiting Work-in-Progress (WIP) 18 | To avoid bottlenecks and ensure focused work, my Kanban board limits the number of tasks that can be in each column at any given time. For example: 19 | - **In Progress**: A maximum of 3 tasks can be in this column simultaneously. 20 | - **In Review**: A maximum of 2 tasks can be in this column at any time. 21 | - **Testing**: A maximum of 2 tasks can be in this column at any time. 22 | 23 | By limiting WIP, I ensure that team members are not overwhelmed with too many tasks and can focus on completing tasks efficiently. This also helps in identifying and addressing bottlenecks early. 24 | 25 | ## Supporting Agile Principles 26 | My Kanban board supports Agile principles in the following ways: 27 | - **Continuous Delivery**: By visualizing the workflow and limiting WIP, the board helps the team deliver tasks continuously and incrementally. This aligns with the Agile principle of delivering working software frequently. 28 | - **Adaptability**: The board allows for easy adaptation to changes in priorities or requirements. Tasks can be reprioritized and moved between columns as needed, ensuring that the team remains flexible and responsive to changes. 29 | - **Transparency**: The visual nature of the Kanban board provides transparency into the status of tasks and overall project progress. Team members and stakeholders can easily see what is being worked on, what is completed, and where there might be issues. 30 | - **Collaboration**: The board fosters collaboration among team members by providing a shared view of the project. Team members can communicate about task statuses, blockers, and priorities, ensuring that everyone is aligned and working towards common goals. 31 | 32 | By utilizing a Kanban board, I can effectively manage tasks, ensure continuous delivery, and maintain adaptability and transparency in the project management process. -------------------------------------------------------------------------------- /config/queue.php: -------------------------------------------------------------------------------- 1 | env('QUEUE_CONNECTION', 'sync'), 17 | 18 | /* 19 | |-------------------------------------------------------------------------- 20 | | Queue Connections 21 | |-------------------------------------------------------------------------- 22 | | 23 | | Here you may configure the connection information for each server that 24 | | is used by your application. A default configuration has been added 25 | | for each back-end shipped with Laravel. You are free to add more. 26 | | 27 | | Drivers: "sync", "database", "beanstalkd", "sqs", "redis", "null" 28 | | 29 | */ 30 | 31 | 'connections' => [ 32 | 33 | 'sync' => [ 34 | 'driver' => 'sync', 35 | ], 36 | 37 | 'database' => [ 38 | 'driver' => 'database', 39 | 'table' => 'jobs', 40 | 'queue' => 'default', 41 | 'retry_after' => 90, 42 | 'after_commit' => false, 43 | ], 44 | 45 | 'beanstalkd' => [ 46 | 'driver' => 'beanstalkd', 47 | 'host' => 'localhost', 48 | 'queue' => 'default', 49 | 'retry_after' => 90, 50 | 'block_for' => 0, 51 | 'after_commit' => false, 52 | ], 53 | 54 | 'sqs' => [ 55 | 'driver' => 'sqs', 56 | 'key' => env('AWS_ACCESS_KEY_ID'), 57 | 'secret' => env('AWS_SECRET_ACCESS_KEY'), 58 | 'prefix' => env('SQS_PREFIX', 'https://sqs.us-east-1.amazonaws.com/your-account-id'), 59 | 'queue' => env('SQS_QUEUE', 'default'), 60 | 'suffix' => env('SQS_SUFFIX'), 61 | 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'), 62 | 'after_commit' => false, 63 | ], 64 | 65 | 'redis' => [ 66 | 'driver' => 'redis', 67 | 'connection' => 'default', 68 | 'queue' => env('REDIS_QUEUE', 'default'), 69 | 'retry_after' => 90, 70 | 'block_for' => null, 71 | 'after_commit' => false, 72 | ], 73 | 74 | ], 75 | 76 | /* 77 | |-------------------------------------------------------------------------- 78 | | Failed Queue Jobs 79 | |-------------------------------------------------------------------------- 80 | | 81 | | These options configure the behavior of failed queue job logging so you 82 | | can control which database and table are used to store the jobs that 83 | | have failed. You may change them to any database / table you wish. 84 | | 85 | */ 86 | 87 | 'failed' => [ 88 | 'driver' => env('QUEUE_FAILED_DRIVER', 'database-uuids'), 89 | 'database' => env('DB_CONNECTION', 'mysql'), 90 | 'table' => 'failed_jobs', 91 | ], 92 | 93 | ]; 94 | -------------------------------------------------------------------------------- /routes/web.php: -------------------------------------------------------------------------------- 1 | name('stores.index'); 23 | // Route::get('/stores/{id}', [StoreController::class, 'show'])->name('stores.show'); 24 | // Route::get('/stores/create', [StoreController::class, 'create'])->name('stores.create'); 25 | 26 | Route::get('/stores', [StoreController::class, 'index'])->name('stores.index'); 27 | Route::get('/stores/create', [StoreController::class, 'create'])->name('stores.create'); 28 | Route::post('/stores', [StoreController::class, 'store'])->name('stores.store'); 29 | Route::get('/stores/{id}', [StoreController::class, 'show'])->name('stores.show'); 30 | Route::get('/stores/{id}/qrcode', [StoreController::class, 'generateQRCode'])->name('stores.qrcode'); 31 | 32 | use App\Http\Controllers\SupplierController; 33 | 34 | Route::resource('suppliers', SupplierController::class); 35 | 36 | use App\Http\Controllers\ProductController; 37 | 38 | Route::resource('products', ProductController::class); 39 | 40 | use App\Http\Controllers\ManufacturerController; 41 | Route::resource('manufacturers', ManufacturerController::class); 42 | 43 | use App\Http\Controllers\StoreProductController; 44 | 45 | Route::get('/stores/{store}/products/register', [StoreProductController::class, 'create'])->name('store_products.create'); 46 | Route::post('/stores/{store}/products/register', [StoreProductController::class, 'store'])->name('store_products.store'); 47 | 48 | use App\Http\Controllers\DashboardController; 49 | Route::get('/dashboard', [DashboardController::class, 'index'])->name('dashboard'); 50 | 51 | use App\Http\Controllers\BulkProductController; 52 | // Bulk product operations 53 | Route::get('/products/export', [ProductController::class, 'export'])->name('products.export'); 54 | 55 | // Reports 56 | // routes/web.php 57 | // Sales routes 58 | use App\Http\Controllers\SaleController; 59 | Route::resource('sales', SaleController::class); 60 | Route::get('/sales-dashboard', [SaleController::class, 'dashboard'])->name('sales.dashboard'); 61 | Route::get('/sales-reports', [SaleController::class, 'reports'])->name('sales.reports'); 62 | Route::post('/sales-generate-report', [SaleController::class, 'generateReport'])->name('sales.generate-report'); 63 | 64 | // Predictive Analytics 65 | use App\Http\Controllers\PredictiveAnalyticsController; 66 | Route::get('/analytics', [PredictiveAnalyticsController::class, 'index'])->name('analytics.index'); 67 | Route::post('/analytics/predict', [PredictiveAnalyticsController::class, 'predict'])->name('analytics.predict'); 68 | Route::get('/analytics/dashboard', [PredictiveAnalyticsController::class, 'dashboard'])->name('analytics.dashboard'); 69 | Route::get('/generate-predictions', [PredictiveAnalyticsController::class, 'generatePredictions'])->name('analytics.generate'); -------------------------------------------------------------------------------- /app/Console/Commands/TestPredictions.php: -------------------------------------------------------------------------------- 1 | info('Testing predictions generation...'); 19 | 20 | // Check basic data 21 | $products = Product::count(); 22 | $stores = Store::count(); 23 | 24 | $this->info("Products: {$products}, Stores: {$stores}"); 25 | 26 | if ($products == 0 || $stores == 0) { 27 | $this->warn('Need products and stores to generate predictions.'); 28 | $this->info('Creating sample data...'); 29 | $this->createSampleData(); 30 | } 31 | 32 | // Test prediction generation 33 | $controller = new \App\Http\Controllers\PredictiveAnalyticsController(); 34 | $controller->generatePredictions(); 35 | 36 | // Check results 37 | $predictions = SalesPrediction::count(); 38 | $forecasts = InventoryForecast::count(); 39 | 40 | $this->info("Created {$predictions} predictions and {$forecasts} forecasts"); 41 | 42 | if ($predictions > 0) { 43 | $this->info('First prediction:'); 44 | $first = SalesPrediction::first(); 45 | dump($first->toArray()); 46 | } 47 | 48 | return Command::SUCCESS; 49 | } 50 | 51 | private function createSampleData() 52 | { 53 | // Create a sample store if none exists 54 | if (Store::count() == 0) { 55 | Store::create([ 56 | 'name' => 'Test Store', 57 | 'location' => 'Test Location', 58 | 'owner_name' => 'Test Owner', 59 | 'contact_number' => '1234567890' 60 | ]); 61 | $this->info('Created test store'); 62 | } 63 | 64 | // Create a sample product if none exists 65 | if (Product::count() == 0) { 66 | // First create a supplier and manufacturer if needed 67 | $supplier = \App\Models\Supplier::firstOrCreate([ 68 | 'company_name' => 'Test Supplier' 69 | ], [ 70 | 'contact_person' => 'Test Contact', 71 | 'phone_number' => '1234567890', 72 | 'email' => 'test@supplier.com', 73 | 'address' => 'Test Address' 74 | ]); 75 | 76 | $manufacturer = \App\Models\Manufacturer::firstOrCreate([ 77 | 'name' => 'Test Manufacturer' 78 | ], [ 79 | 'contact_email' => 'test@manufacturer.com', 80 | 'address' => 'Test Address' 81 | ]); 82 | 83 | Product::create([ 84 | 'name' => 'Test Product', 85 | 'description' => 'Test Description', 86 | 'manufacturer_id' => $manufacturer->id, 87 | 'supplier_id' => $supplier->id, 88 | 'expiry_date' => now()->addYear(), 89 | 'price' => 10.99 90 | ]); 91 | $this->info('Created test product'); 92 | } 93 | } 94 | } -------------------------------------------------------------------------------- /ARCHITECTURE.md: -------------------------------------------------------------------------------- 1 | # ARCHITECTURE.md 2 | 3 | ## 1. Project Title 4 | **SpazaSafe: Counterfeit & Expired Product Detection System** 5 | 6 | ## 2. Domain 7 | **Informal Retail & Consumer Safety** 8 | SpazaSafe operates within the informal retail sector, aiming to enhance product authenticity and safety in Spaza shops across South Africa. The system integrates QR codes, suppliers, shop owners, and regulatory bodies to ensure safe and legitimate products are sold. 9 | 10 | ## 3. Problem Statement 11 | Spaza shops often sell expired or counterfeit products due to weak supply chain transparency. Consumers face health risks, and legitimate suppliers lose revenue. SpazaSafe addresses this issue by providing QR-based product authentication, supplier verification, and regulatory tracking. 12 | 13 | ## 4. C4 Architectural Diagrams 14 | 15 | ### 4.1 Context Diagram 16 | Illustrates how SpazaSafe interacts with different users and external systems. 17 | 18 | ```mermaid 19 | flowchart TB 20 | A[Spaza Shop Owner] -->|Scans QR Codes| B[SpazaSafe System] 21 | C[Consumer] -->|Scans QR Codes| B 22 | D[Supplier] -->|Registers Products| B 23 | E[Regulator] -->|Audits Compliance| B 24 | B -->|Provides Authentication| C 25 | ``` 26 | 27 | ### 4.2 Container Diagram 28 | Breaks the system into major components (backend, frontend, database, integrations). 29 | 30 | ```mermaid 31 | flowchart TB 32 | subgraph Frontend 33 | A[React/Vue Web App] 34 | end 35 | 36 | subgraph Backend 37 | B[Laravel API] 38 | C[Authentication Service] 39 | D[Product Verification Service] 40 | E[Delivery Tracking Service] 41 | F[QR Code Service] 42 | end 43 | 44 | subgraph Database 45 | G[MySQL Database] 46 | end 47 | 48 | subgraph External Services 49 | H[QR Code API] 50 | end 51 | 52 | A -->|API Requests| B 53 | B -->|Reads/Writes Data| G 54 | B -->|QR Code Validation| H 55 | B --> C 56 | B --> D 57 | B --> E 58 | B --> F 59 | ``` 60 | 61 | ### 4.3 Component Diagram 62 | Details key components within the system. 63 | 64 | ```mermaid 65 | flowchart TB 66 | subgraph Backend 67 | A[Laravel API] 68 | B[Auth Controller] 69 | C[Product Controller] 70 | D[QR Code Controller] 71 | E[Delivery Controller] 72 | end 73 | 74 | subgraph Services 75 | F[Authentication Service] 76 | G[Product Verification Service] 77 | H[QR Code Processing Service] 78 | I[Delivery Tracking Service] 79 | end 80 | 81 | A --> B 82 | A --> C 83 | A --> D 84 | A --> E 85 | 86 | B --> F 87 | C --> G 88 | D --> H 89 | E --> I 90 | ``` 91 | 92 | ### 4.4 Code Diagram (Level 4 - Code Structure) 93 | Defines high-level structure for Laravel application. 94 | 95 | ```mermaid 96 | flowchart TB 97 | subgraph Laravel Application 98 | A[Routes] 99 | B[Controllers] 100 | C[Models] 101 | D[Middleware] 102 | E[Services] 103 | F[Database Migrations] 104 | end 105 | 106 | subgraph Controllers 107 | G[AuthController] 108 | H[ProductController] 109 | I[QRController] 110 | J[DeliveryController] 111 | end 112 | 113 | subgraph Services 114 | K[Auth Service] 115 | L[Product Service] 116 | M[QR Code Service] 117 | N[Delivery Service] 118 | end 119 | 120 | A --> B 121 | B --> C 122 | B --> D 123 | B --> E 124 | E --> K 125 | E --> L 126 | E --> M 127 | E --> N 128 | C --> F 129 | ``` 130 | 131 | --- 132 | -------------------------------------------------------------------------------- /User_Stories.md: -------------------------------------------------------------------------------- 1 | # User Stories 2 | 3 | | Story ID | User Story | Acceptance Criteria | Priority (High/Medium/Low) | 4 | |----------|------------|---------------------|----------------------------| 5 | | US-001 | As a customer, I want to scan QR codes to verify product authenticity so that I can ensure the products I purchase are genuine. | The system displays product authenticity status within 1 second after scanning the QR code. | High | 6 | | US-002 | As a shop owner, I want to receive alerts about products nearing their expiry date so that I can remove them from inventory in time. | Alerts are sent to the shop owner 30 days before product expiry. | High | 7 | | US-003 | As an environmental health professional, I want to report counterfeit products so that regulators can take action against counterfeit goods. | The report is logged and a notification is sent to the regulator within 1 hour of submission. | High | 8 | | US-004 | As a customer, I want to provide feedback on product quality so that manufacturers can improve their products. | Feedback submission is acknowledged within 24 hours, and feedback is recorded in the system. | Medium | 9 | | US-005 | As a customer, I want to access educational resources about counterfeit products so that I can learn how to identify them. | Educational resources are accessible online and can be viewed without any errors. | Medium | 10 | | US-006 | As a regulator, I want to monitor compliance of stores and manufacturers so that I can ensure health regulations are followed. | Compliance reports are generated and displayed accurately. | High | 11 | | US-007 | As a shop owner, I want to manage my inventory levels so that I can keep track of product sales and monitor expired products. | Inventory is updated in real-time and accurately reflects the current stock. | High | 12 | | US-008 | As a developer, I want to maintain an audit trail of all transactions and activities so that the system's integrity is ensured. | Audit trail includes timestamps, user IDs, and action details for all activities. | Medium | 13 | | US-009 | As a customer, I want to securely log in using multi-factor authentication so that my account is protected from unauthorized access. | The system enforces MFA and requests a verification code for each login attempt. | High | 14 | | US-010 | As a regulator, I want to generate data analytics on product sales and counterfeit incidents so that I can make informed decisions. | Analytics reports are available for download in CSV format. | Medium | 15 | | US-011 | As a shop owner, I want to register new products by scanning their QR codes so that my inventory is updated quickly. | Product registration is completed within 2 seconds of scanning the QR code. | High | 16 | | US-012 | As a regulator, I want to send alerts and notifications about non-compliance and counterfeit reports so that shop owners and manufacturers are informed. | Alerts are sent via email and SMS within 5 minutes of detection. | High | 17 | 18 | ## Non-Functional User Stories 19 | 20 | | Story ID | User Story | Acceptance Criteria | Priority (High/Medium/Low) | 21 | |----------|------------|---------------------|----------------------------| 22 | | US-NF-001| As a system admin, I want user data encrypted with AES-256 so that security compliance is met. | All user data is encrypted using AES-256 encryption. | High | 23 | | US-NF-002| As a performance tester, I want the system to handle 1,000 concurrent users so that it performs well under high load. | The system response time is ≤ 2 seconds for 1,000 concurrent users. | High | -------------------------------------------------------------------------------- /config/cache.php: -------------------------------------------------------------------------------- 1 | env('CACHE_DRIVER', 'file'), 19 | 20 | /* 21 | |-------------------------------------------------------------------------- 22 | | Cache Stores 23 | |-------------------------------------------------------------------------- 24 | | 25 | | Here you may define all of the cache "stores" for your application as 26 | | well as their drivers. You may even define multiple stores for the 27 | | same cache driver to group types of items stored in your caches. 28 | | 29 | | Supported drivers: "apc", "array", "database", "file", 30 | | "memcached", "redis", "dynamodb", "octane", "null" 31 | | 32 | */ 33 | 34 | 'stores' => [ 35 | 36 | 'apc' => [ 37 | 'driver' => 'apc', 38 | ], 39 | 40 | 'array' => [ 41 | 'driver' => 'array', 42 | 'serialize' => false, 43 | ], 44 | 45 | 'database' => [ 46 | 'driver' => 'database', 47 | 'table' => 'cache', 48 | 'connection' => null, 49 | 'lock_connection' => null, 50 | ], 51 | 52 | 'file' => [ 53 | 'driver' => 'file', 54 | 'path' => storage_path('framework/cache/data'), 55 | ], 56 | 57 | 'memcached' => [ 58 | 'driver' => 'memcached', 59 | 'persistent_id' => env('MEMCACHED_PERSISTENT_ID'), 60 | 'sasl' => [ 61 | env('MEMCACHED_USERNAME'), 62 | env('MEMCACHED_PASSWORD'), 63 | ], 64 | 'options' => [ 65 | // Memcached::OPT_CONNECT_TIMEOUT => 2000, 66 | ], 67 | 'servers' => [ 68 | [ 69 | 'host' => env('MEMCACHED_HOST', '127.0.0.1'), 70 | 'port' => env('MEMCACHED_PORT', 11211), 71 | 'weight' => 100, 72 | ], 73 | ], 74 | ], 75 | 76 | 'redis' => [ 77 | 'driver' => 'redis', 78 | 'connection' => 'cache', 79 | 'lock_connection' => 'default', 80 | ], 81 | 82 | 'dynamodb' => [ 83 | 'driver' => 'dynamodb', 84 | 'key' => env('AWS_ACCESS_KEY_ID'), 85 | 'secret' => env('AWS_SECRET_ACCESS_KEY'), 86 | 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'), 87 | 'table' => env('DYNAMODB_CACHE_TABLE', 'cache'), 88 | 'endpoint' => env('DYNAMODB_ENDPOINT'), 89 | ], 90 | 91 | 'octane' => [ 92 | 'driver' => 'octane', 93 | ], 94 | 95 | ], 96 | 97 | /* 98 | |-------------------------------------------------------------------------- 99 | | Cache Key Prefix 100 | |-------------------------------------------------------------------------- 101 | | 102 | | When utilizing the APC, database, memcached, Redis, or DynamoDB cache 103 | | stores there might be other applications using the same cache. For 104 | | that reason, you may prefix every cache key to avoid collisions. 105 | | 106 | */ 107 | 108 | 'prefix' => env('CACHE_PREFIX', Str::slug(env('APP_NAME', 'laravel'), '_').'_cache_'), 109 | 110 | ]; 111 | -------------------------------------------------------------------------------- /app/Http/Controllers/ProductController.php: -------------------------------------------------------------------------------- 1 | get(); 21 | return view('products.index', compact('products')); 22 | } 23 | 24 | /** 25 | * Show the form for creating a new product assigned to a supplier. 26 | */ 27 | public function create() 28 | { 29 | $suppliers = Supplier::all(); 30 | $manufacturers = Manufacturer::all(); 31 | return view('products.create', compact('suppliers', 'manufacturers')); 32 | } 33 | 34 | /** 35 | * Store a newly created product and generate its QR code. 36 | */ 37 | public function store(Request $request) 38 | { 39 | // Validate the incoming request 40 | $request->validate([ 41 | 'supplier_id' => 'required|exists:suppliers,id', 42 | 'manufacturer_id' => 'required|exists:manufacturers,id', 43 | 'name' => 'required|string|max:255', 44 | 'description' => 'nullable|string', 45 | 'expiry_date' => 'nullable|date', 46 | ]); 47 | 48 | // Create the product 49 | $product = Product::create($request->all()); 50 | 51 | // Generate QR code content (URL pointing to the product details page) 52 | $qrCodeData = route('products.show', $product->id); 53 | 54 | // Generate and store the QR code 55 | $qrCodeImage = QrCode::size(200)->generate($qrCodeData); 56 | $product->qr_code = $qrCodeImage; // Store QR code in the product's `qr_code` column 57 | $product->save(); 58 | 59 | // Redirect to the supplier's page with success message 60 | return redirect()->route('suppliers.show', $product->supplier_id) 61 | ->with('success', 'Product created successfully!'); 62 | } 63 | 64 | /** 65 | * Display the specified product with its QR code. 66 | */ 67 | public function show(Product $product) 68 | { 69 | return view('products.show', compact('product')); 70 | } 71 | 72 | 73 | 74 | public function export() 75 | { 76 | $products = Product::with(['supplier', 'manufacturer'])->get(); 77 | 78 | $csvFileName = 'products_export_' . date('Y-m-d_H-i-s') . '.csv'; 79 | 80 | $headers = [ 81 | 'Content-Type' => 'text/csv; charset=utf-8', 82 | 'Content-Disposition' => 'attachment; filename="' . $csvFileName . '"', 83 | ]; 84 | 85 | $callback = function() use ($products) { 86 | $file = fopen('php://output', 'w'); 87 | fwrite($file, "\xEF\xBB\xBF"); // UTF-8 BOM 88 | 89 | fputcsv($file, ['ID', 'Name', 'Description', 'Manufacturer', 'Supplier', 'Price', 'Expiry Date']); 90 | 91 | foreach ($products as $product) { 92 | fputcsv($file, [ 93 | $product->id, 94 | $product->name, 95 | $product->description, 96 | $product->manufacturer->name ?? 'N/A', 97 | $product->supplier->company_name ?? 'N/A', 98 | $product->price, 99 | $product->expiry_date 100 | ]); 101 | } 102 | 103 | fclose($file); 104 | }; 105 | 106 | return Response::stream($callback, 200, $headers); 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /config/mail.php: -------------------------------------------------------------------------------- 1 | env('MAIL_MAILER', 'smtp'), 17 | 18 | /* 19 | |-------------------------------------------------------------------------- 20 | | Mailer Configurations 21 | |-------------------------------------------------------------------------- 22 | | 23 | | Here you may configure all of the mailers used by your application plus 24 | | their respective settings. Several examples have been configured for 25 | | you and you are free to add your own as your application requires. 26 | | 27 | | Laravel supports a variety of mail "transport" drivers to be used while 28 | | sending an e-mail. You will specify which one you are using for your 29 | | mailers below. You are free to add additional mailers as required. 30 | | 31 | | Supported: "smtp", "sendmail", "mailgun", "ses", 32 | | "postmark", "log", "array", "failover" 33 | | 34 | */ 35 | 36 | 'mailers' => [ 37 | 'smtp' => [ 38 | 'transport' => 'smtp', 39 | 'host' => env('MAIL_HOST', 'smtp.mailgun.org'), 40 | 'port' => env('MAIL_PORT', 587), 41 | 'encryption' => env('MAIL_ENCRYPTION', 'tls'), 42 | 'username' => env('MAIL_USERNAME'), 43 | 'password' => env('MAIL_PASSWORD'), 44 | 'timeout' => null, 45 | 'local_domain' => env('MAIL_EHLO_DOMAIN'), 46 | ], 47 | 48 | 'ses' => [ 49 | 'transport' => 'ses', 50 | ], 51 | 52 | 'mailgun' => [ 53 | 'transport' => 'mailgun', 54 | ], 55 | 56 | 'postmark' => [ 57 | 'transport' => 'postmark', 58 | ], 59 | 60 | 'sendmail' => [ 61 | 'transport' => 'sendmail', 62 | 'path' => env('MAIL_SENDMAIL_PATH', '/usr/sbin/sendmail -bs -i'), 63 | ], 64 | 65 | 'log' => [ 66 | 'transport' => 'log', 67 | 'channel' => env('MAIL_LOG_CHANNEL'), 68 | ], 69 | 70 | 'array' => [ 71 | 'transport' => 'array', 72 | ], 73 | 74 | 'failover' => [ 75 | 'transport' => 'failover', 76 | 'mailers' => [ 77 | 'smtp', 78 | 'log', 79 | ], 80 | ], 81 | ], 82 | 83 | /* 84 | |-------------------------------------------------------------------------- 85 | | Global "From" Address 86 | |-------------------------------------------------------------------------- 87 | | 88 | | You may wish for all e-mails sent by your application to be sent from 89 | | the same address. Here, you may specify a name and address that is 90 | | used globally for all e-mails that are sent by your application. 91 | | 92 | */ 93 | 94 | 'from' => [ 95 | 'address' => env('MAIL_FROM_ADDRESS', 'hello@example.com'), 96 | 'name' => env('MAIL_FROM_NAME', 'Example'), 97 | ], 98 | 99 | /* 100 | |-------------------------------------------------------------------------- 101 | | Markdown Mail Settings 102 | |-------------------------------------------------------------------------- 103 | | 104 | | If you are using Markdown based email rendering, you may configure your 105 | | theme and component paths here, allowing you to customize the design 106 | | of the emails. Or, you may simply stick with the Laravel defaults! 107 | | 108 | */ 109 | 110 | 'markdown' => [ 111 | 'theme' => 'default', 112 | 113 | 'paths' => [ 114 | resource_path('views/vendor/mail'), 115 | ], 116 | ], 117 | 118 | ]; 119 | -------------------------------------------------------------------------------- /config/auth.php: -------------------------------------------------------------------------------- 1 | [ 17 | 'guard' => 'web', 18 | 'passwords' => 'users', 19 | ], 20 | 21 | /* 22 | |-------------------------------------------------------------------------- 23 | | Authentication Guards 24 | |-------------------------------------------------------------------------- 25 | | 26 | | Next, you may define every authentication guard for your application. 27 | | Of course, a great default configuration has been defined for you 28 | | here which uses session storage and the Eloquent user provider. 29 | | 30 | | All authentication drivers have a user provider. This defines how the 31 | | users are actually retrieved out of your database or other storage 32 | | mechanisms used by this application to persist your user's data. 33 | | 34 | | Supported: "session" 35 | | 36 | */ 37 | 38 | 'guards' => [ 39 | 'web' => [ 40 | 'driver' => 'session', 41 | 'provider' => 'users', 42 | ], 43 | ], 44 | 45 | /* 46 | |-------------------------------------------------------------------------- 47 | | User Providers 48 | |-------------------------------------------------------------------------- 49 | | 50 | | All authentication drivers have a user provider. This defines how the 51 | | users are actually retrieved out of your database or other storage 52 | | mechanisms used by this application to persist your user's data. 53 | | 54 | | If you have multiple user tables or models you may configure multiple 55 | | sources which represent each model / table. These sources may then 56 | | be assigned to any extra authentication guards you have defined. 57 | | 58 | | Supported: "database", "eloquent" 59 | | 60 | */ 61 | 62 | 'providers' => [ 63 | 'users' => [ 64 | 'driver' => 'eloquent', 65 | 'model' => App\Models\User::class, 66 | ], 67 | 68 | // 'users' => [ 69 | // 'driver' => 'database', 70 | // 'table' => 'users', 71 | // ], 72 | ], 73 | 74 | /* 75 | |-------------------------------------------------------------------------- 76 | | Resetting Passwords 77 | |-------------------------------------------------------------------------- 78 | | 79 | | You may specify multiple password reset configurations if you have more 80 | | than one user table or model in the application and you want to have 81 | | separate password reset settings based on the specific user types. 82 | | 83 | | The expire time is the number of minutes that each reset token will be 84 | | considered valid. This security feature keeps tokens short-lived so 85 | | they have less time to be guessed. You may change this as needed. 86 | | 87 | */ 88 | 89 | 'passwords' => [ 90 | 'users' => [ 91 | 'provider' => 'users', 92 | 'table' => 'password_resets', 93 | 'expire' => 60, 94 | 'throttle' => 60, 95 | ], 96 | ], 97 | 98 | /* 99 | |-------------------------------------------------------------------------- 100 | | Password Confirmation Timeout 101 | |-------------------------------------------------------------------------- 102 | | 103 | | Here you may define the amount of seconds before a password confirmation 104 | | times out and the user is prompted to re-enter their password via the 105 | | confirmation screen. By default, the timeout lasts for three hours. 106 | | 107 | */ 108 | 109 | 'password_timeout' => 10800, 110 | 111 | ]; 112 | -------------------------------------------------------------------------------- /resources/views/sales/show.blade.php: -------------------------------------------------------------------------------- 1 | @extends('layouts.app') 2 | 3 | @section('title', 'Sale Details - ' . $sale->id) 4 | 5 | @section('content') 6 |
7 |
8 |

Sale Details

9 | 10 | Back to Sales 11 | 12 |
13 | 14 |
15 |
16 |
17 |
18 |
Sale Information
19 |
20 |
21 |
22 |
23 |

Sale ID: #{{ $sale->id }}

24 |

Date & Time: {{ $sale->sale_date->format('M j, Y H:i') }}

25 |

Store: {{ $sale->store->name }}

26 |

Location: {{ $sale->store->location }}

27 |
28 |
29 |

Product: {{ $sale->product->name }}

30 |

Quantity: {{ $sale->quantity }}

31 |

Unit Price: R{{ number_format($sale->unit_price, 2) }}

32 |

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

33 |
34 |
35 | 36 | @if($sale->notes) 37 |
38 | Notes: 39 |

{{ $sale->notes }}

40 |
41 | @endif 42 |
43 |
44 |
45 | 46 |
47 |
48 |
49 |
Quick Actions
50 |
51 |
52 |
53 | 54 | Edit Sale 55 | 56 |
57 | @csrf 58 | @method('DELETE') 59 | 63 |
64 | 66 | New Sale (Same Store/Product) 67 | 68 |
69 |
70 |
71 | 72 |
73 |
74 |
Product Information
75 |
76 |
77 |

Supplier: {{ $sale->product->supplier->company_name ?? 'N/A' }}

78 |

Manufacturer: {{ $sale->product->manufacturer->name ?? 'N/A' }}

79 |

Expiry Date: {{ $sale->product->expiry_date ?? 'N/A' }}

80 |
81 |
82 |
83 |
84 |
85 | @endsection -------------------------------------------------------------------------------- /Domain_Model_Counterfeit_System.md: -------------------------------------------------------------------------------- 1 | # Domain Model for Counterfeit System 2 | 3 | ## Domain Entities 4 | 5 | ### 1. Product 6 | | Attribute | Description | 7 | |--------------------|---------------------------------| 8 | | `id` | Unique identifier for the product | 9 | | `name` | Name of the product | 10 | | `category` | Category of the product | 11 | | `authenticityScore` | Numerical score indicating authenticity | 12 | | `manufacturer` | Manufacturer of the product | 13 | | `status` | Status of the product (e.g., "Authentic", "Counterfeit")| 14 | 15 | **Methods**: 16 | - `verifyAuthenticity()`: Validates the product's authenticity. 17 | - `updateStatus()`: Updates the product's status based on verification. 18 | 19 | **Relationships**: 20 | - A **Product** can trigger an **Alert** if flagged as counterfeit. 21 | 22 | --- 23 | 24 | ### 2. User 25 | | Attribute | Description | 26 | |--------------------|---------------------------------| 27 | | `id` | Unique identifier for the user | 28 | | `name` | Name of the user | 29 | | `email` | Email address of the user | 30 | | `role` | Role of the user (e.g., "Admin", "Inspector")| 31 | | `reportLimit` | Maximum number of reports the user can submit per day | 32 | 33 | **Methods**: 34 | - `submitReport()`: Allows the user to submit a counterfeit report. 35 | - `viewAlerts()`: Enables the user to view active alerts. 36 | 37 | **Relationships**: 38 | - A **User** submits a **Report** for a **Product**. 39 | 40 | --- 41 | 42 | ### 3. Report 43 | | Attribute | Description | 44 | |--------------------|---------------------------------| 45 | | `id` | Unique identifier for the report| 46 | | `productId` | ID of the reported product | 47 | | `userId` | ID of the user who submitted the report | 48 | | `description` | Description of the issue | 49 | | `timestamp` | Submission date and time | 50 | | `status` | Status of the report (e.g., "Pending", "Reviewed")| 51 | 52 | **Methods**: 53 | - `create()`: Creates a new report. 54 | - `updateStatus()`: Updates the status of the report. 55 | 56 | **Relationships**: 57 | - A **Report** is associated with a **Product** and a **User**. 58 | 59 | --- 60 | 61 | ### 4. Alert 62 | | Attribute | Description | 63 | |--------------------|---------------------------------| 64 | | `id` | Unique identifier for the alert | 65 | | `productId` | ID of the product triggering the alert | 66 | | `severity` | Severity level of the alert | 67 | | `timestamp` | Date and time of the alert | 68 | 69 | **Methods**: 70 | - `generate()`: Generates an alert for a counterfeit product. 71 | - `resolve()`: Marks the alert as resolved. 72 | 73 | **Relationships**: 74 | - An **Alert** is generated for a **Product** and can be viewed by a **User**. 75 | 76 | --- 77 | 78 | ### 5. Compliance Dashboard 79 | | Attribute | Description | 80 | |--------------------|---------------------------------| 81 | | `id` | Unique identifier for the dashboard | 82 | | `userId` | ID of the associated user | 83 | | `totalReports` | Total number of reports submitted | 84 | | `resolvedReports` | Number of reports resolved | 85 | | `activeAlerts` | Number of active alerts | 86 | 87 | **Methods**: 88 | - `generateSummary()`: Provides a summary of reports and alerts. 89 | - `viewDetails()`: Allows users to view detailed statistics. 90 | 91 | **Relationships**: 92 | - A **Compliance Dashboard** is associated with a **User** and aggregates data from **Reports** and **Alerts**. 93 | 94 | --- 95 | 96 | ## Business Rules 97 | 1. A **User** can submit a maximum of 3 reports per day. 98 | 2. A **Product** is flagged as counterfeit if its `authenticityScore < 50`. 99 | 3. A **Report** can only be created for an existing **Product**. 100 | 4. An **Alert** is generated automatically when a **Product** is flagged as counterfeit. 101 | 5. Only users with the "Admin" role can resolve **Alerts**. -------------------------------------------------------------------------------- /config/logging.php: -------------------------------------------------------------------------------- 1 | env('LOG_CHANNEL', 'stack'), 21 | 22 | /* 23 | |-------------------------------------------------------------------------- 24 | | Deprecations Log Channel 25 | |-------------------------------------------------------------------------- 26 | | 27 | | This option controls the log channel that should be used to log warnings 28 | | regarding deprecated PHP and library features. This allows you to get 29 | | your application ready for upcoming major versions of dependencies. 30 | | 31 | */ 32 | 33 | 'deprecations' => [ 34 | 'channel' => env('LOG_DEPRECATIONS_CHANNEL', 'null'), 35 | 'trace' => false, 36 | ], 37 | 38 | /* 39 | |-------------------------------------------------------------------------- 40 | | Log Channels 41 | |-------------------------------------------------------------------------- 42 | | 43 | | Here you may configure the log channels for your application. Out of 44 | | the box, Laravel uses the Monolog PHP logging library. This gives 45 | | you a variety of powerful log handlers / formatters to utilize. 46 | | 47 | | Available Drivers: "single", "daily", "slack", "syslog", 48 | | "errorlog", "monolog", 49 | | "custom", "stack" 50 | | 51 | */ 52 | 53 | 'channels' => [ 54 | 'stack' => [ 55 | 'driver' => 'stack', 56 | 'channels' => ['single'], 57 | 'ignore_exceptions' => false, 58 | ], 59 | 60 | 'single' => [ 61 | 'driver' => 'single', 62 | 'path' => storage_path('logs/laravel.log'), 63 | 'level' => env('LOG_LEVEL', 'debug'), 64 | ], 65 | 66 | 'daily' => [ 67 | 'driver' => 'daily', 68 | 'path' => storage_path('logs/laravel.log'), 69 | 'level' => env('LOG_LEVEL', 'debug'), 70 | 'days' => 14, 71 | ], 72 | 73 | 'slack' => [ 74 | 'driver' => 'slack', 75 | 'url' => env('LOG_SLACK_WEBHOOK_URL'), 76 | 'username' => 'Laravel Log', 77 | 'emoji' => ':boom:', 78 | 'level' => env('LOG_LEVEL', 'critical'), 79 | ], 80 | 81 | 'papertrail' => [ 82 | 'driver' => 'monolog', 83 | 'level' => env('LOG_LEVEL', 'debug'), 84 | 'handler' => env('LOG_PAPERTRAIL_HANDLER', SyslogUdpHandler::class), 85 | 'handler_with' => [ 86 | 'host' => env('PAPERTRAIL_URL'), 87 | 'port' => env('PAPERTRAIL_PORT'), 88 | 'connectionString' => 'tls://'.env('PAPERTRAIL_URL').':'.env('PAPERTRAIL_PORT'), 89 | ], 90 | ], 91 | 92 | 'stderr' => [ 93 | 'driver' => 'monolog', 94 | 'level' => env('LOG_LEVEL', 'debug'), 95 | 'handler' => StreamHandler::class, 96 | 'formatter' => env('LOG_STDERR_FORMATTER'), 97 | 'with' => [ 98 | 'stream' => 'php://stderr', 99 | ], 100 | ], 101 | 102 | 'syslog' => [ 103 | 'driver' => 'syslog', 104 | 'level' => env('LOG_LEVEL', 'debug'), 105 | ], 106 | 107 | 'errorlog' => [ 108 | 'driver' => 'errorlog', 109 | 'level' => env('LOG_LEVEL', 'debug'), 110 | ], 111 | 112 | 'null' => [ 113 | 'driver' => 'monolog', 114 | 'handler' => NullHandler::class, 115 | ], 116 | 117 | 'emergency' => [ 118 | 'path' => storage_path('logs/laravel.log'), 119 | ], 120 | ], 121 | 122 | ]; 123 | -------------------------------------------------------------------------------- /app/Http/Controllers/ReportController.php: -------------------------------------------------------------------------------- 1 | id())->get(); 18 | return view('reports.index', compact('reports')); 19 | } 20 | 21 | public function create() 22 | { 23 | return view('reports.create'); 24 | } 25 | 26 | public function generateSalesReport(Request $request) 27 | { 28 | $request->validate([ 29 | 'start_date' => 'required|date', 30 | 'end_date' => 'required|date|after_or_equal:start_date', 31 | 'store_id' => 'nullable|exists:stores,id' 32 | ]); 33 | 34 | $data = [ 35 | 'start_date' => $request->start_date, 36 | 'end_date' => $request->end_date, 37 | 'store_id' => $request->store_id 38 | ]; 39 | 40 | // Generate report data 41 | $reportData = $this->buildSalesReportData($data); 42 | 43 | if ($request->has('save_report')) { 44 | Report::create([ 45 | 'name' => 'Sales Report ' . now()->format('Y-m-d'), 46 | 'type' => 'sales', 47 | 'parameters' => $data, 48 | 'user_id' => auth()->id() 49 | ]); 50 | } 51 | 52 | $format = $request->get('format', 'html'); 53 | 54 | if ($format === 'pdf') { 55 | return $this->generatePdfReport($reportData, 'sales'); 56 | } 57 | 58 | if ($format === 'csv') { 59 | return $this->generateCsvReport($reportData, 'sales'); 60 | } 61 | 62 | return view('reports.sales', compact('reportData')); 63 | } 64 | 65 | private function buildSalesReportData($data) 66 | { 67 | // Mock sales data - you would replace this with actual sales logic 68 | $sales = [ 69 | 'total_revenue' => 12500.75, 70 | 'total_products_sold' => 245, 71 | 'top_products' => [ 72 | ['name' => 'Bread', 'quantity' => 45, 'revenue' => 675.50], 73 | ['name' => 'Milk', 'quantity' => 38, 'revenue' => 456.00], 74 | ['name' => 'Sugar', 'quantity' => 32, 'revenue' => 320.00] 75 | ], 76 | 'daily_sales' => [ 77 | '2024-01-15' => 1200.50, 78 | '2024-01-16' => 1350.75, 79 | '2024-01-17' => 1100.25 80 | ], 81 | 'period' => [ 82 | 'start' => $data['start_date'], 83 | 'end' => $data['end_date'] 84 | ] 85 | ]; 86 | 87 | return $sales; 88 | } 89 | 90 | private function generatePdfReport($data, $type) 91 | { 92 | $pdf = PDF::loadView('reports.pdf.' . $type, compact('data')); 93 | return $pdf->download('report-' . $type . '-' . now()->format('Y-m-d') . '.pdf'); 94 | } 95 | 96 | private function generateCsvReport($data, $type) 97 | { 98 | $filename = 'report-' . $type . '-' . now()->format('Y-m-d') . '.csv'; 99 | 100 | $headers = [ 101 | 'Content-Type' => 'text/csv', 102 | 'Content-Disposition' => 'attachment; filename="' . $filename . '"', 103 | ]; 104 | 105 | $callback = function() use ($data) { 106 | $file = fopen('php://output', 'w'); 107 | fputcsv($file, ['Product', 'Quantity Sold', 'Revenue']); 108 | 109 | foreach ($data['top_products'] as $product) { 110 | fputcsv($file, [ 111 | $product['name'], 112 | $product['quantity'], 113 | $product['revenue'] 114 | ]); 115 | } 116 | 117 | fclose($file); 118 | }; 119 | 120 | return response()->stream($callback, 200, $headers); 121 | } 122 | 123 | public function inventoryReport() 124 | { 125 | $lowStock = Product::where('quantity', '<', 5)->count(); 126 | $outOfStock = Product::where('quantity', '<=', 0)->count(); 127 | $totalValue = Product::sum(\DB::raw('price * quantity')); 128 | 129 | return view('reports.inventory', compact('lowStock', 'outOfStock', 'totalValue')); 130 | } 131 | } -------------------------------------------------------------------------------- /STAKEHOLDER_ANALYSIS.md: -------------------------------------------------------------------------------- 1 | # Stakeholder Analysis 2 | 3 | In the context of addressing the sale of counterfeit and expired products in Spaza shops, it is crucial to identify and understand the concerns of various stakeholders. This will ensure that the system developed addresses their needs and contributes to the overall goal of improving public health and safety. Below is a detailed analysis of the key stakeholders involved in this project. 4 | 5 | | Stakeholder | Role | Key Concerns | Pain Points | Success Metrics | 6 | |--------------------|------------------------------------------------------------|-----------------------------------------------------------|----------------------------------------------------------|---------------------------------------------------------| 7 | | Customers | Purchase products and verify authenticity using the system | Access to safe and authentic products | Risk of buying counterfeit or expired products | 95% customer satisfaction with product quality | 8 | | Manufacturers | Create QR codes for product packages and supply authentic products | Protection of brand integrity | Counterfeit products damaging their reputation | 90% reduction in counterfeit product reports | 9 | | Shop Owners | Scan QR codes to verify authenticity, register products, and monitor expired products | Ensure product authenticity and quality | Inability to verify product authenticity | 90% reduction in counterfeit product sales | 10 | | Regulators | Monitor compliance of stores and manufacturers, and verify store registration using the system | Enforcement of regulations to prevent counterfeit sales | Insufficient resources for monitoring and enforcement | 50% increase in compliance rates among Spaza shops | 11 | | Developers | Develop and maintain the product verification system | Ensure system reliability and security | Complexity in system integration and maintenance | 99.9% system uptime and successful integration | 12 | | Community Leaders | Advocate for community health and safety | Improve overall health and safety in the community | Lack of awareness and education about counterfeit products | 70% increase in community awareness programs | 13 | | Environmental Health Professionals | Monitor and ensure public health safety | Identify and remove counterfeit and expired products | Difficulty in tracking counterfeit product sources | 80% reduction in counterfeit product cases | 14 | 15 | ### Stakeholder Roles and Context 16 | 17 | 1. **Customers**: Customers are the end-users who purchase products from Spaza shops. Their primary concern is to have access to safe and authentic products. They play a crucial role in the system by using it to scan and verify the authenticity of products before making a purchase. This helps reduce the risk of buying counterfeit or expired products, contributing to their overall satisfaction and safety. 18 | 19 | 2. **Manufacturers**: Manufacturers create QR codes for product packages and supply authentic products to the market. They are concerned with protecting their brand integrity and ensuring that their products are not counterfeited. Counterfeit products can damage their reputation and result in financial losses. 20 | 21 | 3. **Shop Owners**: Shop owners manage the inventory and sales of products in Spaza shops. They scan QR codes to verify the authenticity of products, register products in the store, and monitor expired products. The inability to verify product authenticity is a significant pain point for them. 22 | 23 | 4. **Regulators**: Regulators are responsible for ensuring that Spaza shops comply with health and safety standards. They monitor compliance of stores and manufacturers, and use the system to verify if a store is registered. Enforcement of regulations to prevent the sale of counterfeit products is a key concern, but they often face challenges due to insufficient resources for monitoring and enforcement. 24 | 25 | 5. **Developers**: Developers are tasked with developing and maintaining the product verification system. They need to ensure that the system is reliable, secure, and easy to integrate with existing infrastructure. Complexity in system integration and maintenance is a common challenge they face. 26 | 27 | 6. **Community Leaders**: Community leaders advocate for the health and safety of their communities. They aim to improve overall community health by raising awareness about the dangers of counterfeit products. However, they often struggle with a lack of resources to educate the community effectively. 28 | 29 | 7. **Environmental Health Professionals**: These professionals monitor and ensure public health safety by identifying and removing counterfeit and expired products from the market. They face challenges in tracking the sources of counterfeit products and ensuring they are promptly removed from circulation. 30 | -------------------------------------------------------------------------------- /app/Http/Controllers/SaleController.php: -------------------------------------------------------------------------------- 1 | orderBy('sale_date', 'desc') 22 | ->paginate(20); 23 | 24 | $totalSales = Sale::sum('total_amount'); 25 | $totalUnits = Sale::sum('quantity'); 26 | 27 | return view('sales.index', compact('sales', 'totalSales', 'totalUnits')); 28 | } 29 | 30 | public function create() 31 | { 32 | $stores = Store::all(); 33 | $products = Product::all(); 34 | 35 | return view('sales.create', compact('stores', 'products')); 36 | } 37 | 38 | public function store(Request $request) 39 | { 40 | $validated = $request->validate([ 41 | 'store_id' => 'required|exists:stores,id', 42 | 'product_id' => 'required|exists:products,id', 43 | 'quantity' => 'required|integer|min:1', 44 | 'unit_price' => 'required|numeric|min:0', 45 | 'sale_date' => 'required|date', 46 | 'notes' => 'nullable|string' 47 | ]); 48 | 49 | // Calculate total amount 50 | $validated['total_amount'] = $validated['quantity'] * $validated['unit_price']; 51 | 52 | Sale::create($validated); 53 | 54 | // Update store product quantity (reduce stock) 55 | DB::table('store_product') 56 | ->where('store_id', $validated['store_id']) 57 | ->where('product_id', $validated['product_id']) 58 | ->decrement('quantity', $validated['quantity']); 59 | 60 | return redirect()->route('sales.index') 61 | ->with('success', 'Sale recorded successfully!'); 62 | } 63 | 64 | public function show(Sale $sale) 65 | { 66 | $sale->load(['store', 'product']); 67 | return view('sales.show', compact('sale')); 68 | } 69 | 70 | public function dashboard() 71 | { 72 | $todaySales = Sale::whereDate('sale_date', today())->sum('total_amount'); 73 | $weekSales = Sale::whereBetween('sale_date', [now()->startOfWeek(), now()->endOfWeek()])->sum('total_amount'); 74 | $monthSales = Sale::whereMonth('sale_date', now()->month)->sum('total_amount'); 75 | 76 | $topProducts = Sale::select('product_id', DB::raw('SUM(quantity) as total_sold')) 77 | ->with('product') 78 | ->groupBy('product_id') 79 | ->orderBy('total_sold', 'desc') 80 | ->take(5) 81 | ->get(); 82 | 83 | $recentSales = Sale::with(['store', 'product']) 84 | ->orderBy('sale_date', 'desc') 85 | ->take(10) 86 | ->get(); 87 | 88 | return view('sales.dashboard', compact( 89 | 'todaySales', 90 | 'weekSales', 91 | 'monthSales', 92 | 'topProducts', 93 | 'recentSales' 94 | )); 95 | } 96 | 97 | // public function reports() 98 | // { 99 | // return view('sales.reports'); 100 | // } 101 | 102 | public function generateReport(Request $request) 103 | { 104 | $validated = $request->validate([ 105 | 'start_date' => 'required|date', 106 | 'end_date' => 'required|date|after_or_equal:start_date', 107 | 'store_id' => 'nullable|exists:stores,id' 108 | ]); 109 | 110 | $query = Sale::with(['store', 'product']) 111 | ->whereBetween('sale_date', [$validated['start_date'], $validated['end_date']]); 112 | 113 | if ($validated['store_id']) { 114 | $query->where('store_id', $validated['store_id']); 115 | } 116 | 117 | $sales = $query->get(); 118 | $totalRevenue = $sales->sum('total_amount'); 119 | $totalUnits = $sales->sum('quantity'); 120 | 121 | return view('sales.report-results', compact('sales', 'totalRevenue', 'totalUnits', 'validated')); 122 | } 123 | 124 | public function edit(Sale $sale) 125 | { 126 | $stores = Store::all(); 127 | $products = Product::all(); 128 | return view('sales.edit', compact('sale', 'stores', 'products')); 129 | } 130 | 131 | public function update(Request $request, Sale $sale) 132 | { 133 | $validated = $request->validate([ 134 | 'store_id' => 'required|exists:stores,id', 135 | 'product_id' => 'required|exists:products,id', 136 | 'quantity' => 'required|integer|min:1', 137 | 'unit_price' => 'required|numeric|min:0', 138 | 'sale_date' => 'required|date', 139 | 'notes' => 'nullable|string' 140 | ]); 141 | 142 | $validated['total_amount'] = $validated['quantity'] * $validated['unit_price']; 143 | $sale->update($validated); 144 | 145 | return redirect()->route('sales.show', $sale) 146 | ->with('success', 'Sale updated successfully!'); 147 | } 148 | 149 | public function destroy(Sale $sale) 150 | { 151 | $sale->delete(); 152 | return redirect()->route('sales.index') 153 | ->with('success', 'Sale deleted successfully!'); 154 | } 155 | } -------------------------------------------------------------------------------- /resources/views/sales/dashboard.blade.php: -------------------------------------------------------------------------------- 1 | {{-- resources/views/sales/dashboard.blade.php --}} 2 | @extends('layouts.app') 3 | 4 | @section('title', 'Sales Dashboard') 5 | 6 | @section('content') 7 |
8 |
9 |

Sales Dashboard

10 | 18 |
19 | 20 | 21 |
22 |
23 |
24 |
25 |

R{{ number_format($todaySales, 2) }}

26 |

Today's Sales

27 |
28 |
29 |
30 |
31 |
32 |
33 |

R{{ number_format($weekSales, 2) }}

34 |

This Week

35 |
36 |
37 |
38 |
39 |
40 |
41 |

R{{ number_format($monthSales, 2) }}

42 |

This Month

43 |
44 |
45 |
46 |
47 |
48 |
49 |

{{ $topProducts->sum('total_sold') }}

50 |

Total Units Sold

51 |
52 |
53 |
54 |
55 | 56 | 57 |
58 |
59 |
60 |
61 |
Top Selling Products
62 |
63 |
64 |
65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | @foreach($topProducts as $item) 75 | 76 | 77 | 78 | 79 | 80 | @endforeach 81 | 82 |
ProductUnits SoldRevenue
{{ $item->product->name }}{{ $item->total_sold }}R{{ number_format($item->total_sold * $item->product->price, 2) }}
83 |
84 |
85 |
86 |
87 | 88 |
89 |
90 |
91 |
Recent Sales
92 |
93 |
94 |
95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | @foreach($recentSales as $sale) 105 | 106 | 107 | 108 | 109 | 110 | @endforeach 111 | 112 |
TimeProductAmount
{{ $sale->sale_date->format('H:i') }}{{ $sale->product->name }}R{{ number_format($sale->total_amount, 2) }}
113 |
114 |
115 |
116 |
117 |
118 | 119 | 120 |
121 |
122 |
Sales Trend
123 |
124 |
125 |
126 | 127 |

Sales chart will be displayed here

128 | Integrate with Chart.js for visual analytics 129 |
130 |
131 |
132 |
133 | @endsection -------------------------------------------------------------------------------- /resources/views/products/bulk.blade.php: -------------------------------------------------------------------------------- 1 | @extends('layouts.app') 2 | 3 | @section('title', 'Bulk Product Operations') 4 | 5 | @section('content') 6 |
7 |
8 |

Bulk Product Operations

9 | 10 | Back to Products 11 | 12 |
13 | 14 |
15 | 16 |
17 |
18 |
19 |
Import Products
20 |
21 |
22 |
23 | @csrf 24 | 25 |
26 | 27 | 28 |
29 | Download the CSV template for proper formatting 30 |
31 |
32 | 33 |
34 |
Import Instructions
35 |
    36 |
  • Use the provided template
  • 37 |
  • Keep the header row
  • 38 |
  • Date format: YYYY-MM-DD
  • 39 |
  • Max file size: 5MB
  • 40 |
41 |
42 | 43 | 46 |
47 |
48 |
49 |
50 | 51 | 52 |
53 |
54 |
55 |
Export Products
56 |
57 |
58 |

Export all products to a CSV file. This file can be opened in Excel or any spreadsheet application.

59 | 60 |
61 |
Export includes:
62 |
    63 |
  • Product details
  • 64 |
  • Manufacturer information
  • 65 |
  • Supplier information
  • 66 |
  • Pricing and expiry dates
  • 67 |
68 |
69 | 70 | 71 | Export to CSV 72 | 73 |
74 |
75 |
76 |
77 | 78 | 79 | @if(session('success') || session('error')) 80 |
81 |
82 | @if(session('success')) 83 |
84 | 85 | {{ session('success') }} 86 | 87 |
88 | @endif 89 | 90 | @if(session('error')) 91 |
92 | 93 | {{ session('error') }} 94 | 95 |
96 | @endif 97 |
98 |
99 | @endif 100 |
101 | @endsection 102 | 103 | @section('scripts') 104 | 127 | @endsection -------------------------------------------------------------------------------- /resources/views/products/show.blade.php: -------------------------------------------------------------------------------- 1 | @extends('layouts.app') 2 | 3 | @section('title', 'Product Details') 4 | 5 | @section('content') 6 |
7 |
8 |
9 |

{{ $product->name }}

10 | 11 | Back to Products 12 | 13 |
14 | 15 |
16 |
17 |

Description: {{ $product->description ?? 'No description' }}

18 |

Manufacturer: {{ $product->manufacturer ? $product->manufacturer->name : 'N/A' }}

19 |

Expiry Date: {{ $product->expiry_date ? \Carbon\Carbon::parse($product->expiry_date)->format('M j, Y') : 'N/A' }}

20 |

Supplier: {{ $product->supplier ? $product->supplier->company_name : 'N/A' }}

21 |

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

22 |
23 |
24 |

QR Code

25 |
26 | @if($product->qr_code) 27 | {!! $product->qr_code !!} 28 | @else 29 |
30 |

No QR Code generated yet.

31 |
32 | @csrf 33 | 36 |
37 |
38 | @endif 39 |
40 |
41 |
42 |
43 | 44 | 45 |
46 |
47 |

Stores with this Product

48 | 49 | Add to Store 50 | 51 |
52 | 53 | @if($product->stores->count() > 0) 54 |
55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | @foreach($product->stores as $store) 68 | 69 | 70 | 71 | 76 | 77 | 78 | 83 | 84 | @endforeach 85 | 86 |
Store NameLocationQuantityDelivered AtExpire DateActions
{{ $store->name }}{{ $store->location }} 72 | 73 | {{ $store->pivot->quantity }} 74 | 75 | {{ $store->pivot->delivered_at ? \Carbon\Carbon::parse($store->pivot->delivered_at)->format('M j, Y') : 'N/A' }}{{ $store->pivot->expire_date ? \Carbon\Carbon::parse($store->pivot->expire_date)->format('M j, Y') : 'N/A' }} 79 | 80 | View Store 81 | 82 |
87 |
88 | @else 89 |
90 | This product is not currently in any store. 91 |
92 | @endif 93 |
94 | 95 | 96 |
97 |

Quick Actions

98 |
99 | 100 | Edit Product 101 | 102 |
103 | @csrf 104 | @method('DELETE') 105 | 108 |
109 | 110 | All Products 111 | 112 |
113 |
114 |
115 | @endsection -------------------------------------------------------------------------------- /config/database.php: -------------------------------------------------------------------------------- 1 | env('DB_CONNECTION', 'mysql'), 19 | 20 | /* 21 | |-------------------------------------------------------------------------- 22 | | Database Connections 23 | |-------------------------------------------------------------------------- 24 | | 25 | | Here are each of the database connections setup for your application. 26 | | Of course, examples of configuring each database platform that is 27 | | supported by Laravel is shown below to make development simple. 28 | | 29 | | 30 | | All database work in Laravel is done through the PHP PDO facilities 31 | | so make sure you have the driver for your particular database of 32 | | choice installed on your machine before you begin development. 33 | | 34 | */ 35 | 36 | 'connections' => [ 37 | 38 | 'sqlite' => [ 39 | 'driver' => 'sqlite', 40 | 'url' => env('DATABASE_URL'), 41 | 'database' => env('DB_DATABASE', database_path('database.sqlite')), 42 | 'prefix' => '', 43 | 'foreign_key_constraints' => env('DB_FOREIGN_KEYS', true), 44 | ], 45 | 46 | 'mysql' => [ 47 | 'driver' => 'mysql', 48 | 'url' => env('DATABASE_URL'), 49 | 'host' => env('DB_HOST', '127.0.0.1'), 50 | 'port' => env('DB_PORT', '3306'), 51 | 'database' => env('DB_DATABASE', 'forge'), 52 | 'username' => env('DB_USERNAME', 'forge'), 53 | 'password' => env('DB_PASSWORD', ''), 54 | 'unix_socket' => env('DB_SOCKET', ''), 55 | 'charset' => 'utf8mb4', 56 | 'collation' => 'utf8mb4_unicode_ci', 57 | 'prefix' => '', 58 | 'prefix_indexes' => true, 59 | 'strict' => true, 60 | 'engine' => null, 61 | 'options' => extension_loaded('pdo_mysql') ? array_filter([ 62 | PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'), 63 | ]) : [], 64 | ], 65 | 66 | 'pgsql' => [ 67 | 'driver' => 'pgsql', 68 | 'url' => env('DATABASE_URL'), 69 | 'host' => env('DB_HOST', '127.0.0.1'), 70 | 'port' => env('DB_PORT', '5432'), 71 | 'database' => env('DB_DATABASE', 'forge'), 72 | 'username' => env('DB_USERNAME', 'forge'), 73 | 'password' => env('DB_PASSWORD', ''), 74 | 'charset' => 'utf8', 75 | 'prefix' => '', 76 | 'prefix_indexes' => true, 77 | 'search_path' => 'public', 78 | 'sslmode' => 'prefer', 79 | ], 80 | 81 | 'sqlsrv' => [ 82 | 'driver' => 'sqlsrv', 83 | 'url' => env('DATABASE_URL'), 84 | 'host' => env('DB_HOST', 'localhost'), 85 | 'port' => env('DB_PORT', '1433'), 86 | 'database' => env('DB_DATABASE', 'forge'), 87 | 'username' => env('DB_USERNAME', 'forge'), 88 | 'password' => env('DB_PASSWORD', ''), 89 | 'charset' => 'utf8', 90 | 'prefix' => '', 91 | 'prefix_indexes' => true, 92 | // 'encrypt' => env('DB_ENCRYPT', 'yes'), 93 | // 'trust_server_certificate' => env('DB_TRUST_SERVER_CERTIFICATE', 'false'), 94 | ], 95 | 96 | ], 97 | 98 | /* 99 | |-------------------------------------------------------------------------- 100 | | Migration Repository Table 101 | |-------------------------------------------------------------------------- 102 | | 103 | | This table keeps track of all the migrations that have already run for 104 | | your application. Using this information, we can determine which of 105 | | the migrations on disk haven't actually been run in the database. 106 | | 107 | */ 108 | 109 | 'migrations' => 'migrations', 110 | 111 | /* 112 | |-------------------------------------------------------------------------- 113 | | Redis Databases 114 | |-------------------------------------------------------------------------- 115 | | 116 | | Redis is an open source, fast, and advanced key-value store that also 117 | | provides a richer body of commands than a typical key-value system 118 | | such as APC or Memcached. Laravel makes it easy to dig right in. 119 | | 120 | */ 121 | 122 | 'redis' => [ 123 | 124 | 'client' => env('REDIS_CLIENT', 'phpredis'), 125 | 126 | 'options' => [ 127 | 'cluster' => env('REDIS_CLUSTER', 'redis'), 128 | 'prefix' => env('REDIS_PREFIX', Str::slug(env('APP_NAME', 'laravel'), '_').'_database_'), 129 | ], 130 | 131 | 'default' => [ 132 | 'url' => env('REDIS_URL'), 133 | 'host' => env('REDIS_HOST', '127.0.0.1'), 134 | 'username' => env('REDIS_USERNAME'), 135 | 'password' => env('REDIS_PASSWORD'), 136 | 'port' => env('REDIS_PORT', '6379'), 137 | 'database' => env('REDIS_DB', '0'), 138 | ], 139 | 140 | 'cache' => [ 141 | 'url' => env('REDIS_URL'), 142 | 'host' => env('REDIS_HOST', '127.0.0.1'), 143 | 'username' => env('REDIS_USERNAME'), 144 | 'password' => env('REDIS_PASSWORD'), 145 | 'port' => env('REDIS_PORT', '6379'), 146 | 'database' => env('REDIS_CACHE_DB', '1'), 147 | ], 148 | 149 | ], 150 | 151 | ]; 152 | --------------------------------------------------------------------------------