From 640f2bb271a872ef425c15cd8c2288d819dd2e81 Mon Sep 17 00:00:00 2001 From: Benoit Borrel Date: Sun, 28 Sep 2025 16:38:53 -0400 Subject: [PATCH 01/16] Composer: require laravel/boost --dev --- composer.json | 1 + composer.lock | 195 +++++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 193 insertions(+), 3 deletions(-) diff --git a/composer.json b/composer.json index 5605c28..7b43f4e 100644 --- a/composer.json +++ b/composer.json @@ -11,6 +11,7 @@ }, "require-dev": { "fakerphp/faker": "^1.23", + "laravel/boost": "^1.0", "laravel/pint": "^1.13", "laravel/sail": "^1.26", "mockery/mockery": "^1.6", diff --git a/composer.lock b/composer.lock index a3884ec..103b9b7 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "8c804e4a8c3c1ea0e6dd61980c279cdd", + "content-hash": "5c59868cbe9b3c1660622f874fc55426", "packages": [ { "name": "brick/math", @@ -5799,6 +5799,135 @@ }, "time": "2020-07-09T08:09:16+00:00" }, + { + "name": "laravel/boost", + "version": "v1.0.18", + "source": { + "type": "git", + "url": "https://github.com/laravel/boost.git", + "reference": "df2a62b5864759ea8cce8a4b7575b657e9c7d4ab" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/boost/zipball/df2a62b5864759ea8cce8a4b7575b657e9c7d4ab", + "reference": "df2a62b5864759ea8cce8a4b7575b657e9c7d4ab", + "shasum": "" + }, + "require": { + "guzzlehttp/guzzle": "^7.9", + "illuminate/console": "^10.0|^11.0|^12.0", + "illuminate/contracts": "^10.0|^11.0|^12.0", + "illuminate/routing": "^10.0|^11.0|^12.0", + "illuminate/support": "^10.0|^11.0|^12.0", + "laravel/mcp": "^0.1.0", + "laravel/prompts": "^0.1.9|^0.3", + "laravel/roster": "^0.2", + "php": "^8.1|^8.2" + }, + "require-dev": { + "laravel/pint": "^1.14|^1.23", + "mockery/mockery": "^1.6", + "orchestra/testbench": "^8.22.0|^9.0|^10.0", + "pestphp/pest": "^2.0|^3.0", + "phpstan/phpstan": "^2.0" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Laravel\\Boost\\BoostServiceProvider" + ] + }, + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Laravel\\Boost\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Laravel Boost accelerates AI-assisted development to generate high-quality, Laravel-specific code.", + "homepage": "https://github.com/laravel/boost", + "keywords": [ + "ai", + "dev", + "laravel" + ], + "support": { + "issues": "https://github.com/laravel/boost/issues", + "source": "https://github.com/laravel/boost" + }, + "time": "2025-08-16T09:10:03+00:00" + }, + { + "name": "laravel/mcp", + "version": "v0.1.1", + "source": { + "type": "git", + "url": "https://github.com/laravel/mcp.git", + "reference": "6d6284a491f07c74d34f48dfd999ed52c567c713" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/mcp/zipball/6d6284a491f07c74d34f48dfd999ed52c567c713", + "reference": "6d6284a491f07c74d34f48dfd999ed52c567c713", + "shasum": "" + }, + "require": { + "illuminate/console": "^10.0|^11.0|^12.0", + "illuminate/contracts": "^10.0|^11.0|^12.0", + "illuminate/http": "^10.0|^11.0|^12.0", + "illuminate/routing": "^10.0|^11.0|^12.0", + "illuminate/support": "^10.0|^11.0|^12.0", + "illuminate/validation": "^10.0|^11.0|^12.0", + "php": "^8.1|^8.2" + }, + "require-dev": { + "laravel/pint": "^1.14", + "orchestra/testbench": "^8.22.0|^9.0|^10.0", + "phpstan/phpstan": "^2.0" + }, + "type": "library", + "extra": { + "laravel": { + "aliases": { + "Mcp": "Laravel\\Mcp\\Server\\Facades\\Mcp" + }, + "providers": [ + "Laravel\\Mcp\\Server\\McpServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "Laravel\\Mcp\\": "src/", + "Workbench\\App\\": "workbench/app/", + "Laravel\\Mcp\\Tests\\": "tests/", + "Laravel\\Mcp\\Server\\": "src/Server/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "The easiest way to add MCP servers to your Laravel app.", + "homepage": "https://github.com/laravel/mcp", + "keywords": [ + "dev", + "laravel", + "mcp" + ], + "support": { + "issues": "https://github.com/laravel/mcp/issues", + "source": "https://github.com/laravel/mcp" + }, + "time": "2025-08-16T09:50:43+00:00" + }, { "name": "laravel/pint", "version": "v1.18.1", @@ -5865,6 +5994,66 @@ }, "time": "2024-09-24T17:22:50+00:00" }, + { + "name": "laravel/roster", + "version": "v0.2.2", + "source": { + "type": "git", + "url": "https://github.com/laravel/roster.git", + "reference": "67a39bce557a6cb7e7205a2a9d6c464f0e72956f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/roster/zipball/67a39bce557a6cb7e7205a2a9d6c464f0e72956f", + "reference": "67a39bce557a6cb7e7205a2a9d6c464f0e72956f", + "shasum": "" + }, + "require": { + "illuminate/console": "^10.0|^11.0|^12.0", + "illuminate/contracts": "^10.0|^11.0|^12.0", + "illuminate/routing": "^10.0|^11.0|^12.0", + "illuminate/support": "^10.0|^11.0|^12.0", + "php": "^8.1|^8.2" + }, + "require-dev": { + "laravel/pint": "^1.14", + "mockery/mockery": "^1.6", + "orchestra/testbench": "^8.22.0|^9.0|^10.0", + "pestphp/pest": "^2.0|^3.0", + "phpstan/phpstan": "^2.0" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Laravel\\Roster\\RosterServiceProvider" + ] + }, + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Laravel\\Roster\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Detect packages & approaches in use within a Laravel project", + "homepage": "https://github.com/laravel/roster", + "keywords": [ + "dev", + "laravel" + ], + "support": { + "issues": "https://github.com/laravel/roster/issues", + "source": "https://github.com/laravel/roster" + }, + "time": "2025-07-24T12:31:13+00:00" + }, { "name": "laravel/sail", "version": "v1.35.0", @@ -7756,12 +7945,12 @@ ], "aliases": [], "minimum-stability": "stable", - "stability-flags": [], + "stability-flags": {}, "prefer-stable": true, "prefer-lowest": false, "platform": { "php": "^8.2" }, - "platform-dev": [], + "platform-dev": {}, "plugin-api-version": "2.6.0" } From 605dfbf06ff50763d0745253483d2b28c9c79aaf Mon Sep 17 00:00:00 2001 From: Benoit Borrel Date: Sun, 28 Sep 2025 16:39:47 -0400 Subject: [PATCH 02/16] artisan: boost:install for VSCode + CoPilot --- .github/copilot-instructions.md | 221 ++++++++++++++++++++++++++++++++ 1 file changed, 221 insertions(+) create mode 100644 .github/copilot-instructions.md diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 0000000..ad1a90b --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,221 @@ + +=== foundation rules === + +# Laravel Boost Guidelines + +The Laravel Boost guidelines are specifically curated by Laravel maintainers for this application. These guidelines should be followed closely to enhance the user's satisfaction building Laravel applications. + +## Foundational Context +This application is a Laravel application and its main Laravel ecosystems package & versions are below. You are an expert with them all. Ensure you abide by these specific packages & versions. + +- php - 8.2.29 +- laravel/framework (LARAVEL) - v11 +- laravel/prompts (PROMPTS) - v0 +- laravel/pint (PINT) - v1 +- tailwindcss (TAILWINDCSS) - v3 + + +## Conventions +- You must follow all existing code conventions used in this application. When creating or editing a file, check sibling files for the correct structure, approach, naming. +- Use descriptive names for variables and methods. For example, `isRegisteredForDiscounts`, not `discount()`. +- Check for existing components to reuse before writing a new one. + +## Verification Scripts +- Do not create verification scripts or tinker when tests cover that functionality and prove it works. Unit and feature tests are more important. + +## Application Structure & Architecture +- Stick to existing directory structure - don't create new base folders without approval. +- Do not change the application's dependencies without approval. + +## Frontend Bundling +- If the user doesn't see a frontend change reflected in the UI, it could mean they need to run `npm run build`, `npm run dev`, or `composer run dev`. Ask them. + +## Replies +- Be concise in your explanations - focus on what's important rather than explaining obvious details. + +## Documentation Files +- You must only create documentation files if explicitly requested by the user. + + +=== boost rules === + +## Laravel Boost +- Laravel Boost is an MCP server that comes with powerful tools designed specifically for this application. Use them. + +## Artisan +- Use the `list-artisan-commands` tool when you need to call an Artisan command to double check the available parameters. + +## URLs +- Whenever you share a project URL with the user you should use the `get-absolute-url` tool to ensure you're using the correct scheme, domain / IP, and port. + +## Tinker / Debugging +- You should use the `tinker` tool when you need to execute PHP to debug code or query Eloquent models directly. +- Use the `database-query` tool when you only need to read from the database. + +## Reading Browser Logs With the `browser-logs` Tool +- You can read browser logs, errors, and exceptions using the `browser-logs` tool from Boost. +- Only recent browser logs will be useful - ignore old logs. + +## Searching Documentation (Critically Important) +- Boost comes with a powerful `search-docs` tool you should use before any other approaches. This tool automatically passes a list of installed packages and their versions to the remote Boost API, so it returns only version-specific documentation specific for the user's circumstance. You should pass an array of packages to filter on if you know you need docs for particular packages. +- The 'search-docs' tool is perfect for all Laravel related packages, including Laravel, Inertia, Livewire, Filament, Tailwind, Pest, Nova, Nightwatch, etc. +- You must use this tool to search for Laravel-ecosystem documentation before falling back to other approaches. +- Search the documentation before making code changes to ensure we are taking the correct approach. +- Use multiple, broad, simple, topic based queries to start. For example: `['rate limiting', 'routing rate limiting', 'routing']`. +- Do not add package names to queries - package information is already shared. For example, use `test resource table`, not `filament 4 test resource table`. + +### Available Search Syntax +- You can and should pass multiple queries at once. The most relevant results will be returned first. + +1. Simple Word Searches with auto-stemming - query=authentication - finds 'authenticate' and 'auth' +2. Multiple Words (AND Logic) - query=rate limit - finds knowledge containing both "rate" AND "limit" +3. Quoted Phrases (Exact Position) - query="infinite scroll" - Words must be adjacent and in that order +4. Mixed Queries - query=middleware "rate limit" - "middleware" AND exact phrase "rate limit" +5. Multiple Queries - queries=["authentication", "middleware"] - ANY of these terms + + +=== php rules === + +## PHP + +- Always use curly braces for control structures, even if it has one line. + +### Constructors +- Use PHP 8 constructor property promotion in `__construct()`. + - public function __construct(public GitHub $github) { } +- Do not allow empty `__construct()` methods with zero parameters. + +### Type Declarations +- Always use explicit return type declarations for methods and functions. +- Use appropriate PHP type hints for method parameters. + + +protected function isAccessible(User $user, ?string $path = null): bool +{ + ... +} + + +## Comments +- Prefer PHPDoc blocks over comments. Never use comments within the code itself unless there is something _very_ complex going on. + +## PHPDoc Blocks +- Add useful array shape type definitions for arrays when appropriate. + +## Enums +- Typically, keys in an Enum should be TitleCase. For example: `FavoritePerson`, `BestLake`, `Monthly`. + + +=== laravel/core rules === + +## Do Things the Laravel Way + +- Use `php artisan make:` commands to create new files (i.e. migrations, controllers, models, etc.). You can list available Artisan commands using the `list-artisan-commands` tool. +- If you're creating a generic PHP class, use `artisan make:class`. +- Pass `--no-interaction` to all Artisan commands to ensure they work without user input. You should also pass the correct `--options` to ensure correct behavior. + +### Database +- Always use proper Eloquent relationship methods with return type hints. Prefer relationship methods over raw queries or manual joins. +- Use Eloquent models and relationships before suggesting raw database queries +- Avoid `DB::`; prefer `Model::query()`. Generate code that leverages Laravel's ORM capabilities rather than bypassing them. +- Generate code that prevents N+1 query problems by using eager loading. +- Use Laravel's query builder for very complex database operations. + +### Model Creation +- When creating new models, create useful factories and seeders for them too. Ask the user if they need any other things, using `list-artisan-commands` to check the available options to `php artisan make:model`. + +### APIs & Eloquent Resources +- For APIs, default to using Eloquent API Resources and API versioning unless existing API routes do not, then you should follow existing application convention. + +### Controllers & Validation +- Always create Form Request classes for validation rather than inline validation in controllers. Include both validation rules and custom error messages. +- Check sibling Form Requests to see if the application uses array or string based validation rules. + +### Queues +- Use queued jobs for time-consuming operations with the `ShouldQueue` interface. + +### Authentication & Authorization +- Use Laravel's built-in authentication and authorization features (gates, policies, Sanctum, etc.). + +### URL Generation +- When generating links to other pages, prefer named routes and the `route()` function. + +### Configuration +- Use environment variables only in configuration files - never use the `env()` function directly outside of config files. Always use `config('app.name')`, not `env('APP_NAME')`. + +### Testing +- When creating models for tests, use the factories for the models. Check if the factory has custom states that can be used before manually setting up the model. +- Faker: Use methods such as `$this->faker->word()` or `fake()->randomDigit()`. Follow existing conventions whether to use `$this->faker` or `fake()`. +- When creating tests, make use of `php artisan make:test [options] ` to create a feature test, and pass `--unit` to create a unit test. Most tests should be feature tests. + +### Vite Error +- If you receive an "Illuminate\Foundation\ViteException: Unable to locate file in Vite manifest" error, you can run `npm run build` or ask the user to run `npm run dev` or `composer run dev`. + + +=== laravel/v11 rules === + +## Laravel 11 + +- Use the `search-docs` tool to get version specific documentation. +- Laravel 11 brought a new streamlined file structure which this project now uses. + +### Laravel 11 Structure +- No middleware files in `app/Http/Middleware/`. +- `bootstrap/app.php` is the file to register middleware, exceptions, and routing files. +- `bootstrap/providers.php` contains application specific service providers. +- **No app\Console\Kernel.php** - use `bootstrap/app.php` or `routes/console.php` for console configuration. +- **Commands auto-register** - files in `app/Console/Commands/` are automatically available and do not require manual registration. + +### Database +- When modifying a column, the migration must include all of the attributes that were previously defined on the column. Otherwise, they will be dropped and lost. +- Laravel 11 allows limiting eagerly loaded records natively, without external packages: `$query->latest()->limit(10);`. + +### Models +- Casts can and likely should be set in a `casts()` method on a model rather than the `$casts` property. Follow existing conventions from other models. + +### New Artisan Commands +- List Artisan commands using Boost's MCP tool, if available. New commands available in Laravel 11: + - `php artisan make:enum` + - `php artisan make:class` + - `php artisan make:interface` + + +=== pint/core rules === + +## Laravel Pint Code Formatter + +- You must run `vendor/bin/pint --dirty` before finalizing changes to ensure your code matches the project's expected style. +- Do not run `vendor/bin/pint --test`, simply run `vendor/bin/pint` to fix any formatting issues. + + +=== tailwindcss/core rules === + +## Tailwind Core + +- Use Tailwind CSS classes to style HTML, check and use existing tailwind conventions within the project before writing your own. +- Offer to extract repeated patterns into components that match the project's conventions (i.e. Blade, JSX, Vue, etc..) +- Think through class placement, order, priority, and defaults - remove redundant classes, add classes to parent or child carefully to limit repetition, group elements logically +- You can use the `search-docs` tool to get exact examples from the official documentation when needed. + +### Spacing +- When listing items, use gap utilities for spacing, don't use margins. + + +
+
Superior
+
Michigan
+
Erie
+
+
+ + +### Dark Mode +- If existing pages and components support dark mode, new pages and components must support dark mode in a similar way, typically using `dark:`. + + +=== tailwindcss/v3 rules === + +## Tailwind 3 + +- Always use Tailwind CSS v3 - verify you're using only classes supported by this version. +
\ No newline at end of file From a300e3681067e8f833f4e4b4c4b66859918b06c6 Mon Sep 17 00:00:00 2001 From: Benoit Borrel Date: Sun, 28 Sep 2025 17:23:04 -0400 Subject: [PATCH 03/16] artisan make:model Donation -mfsc --- app/Http/Controllers/DonationController.php | 10 +++++++ app/Models/Donation.php | 12 +++++++++ database/factories/DonationFactory.php | 23 ++++++++++++++++ ...25_09_28_212214_create_donations_table.php | 27 +++++++++++++++++++ database/seeders/DonationSeeder.php | 17 ++++++++++++ 5 files changed, 89 insertions(+) create mode 100644 app/Http/Controllers/DonationController.php create mode 100644 app/Models/Donation.php create mode 100644 database/factories/DonationFactory.php create mode 100644 database/migrations/2025_09_28_212214_create_donations_table.php create mode 100644 database/seeders/DonationSeeder.php diff --git a/app/Http/Controllers/DonationController.php b/app/Http/Controllers/DonationController.php new file mode 100644 index 0000000..de4a837 --- /dev/null +++ b/app/Http/Controllers/DonationController.php @@ -0,0 +1,10 @@ + */ + use HasFactory; +} diff --git a/database/factories/DonationFactory.php b/database/factories/DonationFactory.php new file mode 100644 index 0000000..b56b0bd --- /dev/null +++ b/database/factories/DonationFactory.php @@ -0,0 +1,23 @@ + + */ +class DonationFactory extends Factory +{ + /** + * Define the model's default state. + * + * @return array + */ + public function definition(): array + { + return [ + // + ]; + } +} diff --git a/database/migrations/2025_09_28_212214_create_donations_table.php b/database/migrations/2025_09_28_212214_create_donations_table.php new file mode 100644 index 0000000..898dcdc --- /dev/null +++ b/database/migrations/2025_09_28_212214_create_donations_table.php @@ -0,0 +1,27 @@ +id(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('donations'); + } +}; diff --git a/database/seeders/DonationSeeder.php b/database/seeders/DonationSeeder.php new file mode 100644 index 0000000..946ea7b --- /dev/null +++ b/database/seeders/DonationSeeder.php @@ -0,0 +1,17 @@ + Date: Sun, 28 Sep 2025 20:41:11 -0400 Subject: [PATCH 04/16] DB schema: add donation (one-time) migration + model --- app/DonationStatus.php | 12 ++++++++++++ app/Models/Donation.php | 13 +++++++++++++ .../2025_09_28_212214_create_donations_table.php | 7 +++++++ 3 files changed, 32 insertions(+) create mode 100644 app/DonationStatus.php diff --git a/app/DonationStatus.php b/app/DonationStatus.php new file mode 100644 index 0000000..c8716bc --- /dev/null +++ b/app/DonationStatus.php @@ -0,0 +1,12 @@ + */ use HasFactory; + + /** + * Get the attributes that should be cast. + * + * @return array + */ + protected function casts(): array + { + return [ + 'status' => DonationStatus::class, + ]; + } } diff --git a/database/migrations/2025_09_28_212214_create_donations_table.php b/database/migrations/2025_09_28_212214_create_donations_table.php index 898dcdc..70c6a4c 100644 --- a/database/migrations/2025_09_28_212214_create_donations_table.php +++ b/database/migrations/2025_09_28_212214_create_donations_table.php @@ -1,5 +1,7 @@ id(); + $table->foreignIdFor(User::class); + $table->decimal('amount', total: 8, places: 2); + $table->string('currency', length: 3); + $table->timestampTz('donated_at'); + $table->enum('status', array_column(DonationStatus::cases(), 'value'))->default(DonationStatus::PENDING->value); $table->timestamps(); }); } From d58ef3b84113243633939d111a67c6859578a50a Mon Sep 17 00:00:00 2001 From: Benoit Borrel Date: Sun, 28 Sep 2025 21:17:56 -0400 Subject: [PATCH 05/16] donation: add feature test for create + update --- app/Models/Donation.php | 9 ++++++ database/factories/DonationFactory.php | 6 +++- tests/Feature/DonationTest.php | 39 ++++++++++++++++++++++++++ 3 files changed, 53 insertions(+), 1 deletion(-) create mode 100644 tests/Feature/DonationTest.php diff --git a/app/Models/Donation.php b/app/Models/Donation.php index aae95ed..234c771 100644 --- a/app/Models/Donation.php +++ b/app/Models/Donation.php @@ -11,6 +11,15 @@ class Donation extends Model /** @use HasFactory<\Database\Factories\DonationFactory> */ use HasFactory; + /** + * The attributes that are mass assignable. + * + * @var array + */ + protected $fillable = [ + 'status', + ]; + /** * Get the attributes that should be cast. * diff --git a/database/factories/DonationFactory.php b/database/factories/DonationFactory.php index b56b0bd..fd95371 100644 --- a/database/factories/DonationFactory.php +++ b/database/factories/DonationFactory.php @@ -2,6 +2,7 @@ namespace Database\Factories; +use App\DonationStatus; use Illuminate\Database\Eloquent\Factories\Factory; /** @@ -17,7 +18,10 @@ class DonationFactory extends Factory public function definition(): array { return [ - // + 'user_id' => 1, + 'amount' => 100, + 'currency' => 'CAD', + 'donated_at' => now(), ]; } } diff --git a/tests/Feature/DonationTest.php b/tests/Feature/DonationTest.php new file mode 100644 index 0000000..f32a2b6 --- /dev/null +++ b/tests/Feature/DonationTest.php @@ -0,0 +1,39 @@ +create(); + $donation = Donation::factory()->create(); + + $this->assertDatabaseCount('users', 1); + + $this->assertDatabaseCount('donations', 1); + $this->assertDatabaseHas('donations', [ + 'id' => $donation->id, + 'user_id' => $donation->user_id, + 'amount' => $donation->amount, + 'currency' => $donation->currency, + 'status' => DonationStatus::PENDING, + ]); + + $donation->update([ + 'status' => DonationStatus::COMPLETED, + ]); + $this->assertDatabaseHas('donations', [ + 'id' => $donation->id, + 'status' => DonationStatus::COMPLETED, + ]); + } +} From 4cf050acc545a625f0086f4ab1a81118ea7fcccb Mon Sep 17 00:00:00 2001 From: Benoit Borrel Date: Sun, 28 Sep 2025 21:20:03 -0400 Subject: [PATCH 06/16] php artisan install:api --- bootstrap/app.php | 1 + composer.json | 1 + composer.lock | 66 ++++++++++++++- config/sanctum.php | 84 +++++++++++++++++++ ...14_create_personal_access_tokens_table.php | 33 ++++++++ routes/api.php | 8 ++ 6 files changed, 192 insertions(+), 1 deletion(-) create mode 100644 config/sanctum.php create mode 100644 database/migrations/2025_09_29_011914_create_personal_access_tokens_table.php create mode 100644 routes/api.php diff --git a/bootstrap/app.php b/bootstrap/app.php index 7b162da..d654276 100644 --- a/bootstrap/app.php +++ b/bootstrap/app.php @@ -7,6 +7,7 @@ return Application::configure(basePath: dirname(__DIR__)) ->withRouting( web: __DIR__.'/../routes/web.php', + api: __DIR__.'/../routes/api.php', commands: __DIR__.'/../routes/console.php', health: '/up', ) diff --git a/composer.json b/composer.json index 7b43f4e..5db62ee 100644 --- a/composer.json +++ b/composer.json @@ -7,6 +7,7 @@ "require": { "php": "^8.2", "laravel/framework": "^11.9", + "laravel/sanctum": "^4.0", "laravel/tinker": "^2.9" }, "require-dev": { diff --git a/composer.lock b/composer.lock index 103b9b7..6d7cbc9 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "5c59868cbe9b3c1660622f874fc55426", + "content-hash": "f7fdffb42fff7fe1b38917b6244b5df3", "packages": [ { "name": "brick/math", @@ -1322,6 +1322,70 @@ }, "time": "2024-09-30T14:27:51+00:00" }, + { + "name": "laravel/sanctum", + "version": "v4.2.0", + "source": { + "type": "git", + "url": "https://github.com/laravel/sanctum.git", + "reference": "fd6df4f79f48a72992e8d29a9c0ee25422a0d677" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/sanctum/zipball/fd6df4f79f48a72992e8d29a9c0ee25422a0d677", + "reference": "fd6df4f79f48a72992e8d29a9c0ee25422a0d677", + "shasum": "" + }, + "require": { + "ext-json": "*", + "illuminate/console": "^11.0|^12.0", + "illuminate/contracts": "^11.0|^12.0", + "illuminate/database": "^11.0|^12.0", + "illuminate/support": "^11.0|^12.0", + "php": "^8.2", + "symfony/console": "^7.0" + }, + "require-dev": { + "mockery/mockery": "^1.6", + "orchestra/testbench": "^9.0|^10.0", + "phpstan/phpstan": "^1.10", + "phpunit/phpunit": "^11.3" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Laravel\\Sanctum\\SanctumServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "Laravel\\Sanctum\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "Laravel Sanctum provides a featherweight authentication system for SPAs and simple APIs.", + "keywords": [ + "auth", + "laravel", + "sanctum" + ], + "support": { + "issues": "https://github.com/laravel/sanctum/issues", + "source": "https://github.com/laravel/sanctum" + }, + "time": "2025-07-09T19:45:24+00:00" + }, { "name": "laravel/serializable-closure", "version": "v1.3.5", diff --git a/config/sanctum.php b/config/sanctum.php new file mode 100644 index 0000000..44527d6 --- /dev/null +++ b/config/sanctum.php @@ -0,0 +1,84 @@ + explode(',', env('SANCTUM_STATEFUL_DOMAINS', sprintf( + '%s%s', + 'localhost,localhost:3000,127.0.0.1,127.0.0.1:8000,::1', + Sanctum::currentApplicationUrlWithPort(), + // Sanctum::currentRequestHost(), + ))), + + /* + |-------------------------------------------------------------------------- + | Sanctum Guards + |-------------------------------------------------------------------------- + | + | This array contains the authentication guards that will be checked when + | Sanctum is trying to authenticate a request. If none of these guards + | are able to authenticate the request, Sanctum will use the bearer + | token that's present on an incoming request for authentication. + | + */ + + 'guard' => ['web'], + + /* + |-------------------------------------------------------------------------- + | Expiration Minutes + |-------------------------------------------------------------------------- + | + | This value controls the number of minutes until an issued token will be + | considered expired. This will override any values set in the token's + | "expires_at" attribute, but first-party sessions are not affected. + | + */ + + 'expiration' => null, + + /* + |-------------------------------------------------------------------------- + | Token Prefix + |-------------------------------------------------------------------------- + | + | Sanctum can prefix new tokens in order to take advantage of numerous + | security scanning initiatives maintained by open source platforms + | that notify developers if they commit tokens into repositories. + | + | See: https://docs.github.com/en/code-security/secret-scanning/about-secret-scanning + | + */ + + 'token_prefix' => env('SANCTUM_TOKEN_PREFIX', ''), + + /* + |-------------------------------------------------------------------------- + | Sanctum Middleware + |-------------------------------------------------------------------------- + | + | When authenticating your first-party SPA with Sanctum you may need to + | customize some of the middleware Sanctum uses while processing the + | request. You may change the middleware listed below as required. + | + */ + + 'middleware' => [ + 'authenticate_session' => Laravel\Sanctum\Http\Middleware\AuthenticateSession::class, + 'encrypt_cookies' => Illuminate\Cookie\Middleware\EncryptCookies::class, + 'validate_csrf_token' => Illuminate\Foundation\Http\Middleware\ValidateCsrfToken::class, + ], + +]; diff --git a/database/migrations/2025_09_29_011914_create_personal_access_tokens_table.php b/database/migrations/2025_09_29_011914_create_personal_access_tokens_table.php new file mode 100644 index 0000000..40ff706 --- /dev/null +++ b/database/migrations/2025_09_29_011914_create_personal_access_tokens_table.php @@ -0,0 +1,33 @@ +id(); + $table->morphs('tokenable'); + $table->text('name'); + $table->string('token', 64)->unique(); + $table->text('abilities')->nullable(); + $table->timestamp('last_used_at')->nullable(); + $table->timestamp('expires_at')->nullable()->index(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('personal_access_tokens'); + } +}; diff --git a/routes/api.php b/routes/api.php new file mode 100644 index 0000000..ccc387f --- /dev/null +++ b/routes/api.php @@ -0,0 +1,8 @@ +user(); +})->middleware('auth:sanctum'); From 667fbbf76664f8b1ec1f093172874386c9ca7fde Mon Sep 17 00:00:00 2001 From: Benoit Borrel Date: Mon, 29 Sep 2025 08:58:16 -0400 Subject: [PATCH 07/16] Donation: add API route + ctrl + validation for creation --- app/Http/Controllers/DonationController.php | 22 +++++++++- app/Http/Requests/StoreDonationRequest.php | 46 +++++++++++++++++++++ app/Models/Donation.php | 4 ++ app/Models/User.php | 3 +- database/factories/DonationFactory.php | 2 +- routes/api.php | 4 ++ tests/Feature/DonationTest.php | 40 ++++++++++++++++-- 7 files changed, 113 insertions(+), 8 deletions(-) create mode 100644 app/Http/Requests/StoreDonationRequest.php diff --git a/app/Http/Controllers/DonationController.php b/app/Http/Controllers/DonationController.php index de4a837..fe1d70f 100644 --- a/app/Http/Controllers/DonationController.php +++ b/app/Http/Controllers/DonationController.php @@ -2,9 +2,27 @@ namespace App\Http\Controllers; -use Illuminate\Http\Request; +use App\Http\Requests\StoreDonationRequest; +use App\Models\Donation; +use Illuminate\Http\JsonResponse; +use Symfony\Component\HttpFoundation\Response; class DonationController extends Controller { - // + public function store(StoreDonationRequest $request): JsonResponse + { + $validated = $request->validated(); + + $donation = Donation::create([ + 'user_id' => $request->user()->id, + 'amount' => $validated['amount'], + 'currency' => strtoupper($validated['currency']), + 'donated_at' => now(), + ])->refresh(); + + return response()->json([ + 'message' => 'Donation created successfully', + 'donation' => $donation, + ], Response::HTTP_CREATED); + } } diff --git a/app/Http/Requests/StoreDonationRequest.php b/app/Http/Requests/StoreDonationRequest.php new file mode 100644 index 0000000..217c75a --- /dev/null +++ b/app/Http/Requests/StoreDonationRequest.php @@ -0,0 +1,46 @@ + + */ + public function rules(): array + { + return [ + 'amount' => ['required', 'numeric', 'min:1'], + 'currency' => ['required', 'string', 'size:3'], + // 'status' => ['sometimes', 'string', 'in:cancelled,completed,failed,pending,refunded'], + ]; + } + + /** + * Get custom error messages for validation rules. + * + * @return array + */ + public function messages(): array + { + return [ + 'amount.required' => 'Donation amount is required.', + 'amount.numeric' => 'Donation amount must be a number.', + 'amount.min' => 'Donation amount must be at least 1.', + 'currency.required' => 'Currency is required.', + 'currency.size' => 'Currency must be a 3-letter code.', + // 'status.in' => 'Status must be one of: cancelled, completed, failed, pending, refunded.', + ]; + } +} diff --git a/app/Models/Donation.php b/app/Models/Donation.php index 234c771..f16fcf3 100644 --- a/app/Models/Donation.php +++ b/app/Models/Donation.php @@ -17,6 +17,10 @@ class Donation extends Model * @var array */ protected $fillable = [ + 'user_id', + 'amount', + 'currency', + 'donated_at', 'status', ]; diff --git a/app/Models/User.php b/app/Models/User.php index 3dfbd80..8c65665 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -6,11 +6,12 @@ use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Notifications\Notifiable; +use Laravel\Sanctum\HasApiTokens; class User extends Authenticatable { /** @use HasFactory<\Database\Factories\UserFactory> */ - use HasFactory, Notifiable; + use HasApiTokens, HasFactory, Notifiable; /** * The attributes that are mass assignable. diff --git a/database/factories/DonationFactory.php b/database/factories/DonationFactory.php index fd95371..d78bd2d 100644 --- a/database/factories/DonationFactory.php +++ b/database/factories/DonationFactory.php @@ -19,7 +19,7 @@ public function definition(): array { return [ 'user_id' => 1, - 'amount' => 100, + 'amount' => 100.00, 'currency' => 'CAD', 'donated_at' => now(), ]; diff --git a/routes/api.php b/routes/api.php index ccc387f..37b8cc8 100644 --- a/routes/api.php +++ b/routes/api.php @@ -1,8 +1,12 @@ user(); })->middleware('auth:sanctum'); + +Route::post('/donation', [DonationController::class, 'store']) + ->middleware('auth:sanctum'); diff --git a/tests/Feature/DonationTest.php b/tests/Feature/DonationTest.php index f32a2b6..496dcc4 100644 --- a/tests/Feature/DonationTest.php +++ b/tests/Feature/DonationTest.php @@ -6,13 +6,14 @@ use App\Models\Donation; use App\Models\User; use Illuminate\Foundation\Testing\RefreshDatabase; +use Symfony\Component\HttpFoundation\Response; use Tests\TestCase; class DonationTest extends TestCase { use RefreshDatabase; - public function test_donation_edited(): void + public function test_donation_edition_via_model(): void { $user = User::factory()->create(); $donation = Donation::factory()->create(); @@ -25,15 +26,46 @@ public function test_donation_edited(): void 'user_id' => $donation->user_id, 'amount' => $donation->amount, 'currency' => $donation->currency, - 'status' => DonationStatus::PENDING, + 'status' => DonationStatus::PENDING->value, ]); $donation->update([ - 'status' => DonationStatus::COMPLETED, + 'status' => DonationStatus::COMPLETED->value, ]); $this->assertDatabaseHas('donations', [ 'id' => $donation->id, - 'status' => DonationStatus::COMPLETED, + 'status' => DonationStatus::COMPLETED->value, ]); } + + public function test_donation_edition_via_api(): void + { + $user = User::factory()->create(); + $token = $user->createToken('TestToken')->plainTextToken; + + $payload = [ + 'amount' => 100.00, + 'currency' => 'CAD', + ]; + + $this->assertDatabaseCount('users', 1); + $this->assertDatabaseCount('donations', 0); + + $response = $this->actingAs($user)->postJson('/api/donation', $payload); + + $response->assertStatus(Response::HTTP_CREATED) + ->assertJson([ + 'message' => 'Donation created successfully', + 'donation' => [ + 'user_id' => $user->id, + 'status' => DonationStatus::PENDING->value, + ] + $payload, + ]); + + $this->assertDatabaseCount('donations', 1); + $this->assertDatabaseHas('donations', [ + 'user_id' => $user->id, + 'status' => DonationStatus::PENDING->value, + ] + $payload); + } } From f7264ae7f20d030824acb4c20a6196efea8db95f Mon Sep 17 00:00:00 2001 From: Benoit Borrel Date: Mon, 29 Sep 2025 09:10:57 -0400 Subject: [PATCH 08/16] Donation: improve model factory + feature test --- database/factories/DonationFactory.php | 6 +++--- tests/Feature/DonationTest.php | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/database/factories/DonationFactory.php b/database/factories/DonationFactory.php index d78bd2d..8d209a9 100644 --- a/database/factories/DonationFactory.php +++ b/database/factories/DonationFactory.php @@ -2,7 +2,7 @@ namespace Database\Factories; -use App\DonationStatus; +use App\Models\User; use Illuminate\Database\Eloquent\Factories\Factory; /** @@ -18,8 +18,8 @@ class DonationFactory extends Factory public function definition(): array { return [ - 'user_id' => 1, - 'amount' => 100.00, + 'user_id' => User::factory(), + 'amount' => fake()->randomFloat(nbMaxDecimals: 2, min: 1, max: 500), 'currency' => 'CAD', 'donated_at' => now(), ]; diff --git a/tests/Feature/DonationTest.php b/tests/Feature/DonationTest.php index 496dcc4..46e3ec1 100644 --- a/tests/Feature/DonationTest.php +++ b/tests/Feature/DonationTest.php @@ -15,7 +15,6 @@ class DonationTest extends TestCase public function test_donation_edition_via_model(): void { - $user = User::factory()->create(); $donation = Donation::factory()->create(); $this->assertDatabaseCount('users', 1); @@ -40,6 +39,7 @@ public function test_donation_edition_via_model(): void public function test_donation_edition_via_api(): void { + /** @var User $user */ $user = User::factory()->create(); $token = $user->createToken('TestToken')->plainTextToken; From 8a4d5e939065e315877fe078de1909b138aa31cc Mon Sep 17 00:00:00 2001 From: Benoit Borrel Date: Mon, 29 Sep 2025 09:22:05 -0400 Subject: [PATCH 09/16] API: fix route path for RESTful convention --- routes/api.php | 2 +- tests/Feature/DonationTest.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/routes/api.php b/routes/api.php index 37b8cc8..ed5706e 100644 --- a/routes/api.php +++ b/routes/api.php @@ -8,5 +8,5 @@ return $request->user(); })->middleware('auth:sanctum'); -Route::post('/donation', [DonationController::class, 'store']) +Route::post('/donations', [DonationController::class, 'store']) ->middleware('auth:sanctum'); diff --git a/tests/Feature/DonationTest.php b/tests/Feature/DonationTest.php index 46e3ec1..b637fb3 100644 --- a/tests/Feature/DonationTest.php +++ b/tests/Feature/DonationTest.php @@ -51,7 +51,7 @@ public function test_donation_edition_via_api(): void $this->assertDatabaseCount('users', 1); $this->assertDatabaseCount('donations', 0); - $response = $this->actingAs($user)->postJson('/api/donation', $payload); + $response = $this->actingAs($user)->postJson('/api/donations', $payload); $response->assertStatus(Response::HTTP_CREATED) ->assertJson([ From 4cb3f6352c7b7ead5ef77ba4cd4ec4805154784e Mon Sep 17 00:00:00 2001 From: Benoit Borrel Date: Mon, 29 Sep 2025 09:40:50 -0400 Subject: [PATCH 10/16] Donation: add API route + ctlr + validation for update --- app/Http/Controllers/DonationController.php | 12 +++++++ app/Http/Requests/UpdateDonationRequest.php | 39 +++++++++++++++++++++ routes/api.php | 4 +++ tests/Feature/DonationTest.php | 35 +++++++++++++++--- 4 files changed, 85 insertions(+), 5 deletions(-) create mode 100644 app/Http/Requests/UpdateDonationRequest.php diff --git a/app/Http/Controllers/DonationController.php b/app/Http/Controllers/DonationController.php index fe1d70f..c721c58 100644 --- a/app/Http/Controllers/DonationController.php +++ b/app/Http/Controllers/DonationController.php @@ -3,6 +3,7 @@ namespace App\Http\Controllers; use App\Http\Requests\StoreDonationRequest; +use App\Http\Requests\UpdateDonationRequest; use App\Models\Donation; use Illuminate\Http\JsonResponse; use Symfony\Component\HttpFoundation\Response; @@ -25,4 +26,15 @@ public function store(StoreDonationRequest $request): JsonResponse 'donation' => $donation, ], Response::HTTP_CREATED); } + + public function update(Donation $donation, UpdateDonationRequest $request): JsonResponse + { + $validated = $request->validated(); + $donation->update($validated); + + return response()->json([ + 'message' => 'Donation updated successfully', + 'donation' => $donation, + ], Response::HTTP_OK); + } } diff --git a/app/Http/Requests/UpdateDonationRequest.php b/app/Http/Requests/UpdateDonationRequest.php new file mode 100644 index 0000000..e61e7a1 --- /dev/null +++ b/app/Http/Requests/UpdateDonationRequest.php @@ -0,0 +1,39 @@ + + */ + public function rules(): array + { + return [ + 'status' => ['required', 'string', 'in:cancelled,completed,failed,refunded'], + ]; + } + + /** + * Get custom error messages for validation rules. + * + * @return array + */ + public function messages(): array + { + return [ + 'status.in' => 'Status must be one of: cancelled, completed, failed, refunded.', + ]; + } +} diff --git a/routes/api.php b/routes/api.php index ed5706e..560965a 100644 --- a/routes/api.php +++ b/routes/api.php @@ -10,3 +10,7 @@ Route::post('/donations', [DonationController::class, 'store']) ->middleware('auth:sanctum'); + +Route::patch('/donations/{donation}', [DonationController::class, 'update']) + ->middleware('auth:sanctum'); + diff --git a/tests/Feature/DonationTest.php b/tests/Feature/DonationTest.php index b637fb3..8811e22 100644 --- a/tests/Feature/DonationTest.php +++ b/tests/Feature/DonationTest.php @@ -43,29 +43,54 @@ public function test_donation_edition_via_api(): void $user = User::factory()->create(); $token = $user->createToken('TestToken')->plainTextToken; - $payload = [ - 'amount' => 100.00, + // create a donation + $payloadCreate = [ + 'amount' => '100.00', 'currency' => 'CAD', ]; $this->assertDatabaseCount('users', 1); $this->assertDatabaseCount('donations', 0); - $response = $this->actingAs($user)->postJson('/api/donations', $payload); + $response = $this->actingAs($user)->postJson('/api/donations', $payloadCreate); $response->assertStatus(Response::HTTP_CREATED) ->assertJson([ 'message' => 'Donation created successfully', 'donation' => [ + 'id' => 2, 'user_id' => $user->id, 'status' => DonationStatus::PENDING->value, - ] + $payload, + ] + $payloadCreate, ]); $this->assertDatabaseCount('donations', 1); $this->assertDatabaseHas('donations', [ 'user_id' => $user->id, 'status' => DonationStatus::PENDING->value, - ] + $payload); + ] + $payloadCreate); + + // update a donation status + $payloadUpdate = [ + 'status' => DonationStatus::COMPLETED->value, + ]; + + $response = $this->actingAs($user)->patchJson("/api/donations/2", $payloadUpdate); + + $response->assertStatus(Response::HTTP_OK) + ->assertJson([ + 'message' => 'Donation updated successfully', + 'donation' => [ + 'id' => 2, + 'user_id' => $user->id, + 'status' => DonationStatus::COMPLETED->value, + ] + $payloadCreate + $payloadUpdate + ]); + + $this->assertDatabaseCount('donations', 1); + $this->assertDatabaseHas('donations', [ + 'user_id' => $user->id, + 'status' => DonationStatus::COMPLETED->value, + ] + $payloadCreate + $payloadUpdate); } } From 0234985ddf4e6d126ef4e453f55ab60dacbac2f0 Mon Sep 17 00:00:00 2001 From: Benoit Borrel Date: Mon, 29 Sep 2025 10:49:26 -0400 Subject: [PATCH 11/16] artisan make:model RecurringDonation -mfsc --- .../RecurringDonationController.php | 10 +++++++ app/Models/RecurringDonation.php | 12 +++++++++ .../factories/RecurringDonationFactory.php | 23 ++++++++++++++++ ...44742_create_recurring_donations_table.php | 27 +++++++++++++++++++ database/seeders/RecurringDonationSeeder.php | 17 ++++++++++++ 5 files changed, 89 insertions(+) create mode 100644 app/Http/Controllers/RecurringDonationController.php create mode 100644 app/Models/RecurringDonation.php create mode 100644 database/factories/RecurringDonationFactory.php create mode 100644 database/migrations/2025_09_29_144742_create_recurring_donations_table.php create mode 100644 database/seeders/RecurringDonationSeeder.php diff --git a/app/Http/Controllers/RecurringDonationController.php b/app/Http/Controllers/RecurringDonationController.php new file mode 100644 index 0000000..6553cc7 --- /dev/null +++ b/app/Http/Controllers/RecurringDonationController.php @@ -0,0 +1,10 @@ + */ + use HasFactory; +} diff --git a/database/factories/RecurringDonationFactory.php b/database/factories/RecurringDonationFactory.php new file mode 100644 index 0000000..268a17b --- /dev/null +++ b/database/factories/RecurringDonationFactory.php @@ -0,0 +1,23 @@ + + */ +class RecurringDonationFactory extends Factory +{ + /** + * Define the model's default state. + * + * @return array + */ + public function definition(): array + { + return [ + // + ]; + } +} diff --git a/database/migrations/2025_09_29_144742_create_recurring_donations_table.php b/database/migrations/2025_09_29_144742_create_recurring_donations_table.php new file mode 100644 index 0000000..074add9 --- /dev/null +++ b/database/migrations/2025_09_29_144742_create_recurring_donations_table.php @@ -0,0 +1,27 @@ +id(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('recurring_donations'); + } +}; diff --git a/database/seeders/RecurringDonationSeeder.php b/database/seeders/RecurringDonationSeeder.php new file mode 100644 index 0000000..8c74e64 --- /dev/null +++ b/database/seeders/RecurringDonationSeeder.php @@ -0,0 +1,17 @@ + Date: Mon, 29 Sep 2025 10:56:20 -0400 Subject: [PATCH 12/16] DB schema: add recurring donation migration + model --- app/Models/Donation.php | 1 + app/Models/RecurringDonation.php | 28 ++++++++++++++++ app/RecurringDonationStatus.php | 10 ++++++ ...44742_create_recurring_donations_table.php | 32 +++++++++++++++++++ 4 files changed, 71 insertions(+) create mode 100644 app/RecurringDonationStatus.php diff --git a/app/Models/Donation.php b/app/Models/Donation.php index f16fcf3..103f9ac 100644 --- a/app/Models/Donation.php +++ b/app/Models/Donation.php @@ -22,6 +22,7 @@ class Donation extends Model 'currency', 'donated_at', 'status', + 'recurring_donation_id', ]; /** diff --git a/app/Models/RecurringDonation.php b/app/Models/RecurringDonation.php index 6bc2460..2adc1a9 100644 --- a/app/Models/RecurringDonation.php +++ b/app/Models/RecurringDonation.php @@ -2,6 +2,7 @@ namespace App\Models; +use App\RecurringDonationStatus; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; @@ -9,4 +10,31 @@ class RecurringDonation extends Model { /** @use HasFactory<\Database\Factories\RecurringDonationFactory> */ use HasFactory; + + /** + * The attributes that are mass assignable. + * + * @var array + */ + protected $fillable = [ + 'user_id', + 'amount', + 'currency', + 'schedule', + 'start_date', + 'end_date', + 'status', + ]; + + /** + * Get the attributes that should be cast. + * + * @return array + */ + protected function casts(): array + { + return [ + 'status' => RecurringDonationStatus::class, + ]; + } } diff --git a/app/RecurringDonationStatus.php b/app/RecurringDonationStatus.php new file mode 100644 index 0000000..cce6bfc --- /dev/null +++ b/app/RecurringDonationStatus.php @@ -0,0 +1,10 @@ +id(); + $table->foreignIdFor(User::class); + $table->decimal('amount', total: 8, places: 2); + $table->string('currency', length: 3); + $table->string('schedule'); + $table->timestampTz('start_date'); + $table->timestampTz('end_date')->nullable(); + $table->enum( + 'status', + array_column(RecurringDonationStatus::cases(), 'value') + )->default(RecurringDonationStatus::ACTIVE->value); $table->timestamps(); }); + + // Add a check constraint for cron format: ^([*d/,-]+s){4}[*d/,-]+$ + DB::statement( + "ALTER TABLE recurring_donations + ADD CONSTRAINT chk_schedule_cron_format + CHECK (schedule REGEXP '^([\\*\\d\\/,\\-]+\\s){4}[\\*\\d\\/,\\-]+$')" + ); + + Schema::table('donations', function (Blueprint $table) { + $table->foreignId('recurring_donation_id') + ->nullable() + ->constrained('recurring_donations') + ->cascadeOnDelete(); + }); } /** @@ -22,6 +49,11 @@ public function up(): void */ public function down(): void { + Schema::table('donations', function (Blueprint $table) { + $table->dropForeign(['recurring_donation_id']); + $table->dropColumn('recurring_donation_id'); + }); + Schema::dropIfExists('recurring_donations'); } }; From fb478f8ac7634032ddb90b7386cc59fd7e44f31e Mon Sep 17 00:00:00 2001 From: Benoit Borrel Date: Mon, 29 Sep 2025 12:06:38 -0400 Subject: [PATCH 13/16] Donation: add history by user (route + ctlr + resource collection) --- app/Http/Controllers/DonationController.php | 14 ++++++ app/Http/Resources/DonationCollection.php | 19 ++++++++ app/Http/Resources/DonationResource.php | 29 +++++++++++ routes/api.php | 5 +- tests/Feature/DonationTest.php | 54 +++++++++++++++++++++ 5 files changed, 120 insertions(+), 1 deletion(-) create mode 100644 app/Http/Resources/DonationCollection.php create mode 100644 app/Http/Resources/DonationResource.php diff --git a/app/Http/Controllers/DonationController.php b/app/Http/Controllers/DonationController.php index c721c58..81c677b 100644 --- a/app/Http/Controllers/DonationController.php +++ b/app/Http/Controllers/DonationController.php @@ -2,8 +2,10 @@ namespace App\Http\Controllers; +use App\DonationStatus; use App\Http\Requests\StoreDonationRequest; use App\Http\Requests\UpdateDonationRequest; +use App\Http\Resources\DonationResource; use App\Models\Donation; use Illuminate\Http\JsonResponse; use Symfony\Component\HttpFoundation\Response; @@ -37,4 +39,16 @@ public function update(Donation $donation, UpdateDonationRequest $request): Json 'donation' => $donation, ], Response::HTTP_OK); } + + public function historyByUser(string $user_id): JsonResponse + { + $donations = Donation::where('user_id', $user_id) + ->orderBy('donated_at', 'desc') + ->where('status', DonationStatus::COMPLETED->value) + ->get(); + + return response()->json([ + 'donations' => DonationResource::collection($donations), + ], Response::HTTP_OK); + } } diff --git a/app/Http/Resources/DonationCollection.php b/app/Http/Resources/DonationCollection.php new file mode 100644 index 0000000..37c687d --- /dev/null +++ b/app/Http/Resources/DonationCollection.php @@ -0,0 +1,19 @@ + + */ + public function toArray(Request $request): array + { + return parent::toArray($request); + } +} diff --git a/app/Http/Resources/DonationResource.php b/app/Http/Resources/DonationResource.php new file mode 100644 index 0000000..b2c065f --- /dev/null +++ b/app/Http/Resources/DonationResource.php @@ -0,0 +1,29 @@ + + */ + public function toArray(Request $request): array + { + return [ + 'id' => $this->id, + 'user_id' => $this->user_id, + 'amount' => $this->amount, + 'currency' => $this->currency, + 'status' => $this->status, + 'is_recurring' => $this->recurring_donation_id ? 'true' : 'false', + 'donated_at' => $this->donated_at, + 'created_at' => $this->created_at, + 'updated_at' => $this->updated_at, + ]; + } +} diff --git a/routes/api.php b/routes/api.php index 560965a..dfa8b3f 100644 --- a/routes/api.php +++ b/routes/api.php @@ -8,9 +8,12 @@ return $request->user(); })->middleware('auth:sanctum'); +// @todo group routes belonging to DonationController Route::post('/donations', [DonationController::class, 'store']) ->middleware('auth:sanctum'); - Route::patch('/donations/{donation}', [DonationController::class, 'update']) ->middleware('auth:sanctum'); +Route::get('/donations/history/{user_id}', [DonationController::class, 'historyByUser']) + ->middleware('auth:sanctum') + ->whereNumber('user_id'); diff --git a/tests/Feature/DonationTest.php b/tests/Feature/DonationTest.php index 8811e22..ea9c7a5 100644 --- a/tests/Feature/DonationTest.php +++ b/tests/Feature/DonationTest.php @@ -31,6 +31,7 @@ public function test_donation_edition_via_model(): void $donation->update([ 'status' => DonationStatus::COMPLETED->value, ]); + $this->assertDatabaseHas('donations', [ 'id' => $donation->id, 'status' => DonationStatus::COMPLETED->value, @@ -39,6 +40,7 @@ public function test_donation_edition_via_model(): void public function test_donation_edition_via_api(): void { + // ARRANGE 1 /** @var User $user */ $user = User::factory()->create(); $token = $user->createToken('TestToken')->plainTextToken; @@ -52,8 +54,10 @@ public function test_donation_edition_via_api(): void $this->assertDatabaseCount('users', 1); $this->assertDatabaseCount('donations', 0); + // ACT 1 $response = $this->actingAs($user)->postJson('/api/donations', $payloadCreate); + // ASSERT 1 $response->assertStatus(Response::HTTP_CREATED) ->assertJson([ 'message' => 'Donation created successfully', @@ -70,13 +74,16 @@ public function test_donation_edition_via_api(): void 'status' => DonationStatus::PENDING->value, ] + $payloadCreate); + // ARRANGE 2 // update a donation status $payloadUpdate = [ 'status' => DonationStatus::COMPLETED->value, ]; + // ACT 2 $response = $this->actingAs($user)->patchJson("/api/donations/2", $payloadUpdate); + // ASSERT 2 $response->assertStatus(Response::HTTP_OK) ->assertJson([ 'message' => 'Donation updated successfully', @@ -93,4 +100,51 @@ public function test_donation_edition_via_api(): void 'status' => DonationStatus::COMPLETED->value, ] + $payloadCreate + $payloadUpdate); } + + public function test_donation_history_by_user(): void + { + // ARRANGE + /** @var User $user */ + $user = User::factory()->create(); + $token = $user->createToken('TestToken')->plainTextToken; + $otherUser = User::factory()->create(); + + // Completed donations for $user + $completedDonations = Donation::factory() + ->count(2) + ->create([ + 'user_id' => $user->id, + 'status' => DonationStatus::COMPLETED->value, + ]); + + // Pending donation for $user + Donation::factory()->create([ + 'user_id' => $user->id, + 'status' => DonationStatus::PENDING->value, + ]); + + // Completed donation for another user + Donation::factory()->create([ + 'user_id' => $otherUser->id, + 'status' => DonationStatus::COMPLETED->value, + ]); + + // ACT + $response = $this->actingAs($user)->getJson("/api/donations/history/{$user->id}"); + print_r($response->json()); + + // ASSERT + $response->assertStatus(Response::HTTP_OK) + ->assertJsonCount(2, 'donations') + ->assertJsonFragment([ + 'user_id' => $user->id, + 'status' => DonationStatus::COMPLETED->value, + ]); + + // Ensure only completed donations for $user are returned + foreach ($response->json('donations') as $donation) { + $this->assertEquals($user->id, $donation['user_id']); + $this->assertEquals(DonationStatus::COMPLETED->value, $donation['status']); + } + } } From 994b4b15fde32e01f3288798fe046f9ee61fe1b8 Mon Sep 17 00:00:00 2001 From: Benoit Borrel Date: Mon, 29 Sep 2025 12:09:50 -0400 Subject: [PATCH 14/16] Group donationsI routes by controller --- routes/api.php | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/routes/api.php b/routes/api.php index dfa8b3f..220d250 100644 --- a/routes/api.php +++ b/routes/api.php @@ -9,11 +9,9 @@ })->middleware('auth:sanctum'); // @todo group routes belonging to DonationController -Route::post('/donations', [DonationController::class, 'store']) - ->middleware('auth:sanctum'); -Route::patch('/donations/{donation}', [DonationController::class, 'update']) - ->middleware('auth:sanctum'); -Route::get('/donations/history/{user_id}', [DonationController::class, 'historyByUser']) - ->middleware('auth:sanctum') - ->whereNumber('user_id'); +Route::controller(DonationController::class)->group(function () { + Route::post('/donations', 'store')->middleware('auth:sanctum'); + Route::patch('/donations/{donation}', 'update')->middleware('auth:sanctum'); + Route::get('/donations/history/{user_id}', 'historyByUser')->middleware('auth:sanctum')->whereNumber('user_id'); +}); From d866340cc41e190cc6bfc17e5134599cb86c6b38 Mon Sep 17 00:00:00 2001 From: Benoit Borrel Date: Mon, 29 Sep 2025 12:10:31 -0400 Subject: [PATCH 15/16] Remove forgotten print_r() --- tests/Feature/DonationTest.php | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/Feature/DonationTest.php b/tests/Feature/DonationTest.php index ea9c7a5..1b93b3e 100644 --- a/tests/Feature/DonationTest.php +++ b/tests/Feature/DonationTest.php @@ -131,7 +131,6 @@ public function test_donation_history_by_user(): void // ACT $response = $this->actingAs($user)->getJson("/api/donations/history/{$user->id}"); - print_r($response->json()); // ASSERT $response->assertStatus(Response::HTTP_OK) From 4719fffb493309d7265881ad1a31277f9678bcac Mon Sep 17 00:00:00 2001 From: Benoit Borrel Date: Mon, 29 Sep 2025 12:16:27 -0400 Subject: [PATCH 16/16] Ignore + remove /.github --- .github/copilot-instructions.md | 221 -------------------------------- .gitignore | 2 + 2 files changed, 2 insertions(+), 221 deletions(-) delete mode 100644 .github/copilot-instructions.md diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md deleted file mode 100644 index ad1a90b..0000000 --- a/.github/copilot-instructions.md +++ /dev/null @@ -1,221 +0,0 @@ - -=== foundation rules === - -# Laravel Boost Guidelines - -The Laravel Boost guidelines are specifically curated by Laravel maintainers for this application. These guidelines should be followed closely to enhance the user's satisfaction building Laravel applications. - -## Foundational Context -This application is a Laravel application and its main Laravel ecosystems package & versions are below. You are an expert with them all. Ensure you abide by these specific packages & versions. - -- php - 8.2.29 -- laravel/framework (LARAVEL) - v11 -- laravel/prompts (PROMPTS) - v0 -- laravel/pint (PINT) - v1 -- tailwindcss (TAILWINDCSS) - v3 - - -## Conventions -- You must follow all existing code conventions used in this application. When creating or editing a file, check sibling files for the correct structure, approach, naming. -- Use descriptive names for variables and methods. For example, `isRegisteredForDiscounts`, not `discount()`. -- Check for existing components to reuse before writing a new one. - -## Verification Scripts -- Do not create verification scripts or tinker when tests cover that functionality and prove it works. Unit and feature tests are more important. - -## Application Structure & Architecture -- Stick to existing directory structure - don't create new base folders without approval. -- Do not change the application's dependencies without approval. - -## Frontend Bundling -- If the user doesn't see a frontend change reflected in the UI, it could mean they need to run `npm run build`, `npm run dev`, or `composer run dev`. Ask them. - -## Replies -- Be concise in your explanations - focus on what's important rather than explaining obvious details. - -## Documentation Files -- You must only create documentation files if explicitly requested by the user. - - -=== boost rules === - -## Laravel Boost -- Laravel Boost is an MCP server that comes with powerful tools designed specifically for this application. Use them. - -## Artisan -- Use the `list-artisan-commands` tool when you need to call an Artisan command to double check the available parameters. - -## URLs -- Whenever you share a project URL with the user you should use the `get-absolute-url` tool to ensure you're using the correct scheme, domain / IP, and port. - -## Tinker / Debugging -- You should use the `tinker` tool when you need to execute PHP to debug code or query Eloquent models directly. -- Use the `database-query` tool when you only need to read from the database. - -## Reading Browser Logs With the `browser-logs` Tool -- You can read browser logs, errors, and exceptions using the `browser-logs` tool from Boost. -- Only recent browser logs will be useful - ignore old logs. - -## Searching Documentation (Critically Important) -- Boost comes with a powerful `search-docs` tool you should use before any other approaches. This tool automatically passes a list of installed packages and their versions to the remote Boost API, so it returns only version-specific documentation specific for the user's circumstance. You should pass an array of packages to filter on if you know you need docs for particular packages. -- The 'search-docs' tool is perfect for all Laravel related packages, including Laravel, Inertia, Livewire, Filament, Tailwind, Pest, Nova, Nightwatch, etc. -- You must use this tool to search for Laravel-ecosystem documentation before falling back to other approaches. -- Search the documentation before making code changes to ensure we are taking the correct approach. -- Use multiple, broad, simple, topic based queries to start. For example: `['rate limiting', 'routing rate limiting', 'routing']`. -- Do not add package names to queries - package information is already shared. For example, use `test resource table`, not `filament 4 test resource table`. - -### Available Search Syntax -- You can and should pass multiple queries at once. The most relevant results will be returned first. - -1. Simple Word Searches with auto-stemming - query=authentication - finds 'authenticate' and 'auth' -2. Multiple Words (AND Logic) - query=rate limit - finds knowledge containing both "rate" AND "limit" -3. Quoted Phrases (Exact Position) - query="infinite scroll" - Words must be adjacent and in that order -4. Mixed Queries - query=middleware "rate limit" - "middleware" AND exact phrase "rate limit" -5. Multiple Queries - queries=["authentication", "middleware"] - ANY of these terms - - -=== php rules === - -## PHP - -- Always use curly braces for control structures, even if it has one line. - -### Constructors -- Use PHP 8 constructor property promotion in `__construct()`. - - public function __construct(public GitHub $github) { } -- Do not allow empty `__construct()` methods with zero parameters. - -### Type Declarations -- Always use explicit return type declarations for methods and functions. -- Use appropriate PHP type hints for method parameters. - - -protected function isAccessible(User $user, ?string $path = null): bool -{ - ... -} - - -## Comments -- Prefer PHPDoc blocks over comments. Never use comments within the code itself unless there is something _very_ complex going on. - -## PHPDoc Blocks -- Add useful array shape type definitions for arrays when appropriate. - -## Enums -- Typically, keys in an Enum should be TitleCase. For example: `FavoritePerson`, `BestLake`, `Monthly`. - - -=== laravel/core rules === - -## Do Things the Laravel Way - -- Use `php artisan make:` commands to create new files (i.e. migrations, controllers, models, etc.). You can list available Artisan commands using the `list-artisan-commands` tool. -- If you're creating a generic PHP class, use `artisan make:class`. -- Pass `--no-interaction` to all Artisan commands to ensure they work without user input. You should also pass the correct `--options` to ensure correct behavior. - -### Database -- Always use proper Eloquent relationship methods with return type hints. Prefer relationship methods over raw queries or manual joins. -- Use Eloquent models and relationships before suggesting raw database queries -- Avoid `DB::`; prefer `Model::query()`. Generate code that leverages Laravel's ORM capabilities rather than bypassing them. -- Generate code that prevents N+1 query problems by using eager loading. -- Use Laravel's query builder for very complex database operations. - -### Model Creation -- When creating new models, create useful factories and seeders for them too. Ask the user if they need any other things, using `list-artisan-commands` to check the available options to `php artisan make:model`. - -### APIs & Eloquent Resources -- For APIs, default to using Eloquent API Resources and API versioning unless existing API routes do not, then you should follow existing application convention. - -### Controllers & Validation -- Always create Form Request classes for validation rather than inline validation in controllers. Include both validation rules and custom error messages. -- Check sibling Form Requests to see if the application uses array or string based validation rules. - -### Queues -- Use queued jobs for time-consuming operations with the `ShouldQueue` interface. - -### Authentication & Authorization -- Use Laravel's built-in authentication and authorization features (gates, policies, Sanctum, etc.). - -### URL Generation -- When generating links to other pages, prefer named routes and the `route()` function. - -### Configuration -- Use environment variables only in configuration files - never use the `env()` function directly outside of config files. Always use `config('app.name')`, not `env('APP_NAME')`. - -### Testing -- When creating models for tests, use the factories for the models. Check if the factory has custom states that can be used before manually setting up the model. -- Faker: Use methods such as `$this->faker->word()` or `fake()->randomDigit()`. Follow existing conventions whether to use `$this->faker` or `fake()`. -- When creating tests, make use of `php artisan make:test [options] ` to create a feature test, and pass `--unit` to create a unit test. Most tests should be feature tests. - -### Vite Error -- If you receive an "Illuminate\Foundation\ViteException: Unable to locate file in Vite manifest" error, you can run `npm run build` or ask the user to run `npm run dev` or `composer run dev`. - - -=== laravel/v11 rules === - -## Laravel 11 - -- Use the `search-docs` tool to get version specific documentation. -- Laravel 11 brought a new streamlined file structure which this project now uses. - -### Laravel 11 Structure -- No middleware files in `app/Http/Middleware/`. -- `bootstrap/app.php` is the file to register middleware, exceptions, and routing files. -- `bootstrap/providers.php` contains application specific service providers. -- **No app\Console\Kernel.php** - use `bootstrap/app.php` or `routes/console.php` for console configuration. -- **Commands auto-register** - files in `app/Console/Commands/` are automatically available and do not require manual registration. - -### Database -- When modifying a column, the migration must include all of the attributes that were previously defined on the column. Otherwise, they will be dropped and lost. -- Laravel 11 allows limiting eagerly loaded records natively, without external packages: `$query->latest()->limit(10);`. - -### Models -- Casts can and likely should be set in a `casts()` method on a model rather than the `$casts` property. Follow existing conventions from other models. - -### New Artisan Commands -- List Artisan commands using Boost's MCP tool, if available. New commands available in Laravel 11: - - `php artisan make:enum` - - `php artisan make:class` - - `php artisan make:interface` - - -=== pint/core rules === - -## Laravel Pint Code Formatter - -- You must run `vendor/bin/pint --dirty` before finalizing changes to ensure your code matches the project's expected style. -- Do not run `vendor/bin/pint --test`, simply run `vendor/bin/pint` to fix any formatting issues. - - -=== tailwindcss/core rules === - -## Tailwind Core - -- Use Tailwind CSS classes to style HTML, check and use existing tailwind conventions within the project before writing your own. -- Offer to extract repeated patterns into components that match the project's conventions (i.e. Blade, JSX, Vue, etc..) -- Think through class placement, order, priority, and defaults - remove redundant classes, add classes to parent or child carefully to limit repetition, group elements logically -- You can use the `search-docs` tool to get exact examples from the official documentation when needed. - -### Spacing -- When listing items, use gap utilities for spacing, don't use margins. - - -
-
Superior
-
Michigan
-
Erie
-
-
- - -### Dark Mode -- If existing pages and components support dark mode, new pages and components must support dark mode in a similar way, typically using `dark:`. - - -=== tailwindcss/v3 rules === - -## Tailwind 3 - -- Always use Tailwind CSS v3 - verify you're using only classes supported by this version. -
\ No newline at end of file diff --git a/.gitignore b/.gitignore index afa306b..95d7c30 100644 --- a/.gitignore +++ b/.gitignore @@ -19,3 +19,5 @@ yarn-error.log /.idea /.vscode /.zed + +/.github