Compare commits
1 Commits
| Author | SHA1 | Date |
|---|---|---|
|
|
3c7cfcd149 |
|
|
@ -1,436 +0,0 @@
|
|||
---
|
||||
name: ai-sdk-development
|
||||
description: TRIGGER when working with ai-sdk which is Laravel official first-party AI SDK. Activate when building, editing AI agents, chatbots, text generation, image generation, audio/TTS, transcription/STT, embeddings, RAG, vector stores, reranking, structured output, streaming, conversation memory, tools, queueing, broadcasting, and provider failover across OpenAI, Anthropic, Gemini, Azure, Groq, xAI, DeepSeek, Mistral, Ollama, ElevenLabs, Cohere, Jina, and VoyageAI. Invoke when the user references ai-sdk, the `Laravel\Ai\` namespace, or this project's AI features — not for other AI packages used directly.
|
||||
license: MIT
|
||||
metadata:
|
||||
author: laravel
|
||||
---
|
||||
|
||||
# Developing with the Laravel AI SDK
|
||||
|
||||
The Laravel AI SDK (`laravel/ai`) is the official AI package for Laravel, providing a unified API for agents, images, audio, transcription, embeddings, reranking, vector stores, and file management across multiple AI providers.
|
||||
|
||||
## Searching the Documentation
|
||||
|
||||
This package is new. Always search the documentation before implementing any feature. Never guess at APIs — the documentation is the single source of truth.
|
||||
|
||||
- Use broad, simple queries that match the documentation section headings below.
|
||||
- Do not add package names to queries — package information is shared automatically. Use `test agent fake`, not `laravel ai test agent fake`.
|
||||
- Run multiple queries at once — the most relevant results are returned first.
|
||||
|
||||
### Documentation Sections
|
||||
|
||||
Use these section headings as query terms for accurate results:
|
||||
|
||||
- Introduction, Installation, Configuration, Provider Support
|
||||
- Agents: Prompting, Conversation Context, Structured Output, Attachments, Streaming, Broadcasting, Queueing, Tools, Provider Tools, Middleware, Anonymous Agents, Agent Configuration
|
||||
- Images
|
||||
- Audio (TTS)
|
||||
- Transcription (STT)
|
||||
- Embeddings: Querying Embeddings, Caching Embeddings
|
||||
- Reranking
|
||||
- Files
|
||||
- Vector Stores: Adding Files to Stores
|
||||
- Failover
|
||||
- Testing: Agents, Images, Audio, Transcriptions, Embeddings, Reranking, Files, Vector Stores
|
||||
- Events
|
||||
|
||||
## Decision Workflow
|
||||
|
||||
Determine the right entry point before writing code:
|
||||
|
||||
Text generation or chat? → Agent class with `Promptable` trait
|
||||
Chat with conversation history? → Agent + `Conversational` interface (manual) or `RemembersConversations` trait (automatic)
|
||||
Structured JSON output? → Agent + `HasStructuredOutput` interface
|
||||
Image generation? → `Image::of()->generate()`
|
||||
Audio synthesis? → `Audio::of()->generate()`
|
||||
Transcription? → `Transcription::fromPath()->generate()`
|
||||
Embeddings? → `Embeddings::for()->generate()`
|
||||
Reranking? → `Reranking::of()->rerank()`
|
||||
File storage? → `Document::fromPath()->put()`
|
||||
Vector stores? → `Stores::create()`
|
||||
|
||||
## Basic Usage Examples
|
||||
|
||||
### Agents
|
||||
|
||||
```php
|
||||
use Laravel\Ai\Contracts\Agent;
|
||||
use Laravel\Ai\Enums\Lab;
|
||||
use Laravel\Ai\Promptable;
|
||||
|
||||
class SalesCoach implements Agent
|
||||
{
|
||||
use Promptable;
|
||||
|
||||
public function instructions(): string
|
||||
{
|
||||
return 'You are a sales coach.';
|
||||
}
|
||||
}
|
||||
|
||||
// Prompting
|
||||
$response = (new SalesCoach)->prompt('Analyze this transcript...');
|
||||
echo $response->text;
|
||||
|
||||
// Container resolution with dependency injection
|
||||
$agent = SalesCoach::make(user: $user);
|
||||
|
||||
// Override provider, model, or timeout per-prompt
|
||||
$response = (new SalesCoach)->prompt(
|
||||
'Analyze this transcript...',
|
||||
provider: Lab::Anthropic,
|
||||
model: 'claude-haiku-4-5-20251001',
|
||||
timeout: 120,
|
||||
);
|
||||
|
||||
// Streaming (returns SSE response from a route)
|
||||
return (new SalesCoach)->stream('Analyze this transcript...');
|
||||
|
||||
// Queueing
|
||||
(new SalesCoach)->queue('Analyze this transcript...')
|
||||
->then(fn ($response) => /* ... */);
|
||||
|
||||
// Anonymous agents
|
||||
use function Laravel\Ai\{agent};
|
||||
|
||||
$response = agent(instructions: 'You are a helpful assistant.')->prompt('Hello');
|
||||
```
|
||||
|
||||
### Conversation Context
|
||||
|
||||
Manual conversation history via the `Conversational` interface:
|
||||
|
||||
```php
|
||||
use Laravel\Ai\Contracts\Agent;
|
||||
use Laravel\Ai\Contracts\Conversational;
|
||||
use Laravel\Ai\Messages\Message;
|
||||
use Laravel\Ai\Promptable;
|
||||
|
||||
class SalesCoach implements Agent, Conversational
|
||||
{
|
||||
use Promptable;
|
||||
|
||||
public function __construct(public User $user) {}
|
||||
|
||||
public function instructions(): string { return 'You are a sales coach.'; }
|
||||
|
||||
public function messages(): iterable
|
||||
{
|
||||
return History::where('user_id', $this->user->id)
|
||||
->latest()->limit(50)->get()->reverse()
|
||||
->map(fn ($m) => new Message($m->role, $m->content))
|
||||
->all();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Automatic conversation persistence via the `RemembersConversations` trait:
|
||||
|
||||
```php
|
||||
use Laravel\Ai\Concerns\RemembersConversations;
|
||||
use Laravel\Ai\Contracts\Agent;
|
||||
use Laravel\Ai\Contracts\Conversational;
|
||||
use Laravel\Ai\Promptable;
|
||||
|
||||
class SalesCoach implements Agent, Conversational
|
||||
{
|
||||
use Promptable, RemembersConversations;
|
||||
|
||||
public function instructions(): string { return 'You are a sales coach.'; }
|
||||
}
|
||||
|
||||
// Start a new conversation
|
||||
$response = (new SalesCoach)->forUser($user)->prompt('Hello!');
|
||||
$conversationId = $response->conversationId;
|
||||
|
||||
// Continue an existing conversation
|
||||
$response = (new SalesCoach)->continue($conversationId, as: $user)->prompt('Tell me more.');
|
||||
```
|
||||
|
||||
### Structured Output
|
||||
|
||||
```php
|
||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||
use Laravel\Ai\Contracts\Agent;
|
||||
use Laravel\Ai\Contracts\HasStructuredOutput;
|
||||
use Laravel\Ai\Promptable;
|
||||
|
||||
class Reviewer implements Agent, HasStructuredOutput
|
||||
{
|
||||
use Promptable;
|
||||
|
||||
public function instructions(): string { return 'Review and score content.'; }
|
||||
|
||||
public function schema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'feedback' => $schema->string()->required(),
|
||||
'score' => $schema->integer()->min(1)->max(10)->required(),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
$response = (new Reviewer)->prompt('Review this...');
|
||||
echo $response['score']; // Access like an array
|
||||
```
|
||||
|
||||
### Images
|
||||
|
||||
```php
|
||||
use Laravel\Ai\Image;
|
||||
|
||||
$image = Image::of('A sunset over mountains')
|
||||
->landscape()
|
||||
->quality('high')
|
||||
->generate();
|
||||
|
||||
$path = $image->store(); // Store to default disk
|
||||
```
|
||||
|
||||
### Audio
|
||||
|
||||
```php
|
||||
use Laravel\Ai\Audio;
|
||||
|
||||
$audio = Audio::of('Hello from Laravel.')
|
||||
->female()
|
||||
->instructions('Speak warmly')
|
||||
->generate();
|
||||
|
||||
$path = $audio->store();
|
||||
```
|
||||
|
||||
### Transcription
|
||||
|
||||
```php
|
||||
use Laravel\Ai\Transcription;
|
||||
|
||||
$transcript = Transcription::fromStorage('audio.mp3')
|
||||
->diarize()
|
||||
->generate();
|
||||
|
||||
echo (string) $transcript;
|
||||
```
|
||||
|
||||
### Embeddings
|
||||
|
||||
```php
|
||||
use Laravel\Ai\Embeddings;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
$response = Embeddings::for(['Text one', 'Text two'])
|
||||
->dimensions(1536)
|
||||
->cache()
|
||||
->generate();
|
||||
|
||||
// Single string via Stringable
|
||||
$embedding = Str::of('Napa Valley has great wine.')->toEmbeddings();
|
||||
```
|
||||
|
||||
### Reranking
|
||||
|
||||
```php
|
||||
use Laravel\Ai\Reranking;
|
||||
|
||||
$response = Reranking::of(['Django is Python.', 'Laravel is PHP.', 'React is JS.'])
|
||||
->limit(5)
|
||||
->rerank('PHP frameworks');
|
||||
|
||||
$response->first()->document; // "Laravel is PHP."
|
||||
```
|
||||
|
||||
### Files and Vector Stores
|
||||
|
||||
```php
|
||||
use Laravel\Ai\Files\Document;
|
||||
use Laravel\Ai\Stores;
|
||||
|
||||
// Store a file with the provider
|
||||
$file = Document::fromPath('/path/to/doc.pdf')->put();
|
||||
|
||||
// Create a vector store and add files
|
||||
$store = Stores::create('Knowledge Base');
|
||||
$store->add($file->id);
|
||||
$store->add(Document::fromStorage('manual.pdf')); // Store + add in one step
|
||||
```
|
||||
|
||||
## Agent Configuration
|
||||
|
||||
### PHP Attributes
|
||||
|
||||
```php
|
||||
use Laravel\Ai\Attributes\{Provider, Model, MaxSteps, MaxTokens, Temperature, Timeout};
|
||||
use Laravel\Ai\Enums\Lab;
|
||||
|
||||
#[Provider(Lab::Anthropic)]
|
||||
#[Model('claude-haiku-4-5-20251001')]
|
||||
#[MaxSteps(10)]
|
||||
#[MaxTokens(4096)]
|
||||
#[Temperature(0.7)]
|
||||
#[Timeout(120)]
|
||||
class MyAgent implements Agent
|
||||
{
|
||||
use Promptable;
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
The `#[UseCheapestModel]` and `#[UseSmartestModel]` attributes are also available for automatic model selection.
|
||||
|
||||
### Tools
|
||||
|
||||
Implement the `HasTools` interface and scaffold tools with `php artisan make:tool`:
|
||||
|
||||
```php
|
||||
use Laravel\Ai\Contracts\HasTools;
|
||||
|
||||
class MyAgent implements Agent, HasTools
|
||||
{
|
||||
use Promptable;
|
||||
|
||||
public function tools(): iterable
|
||||
{
|
||||
return [new MyCustomTool];
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Provider Tools
|
||||
|
||||
```php
|
||||
use Laravel\Ai\Providers\Tools\{WebSearch, WebFetch, FileSearch};
|
||||
|
||||
public function tools(): iterable
|
||||
{
|
||||
return [
|
||||
(new WebSearch)->max(5)->allow(['laravel.com']),
|
||||
new WebFetch,
|
||||
new FileSearch(stores: ['store_id']),
|
||||
];
|
||||
}
|
||||
```
|
||||
|
||||
### Conversation Memory
|
||||
|
||||
```php
|
||||
use Laravel\Ai\Concerns\RemembersConversations;
|
||||
use Laravel\Ai\Contracts\Conversational;
|
||||
|
||||
class ChatBot implements Agent, Conversational
|
||||
{
|
||||
use Promptable, RemembersConversations;
|
||||
// ...
|
||||
}
|
||||
|
||||
$response = (new ChatBot)->forUser($user)->prompt('Hello!');
|
||||
$response = (new ChatBot)->continue($conversationId, as: $user)->prompt('More...');
|
||||
```
|
||||
|
||||
### Failover
|
||||
|
||||
```php
|
||||
$response = (new MyAgent)->prompt('Hello', provider: [Lab::OpenAI, Lab::Anthropic]);
|
||||
```
|
||||
|
||||
## Testing and Faking
|
||||
|
||||
Each capability supports `fake()` with assertions:
|
||||
|
||||
```php
|
||||
use App\Ai\Agents\SalesCoach;
|
||||
use Laravel\Ai\{Image, Audio, Transcription, Embeddings, Reranking, Files, Stores};
|
||||
|
||||
// Agents
|
||||
SalesCoach::fake(['Response 1', 'Response 2']);
|
||||
SalesCoach::assertPrompted('query');
|
||||
SalesCoach::assertNotPrompted('query');
|
||||
SalesCoach::assertNeverPrompted();
|
||||
SalesCoach::fake()->preventStrayPrompts();
|
||||
|
||||
// Images
|
||||
Image::fake();
|
||||
Image::assertGenerated(fn ($prompt) => $prompt->contains('sunset'));
|
||||
Image::assertNothingGenerated();
|
||||
|
||||
// Audio
|
||||
Audio::fake();
|
||||
Audio::assertGenerated(fn ($prompt) => $prompt->contains('Hello'));
|
||||
|
||||
// Transcription
|
||||
Transcription::fake(['Transcribed text.']);
|
||||
Transcription::assertGenerated(fn ($prompt) => $prompt->isDiarized());
|
||||
|
||||
// Embeddings
|
||||
Embeddings::fake();
|
||||
Embeddings::assertGenerated(fn ($prompt) => $prompt->contains('Laravel'));
|
||||
|
||||
// Reranking
|
||||
Reranking::fake();
|
||||
Reranking::assertReranked(fn ($prompt) => $prompt->contains('PHP'));
|
||||
|
||||
// Files
|
||||
Files::fake();
|
||||
Files::assertStored(fn ($file) => $file->mimeType() === 'text/plain');
|
||||
|
||||
// Stores
|
||||
Stores::fake();
|
||||
Stores::assertCreated('Knowledge Base');
|
||||
$store = Stores::get('id');
|
||||
$store->assertAdded('file_id');
|
||||
```
|
||||
|
||||
## Key Patterns
|
||||
|
||||
- Namespace: `Laravel\Ai\`
|
||||
- Package: `composer require laravel/ai`
|
||||
- Agent pattern: Implement the `Agent` interface and use the `Promptable` trait
|
||||
- Optional interfaces: `HasTools`, `HasMiddleware`, `HasStructuredOutput`, `Conversational`
|
||||
- Entry-point classes: `Image`, `Audio`, `Transcription`, `Embeddings`, `Reranking`, `Stores`
|
||||
- Provider enum: `Laravel\Ai\Enums\Lab` (prefer over plain strings)
|
||||
- Artisan commands: `php artisan make:agent`, `php artisan make:tool`
|
||||
- Global helper: `agent()` for anonymous agents
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
### Wrong Namespace
|
||||
|
||||
The namespace is `Laravel\Ai`, not `Illuminate\Ai` or `Laravel\AI`.
|
||||
|
||||
```php
|
||||
// Correct
|
||||
use Laravel\Ai\Image;
|
||||
use Laravel\Ai\Contracts\Agent;
|
||||
use Laravel\Ai\Promptable;
|
||||
|
||||
// Wrong — these do not exist
|
||||
use Illuminate\Ai\Image;
|
||||
use Laravel\AI\Agent;
|
||||
```
|
||||
|
||||
### Unsupported Provider Capability
|
||||
|
||||
Calling a capability not supported by a provider throws a `LogicException`. Refer to the provider support table below.
|
||||
|
||||
## Provider Support
|
||||
|
||||
| Feature | Providers |
|
||||
| ---------- | --------------------------------------------------------------- |
|
||||
| Text | OpenAI, Anthropic, Gemini, Azure, Groq, xAI, DeepSeek, Mistral, Ollama |
|
||||
| Images | OpenAI, Gemini, xAI |
|
||||
| TTS | OpenAI, ElevenLabs |
|
||||
| STT | OpenAI, ElevenLabs, Mistral |
|
||||
| Embeddings | OpenAI, Gemini, Azure, Cohere, Mistral, Jina, VoyageAI |
|
||||
| Reranking | Cohere, Jina |
|
||||
| Files | OpenAI, Anthropic, Gemini |
|
||||
|
||||
Use the `Laravel\Ai\Enums\Lab` enum to reference providers in code instead of plain strings:
|
||||
|
||||
```php
|
||||
use Laravel\Ai\Enums\Lab;
|
||||
|
||||
Lab::Anthropic;
|
||||
Lab::OpenAI;
|
||||
Lab::Gemini;
|
||||
// ...
|
||||
```
|
||||
|
|
@ -1,108 +0,0 @@
|
|||
---
|
||||
name: cashier-stripe-development
|
||||
description: "Handles Laravel Cashier Stripe integration including subscriptions, webhooks, Stripe Checkout, invoices, charges, refunds, trials, coupons, metered billing, and payment failure handling. Triggered when a user mentions Cashier, Billable, IncompletePayment, stripe_id, newSubscription, Stripe subscriptions, or billing. Also applies when setting up webhooks, handling SCA/3DS payment failures, testing with Stripe test cards, or troubleshooting incomplete subscriptions, CSRF webhook errors, or migration publish issues."
|
||||
license: MIT
|
||||
metadata:
|
||||
author: laravel
|
||||
---
|
||||
|
||||
# Cashier Stripe Development
|
||||
|
||||
## When to Apply
|
||||
|
||||
Activate this skill when:
|
||||
|
||||
- Installing or configuring Laravel Cashier Stripe
|
||||
- Setting up subscriptions, trials, quantities, or plan swapping
|
||||
- Handling webhooks or SCA/3DS payment failures
|
||||
- Working with Stripe Checkout, invoices, or charges
|
||||
- Testing billing scenarios with Stripe test cards or tokens
|
||||
|
||||
## Documentation
|
||||
|
||||
Use `search-docs` for detailed Cashier patterns and documentation covering subscriptions, webhooks, Stripe Checkout, invoices, payment methods, and testing.
|
||||
|
||||
For deeper guidance on specific topics, read the relevant reference file before implementing:
|
||||
|
||||
- `references/subscriptions.md` covers subscription creation, status checks, swapping, trials, quantities, and multiple products
|
||||
- `references/webhooks.md` covers webhook setup, custom handlers, CSRF exclusion, and local development with the Stripe CLI
|
||||
- `references/testing.md` covers Stripe test cards, payment method tokens, and feature test patterns
|
||||
|
||||
## Basic Usage
|
||||
|
||||
### Installation
|
||||
|
||||
```bash
|
||||
php artisan vendor:publish --tag="cashier-migrations"
|
||||
php artisan migrate
|
||||
php artisan vendor:publish --tag="cashier-config"
|
||||
```
|
||||
|
||||
### Environment Variables
|
||||
|
||||
```
|
||||
STRIPE_KEY=pk_test_...
|
||||
STRIPE_SECRET=sk_test_...
|
||||
STRIPE_WEBHOOK_SECRET=whsec_...
|
||||
CASHIER_CURRENCY=usd
|
||||
CASHIER_CURRENCY_LOCALE=en_US
|
||||
```
|
||||
|
||||
### Billable Model
|
||||
|
||||
<!-- Add Billable Trait -->
|
||||
```php
|
||||
use Laravel\Cashier\Billable;
|
||||
|
||||
class User extends Authenticatable
|
||||
{
|
||||
use Billable;
|
||||
}
|
||||
```
|
||||
|
||||
For a non-User model, register it in a service provider:
|
||||
|
||||
<!-- Custom Billable Model -->
|
||||
```php
|
||||
// In AppServiceProvider::boot()
|
||||
Cashier::useCustomerModel(Team::class);
|
||||
```
|
||||
|
||||
### Creating a Subscription
|
||||
|
||||
<!-- Create Subscription -->
|
||||
```php
|
||||
use Laravel\Cashier\Exceptions\IncompletePayment;
|
||||
|
||||
try {
|
||||
$user->newSubscription('default', 'price_xxxx')->create($paymentMethodId);
|
||||
} catch (IncompletePayment $e) {
|
||||
return redirect()->route('cashier.payment', [$e->payment->id, 'redirect' => route('home')]);
|
||||
}
|
||||
```
|
||||
|
||||
Always wrap subscription creation in a try/catch for `IncompletePayment`. When a card requires 3DS authentication, Cashier throws this exception. The `cashier.payment` route is auto-registered and handles the confirmation flow.
|
||||
|
||||
## Verification
|
||||
|
||||
1. Run migrations and confirm `stripe_id`, `pm_type`, `pm_last_four`, and `trial_ends_at` columns exist on the billable model table
|
||||
2. Test the webhook endpoint with `stripe listen --forward-to localhost/stripe/webhook` if you use the default path, or swap `stripe` for your configured `CASHIER_PATH`
|
||||
3. Confirm `$user->subscribed('default')` returns the expected value for active and incomplete subscriptions
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
- The migration publish tag is `cashier-migrations`, not `cashier`. Running `migrate` before publishing results in missing columns and tables.
|
||||
- `CASHIER_CURRENCY` must be set explicitly. It defaults to USD, which silently breaks non-US apps.
|
||||
- The Stripe CLI generates its own webhook signing secret. It is different from the Dashboard endpoint secret. Using the wrong one causes signature verification failures.
|
||||
- The webhook route must be excluded from CSRF verification using your configured `cashier.path`. If you change `CASHIER_PATH` from `stripe` to `billing`, exclude `billing/*`, not `stripe/*`.
|
||||
- `canceled()` returns true as soon as `cancel()` is called, but the user still has access during the grace period. Use `ended()` to confirm access is fully revoked.
|
||||
- `subscribed()` returns true during the grace period even though the subscription is canceled.
|
||||
- `subscribed()` returns false for `incomplete` and `past_due` subscriptions by default.
|
||||
- Prices cannot be swapped and quantity cannot be updated while a subscription has an incomplete payment.
|
||||
- When extending `WebhookController`, call `Cashier::ignoreRoutes()` in a service provider and re-register both `cashier.payment` and `cashier.webhook` under the configured `cashier.path`.
|
||||
- Use `Cashier::useCustomerModel()` in a service provider to set a custom billable model. There is no `CASHIER_MODEL` env var.
|
||||
- `trial_ends_at` is a local database column synced via webhooks. It will be stale if webhooks are not configured in production.
|
||||
- In MySQL, the `stripe_id` column must use `utf8_bin` collation to avoid case-sensitivity issues.
|
||||
- `noProrate()` has no effect when combined with `swapAndInvoice()`. That method always prorates.
|
||||
- Methods like `withPromotionCode()` require the Stripe API ID such as `promo_xxxx`, not the customer-facing code. Use `findPromotionCode()` to resolve a code to its ID.
|
||||
- Always use `search-docs` for the latest Cashier documentation rather than relying on this skill alone.
|
||||
|
|
@ -1,108 +0,0 @@
|
|||
# Subscriptions Reference
|
||||
|
||||
Use `search-docs` for authoritative documentation on subscriptions.
|
||||
|
||||
## Status Checks
|
||||
|
||||
| Method | Returns true when |
|
||||
|---|---|
|
||||
| `$user->subscribed('default')` | Active or on grace period |
|
||||
| `->onTrial()` | Trial period active |
|
||||
| `->onGracePeriod()` | Canceled, period not yet ended |
|
||||
| `->canceled()` | `ends_at` is set, may still have access |
|
||||
| `->ended()` | Canceled and grace period expired |
|
||||
| `->incomplete()` | Awaiting SCA/3DS confirmation |
|
||||
| `->pastDue()` | Payment overdue |
|
||||
| `->recurring()` | Active and not on trial |
|
||||
|
||||
Check by product or price:
|
||||
|
||||
```php
|
||||
$user->subscribedToProduct('prod_premium', 'default');
|
||||
$user->subscribedToPrice('price_monthly', 'default');
|
||||
```
|
||||
|
||||
## Swapping Plans
|
||||
|
||||
```php
|
||||
$user->subscription('default')->swap('price_new');
|
||||
$user->subscription('default')->noProrate()->swap('price_new');
|
||||
$user->subscription('default')->swapAndInvoice('price_new');
|
||||
$user->subscription('default')->skipTrial()->swap('price_new');
|
||||
```
|
||||
|
||||
## Quantity
|
||||
|
||||
```php
|
||||
$user->subscription('default')->incrementQuantity();
|
||||
$user->subscription('default')->decrementQuantity();
|
||||
$user->subscription('default')->updateQuantity(10);
|
||||
$user->subscription('default')->noProrate()->updateQuantity(10);
|
||||
```
|
||||
|
||||
## Trials
|
||||
|
||||
```php
|
||||
$user->newSubscription('default', 'price_xxxx')
|
||||
->trialDays(14)
|
||||
->create($paymentMethodId);
|
||||
|
||||
$subscription->extendTrial(now()->addDays(7));
|
||||
```
|
||||
|
||||
## Multiple Products on One Subscription
|
||||
|
||||
```php
|
||||
$user->newSubscription('default', ['price_monthly', 'price_chat'])
|
||||
->quantity(5, 'price_chat')
|
||||
->create($paymentMethod);
|
||||
|
||||
$user->subscription('default')->addPrice('price_chat');
|
||||
$user->subscription('default')->removePrice('price_chat');
|
||||
$user->subscription('default')->swap(['price_pro', 'price_chat']);
|
||||
```
|
||||
|
||||
## Multiple Subscriptions
|
||||
|
||||
```php
|
||||
$user->newSubscription('swimming', 'price_swimming_monthly')->create($pm);
|
||||
$user->newSubscription('gym', 'price_gym_monthly')->create($pm);
|
||||
|
||||
$user->subscription('swimming')->swap('price_swimming_yearly');
|
||||
$user->subscription('gym')->cancel();
|
||||
```
|
||||
|
||||
## Cancellation and Resumption
|
||||
|
||||
```php
|
||||
$user->subscription('default')->cancel(); // At end of billing period
|
||||
$user->subscription('default')->cancelNow(); // Immediately
|
||||
$user->subscription('default')->resume(); // During grace period only
|
||||
```
|
||||
|
||||
## Incomplete Payment Handling
|
||||
|
||||
```php
|
||||
if ($user->hasIncompletePayment('default')) {
|
||||
$paymentId = $user->subscription('default')->latestPayment()->id;
|
||||
return redirect()->route('cashier.payment', $paymentId);
|
||||
}
|
||||
```
|
||||
|
||||
Opt out of default deactivation behavior:
|
||||
|
||||
```php
|
||||
Cashier::keepPastDueSubscriptionsActive();
|
||||
Cashier::keepIncompleteSubscriptionsActive();
|
||||
```
|
||||
|
||||
## Metered / Usage-Based Billing
|
||||
|
||||
```php
|
||||
$user->newSubscription('default')
|
||||
->meteredPrice('price_metered')
|
||||
->create($paymentMethodId);
|
||||
|
||||
$user->reportMeterEvent('emails-sent');
|
||||
$user->reportMeterEvent('emails-sent', quantity: 15);
|
||||
```
|
||||
|
|
@ -1,52 +0,0 @@
|
|||
# Testing Reference
|
||||
|
||||
Use `search-docs` for authoritative documentation on testing Cashier integrations.
|
||||
|
||||
## Test Cards and Tokens
|
||||
|
||||
Use card numbers for browser-based flows (Stripe.js / Checkout). Use `pm_card_*` tokens directly in feature tests that call the Stripe API.
|
||||
|
||||
| Card Number | Token | Behavior |
|
||||
|---|---|---|
|
||||
| `4242 4242 4242 4242` | `pm_card_visa` | Succeeds immediately |
|
||||
| `4000 0025 0000 3155` | `pm_card_threeDSecure2Required` | Requires SCA/3DS |
|
||||
| `4000 0027 6000 3184` | `pm_card_authenticationRequired` | Requires authentication |
|
||||
| `4000 0000 0000 9995` | `pm_card_chargeDeclinedInsufficientFunds` | Declined, insufficient funds |
|
||||
| `4000 0000 0000 0002` | `pm_card_chargeDeclined` | Declined |
|
||||
|
||||
Use expiry `12/34`, any CVC, any ZIP for card number inputs.
|
||||
|
||||
## Feature Test Example
|
||||
|
||||
Feature tests that hit the real Stripe test API use `pm_card_*` tokens:
|
||||
|
||||
```php
|
||||
public function test_user_can_subscribe(): void
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
|
||||
$user->newSubscription('default', 'price_xxxx')
|
||||
->create('pm_card_visa');
|
||||
|
||||
$this->assertTrue($user->subscribed('default'));
|
||||
}
|
||||
|
||||
public function test_incomplete_payment_is_handled(): void
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
|
||||
try {
|
||||
$user->newSubscription('default', 'price_xxxx')
|
||||
->create('pm_card_threeDSecure2Required');
|
||||
} catch (\Laravel\Cashier\Exceptions\IncompletePayment $e) {
|
||||
$this->assertTrue($user->subscription('default')->incomplete());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Setup Notes
|
||||
|
||||
- Use Stripe test mode keys (`sk_test_...`, `pk_test_...`) in your test environment
|
||||
- Cashier does not ship a global `fake()` helper. Tests hit the real Stripe test API by default.
|
||||
- Refer to `tests/Feature/` in the Cashier package itself for integration test patterns covering subscription creation, payment methods, and webhook handling
|
||||
- Use `search-docs` for current guidance on mocking Stripe HTTP calls or using Stripe's test clock feature for time-sensitive scenarios
|
||||
|
|
@ -1,132 +0,0 @@
|
|||
# Webhooks Reference
|
||||
|
||||
Use `search-docs` for authoritative documentation on webhooks.
|
||||
|
||||
## Auto-Registered Routes
|
||||
|
||||
Cashier registers two routes automatically under the `cashier.path` prefix (`config('cashier.path')`, default `stripe`):
|
||||
|
||||
- `POST /{cashier.path}/webhook` named `cashier.webhook`
|
||||
- `GET /{cashier.path}/payment/{id}` named `cashier.payment`
|
||||
|
||||
With the default config these are `/stripe/webhook` and `/stripe/payment/{id}`. If you set `CASHIER_PATH=billing`, they become `/billing/webhook` and `/billing/payment/{id}`.
|
||||
|
||||
## CSRF Exclusion
|
||||
|
||||
Use the same path prefix you configured for Cashier here. If `CASHIER_PATH=billing`, exclude `billing/*` instead of `stripe/*`.
|
||||
|
||||
**Laravel 11+ (`bootstrap/app.php`, default path example):**
|
||||
|
||||
```php
|
||||
->withMiddleware(function (Middleware $middleware) {
|
||||
$middleware->validateCsrfTokens(except: ['stripe/*']);
|
||||
})
|
||||
```
|
||||
|
||||
**Laravel 10 (`app/Http/Middleware/VerifyCsrfToken.php`, default path example):**
|
||||
|
||||
```php
|
||||
protected $except = [
|
||||
'stripe/*',
|
||||
];
|
||||
```
|
||||
|
||||
## Local Development with Stripe CLI
|
||||
|
||||
If you changed `cashier.path`, forward Stripe CLI events to that URL instead of `/stripe/webhook`.
|
||||
|
||||
```bash
|
||||
stripe login
|
||||
stripe listen --forward-to your-app.test/stripe/webhook
|
||||
stripe trigger invoice.payment_succeeded
|
||||
```
|
||||
|
||||
The CLI outputs a `whsec_...` signing secret specific to that session. Set it as `STRIPE_WEBHOOK_SECRET` locally. It is not the same as the Dashboard endpoint secret.
|
||||
|
||||
## Registering Events in the Stripe Dashboard
|
||||
|
||||
Use the Artisan command to create the endpoint automatically with all required events:
|
||||
|
||||
```bash
|
||||
php artisan cashier:webhook
|
||||
```
|
||||
|
||||
Cashier's `cashier:webhook` command registers these events by default:
|
||||
|
||||
- `customer.subscription.created`
|
||||
- `customer.subscription.updated`
|
||||
- `customer.subscription.deleted`
|
||||
- `customer.updated` / `customer.deleted`
|
||||
- `invoice.payment_action_required`
|
||||
- `invoice.payment_succeeded`
|
||||
- `payment_method.automatically_updated`
|
||||
|
||||
Cashier's `WebhookController` has built-in handlers for all of the above except `invoice.payment_succeeded`. For renewal hooks, prefer `WebhookReceived` / `WebhookHandled` listeners unless you intentionally add your own controller method.
|
||||
|
||||
## Custom Handlers: Extending WebhookController
|
||||
|
||||
Method name pattern: `handle` + StudlyCase of event type with dots replaced by underscores.
|
||||
|
||||
`customer.subscription.created` becomes `handleCustomerSubscriptionCreated`.
|
||||
|
||||
```php
|
||||
use Laravel\Cashier\Http\Controllers\WebhookController as CashierController;
|
||||
|
||||
class StripeWebhookController extends CashierController
|
||||
{
|
||||
public function handleCustomerSubscriptionCreated(array $payload)
|
||||
{
|
||||
$response = parent::handleCustomerSubscriptionCreated($payload);
|
||||
|
||||
// your logic after Cashier syncs the subscription
|
||||
|
||||
return $response;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
If you add a method for an event Cashier does not handle internally, such as `invoice.payment_succeeded`, do not call `parent::handle...()` unless the base controller actually defines that method.
|
||||
|
||||
In a service provider, disable auto-registration and re-register both Cashier routes so the incomplete-payment flow and `cashier:webhook` command keep working:
|
||||
|
||||
```php
|
||||
Cashier::ignoreRoutes();
|
||||
```
|
||||
|
||||
```php
|
||||
// routes/web.php
|
||||
use App\Http\Controllers\StripeWebhookController;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use Laravel\Cashier\Http\Controllers\PaymentController;
|
||||
|
||||
Route::prefix(config('cashier.path'))
|
||||
->name('cashier.')
|
||||
->group(function () {
|
||||
Route::get('payment/{id}', [PaymentController::class, 'show'])->name('payment');
|
||||
Route::post('webhook', [StripeWebhookController::class, 'handleWebhook'])->name('webhook');
|
||||
});
|
||||
```
|
||||
|
||||
Keep the `cashier.webhook` route name unless you plan to pass `--url` explicitly to `php artisan cashier:webhook`.
|
||||
|
||||
## Custom Handlers: Listening to Events
|
||||
|
||||
The simpler option when you do not need to replace Cashier's internal logic, or when you want to react to events such as `invoice.payment_succeeded` that Cashier does not process itself:
|
||||
|
||||
```php
|
||||
use Laravel\Cashier\Events\WebhookReceived;
|
||||
use Laravel\Cashier\Events\WebhookHandled;
|
||||
|
||||
// WebhookReceived fires for every event before Cashier processes it
|
||||
// WebhookHandled fires after Cashier processes it
|
||||
|
||||
Event::listen(WebhookReceived::class, function (WebhookReceived $event) {
|
||||
if ($event->payload['type'] === 'invoice.payment_succeeded') {
|
||||
// handle renewal
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
## Signature Verification
|
||||
|
||||
`VerifyWebhookSignature` middleware is applied automatically when `cashier.webhook.secret` is set. No extra wiring is needed.
|
||||
|
|
@ -1,128 +0,0 @@
|
|||
---
|
||||
name: fortify-development
|
||||
description: Laravel Fortify headless authentication backend development. Activate when implementing authentication features including login, registration, password reset, email verification, two-factor authentication (2FA/TOTP), profile updates, headless auth, authentication scaffolding, or auth guards in Laravel applications.
|
||||
---
|
||||
|
||||
# Laravel Fortify Development
|
||||
|
||||
Fortify is a headless authentication backend that provides authentication routes and controllers for Laravel applications.
|
||||
|
||||
## Documentation
|
||||
|
||||
Use `search-docs` for detailed Laravel Fortify patterns and documentation.
|
||||
|
||||
## Usage
|
||||
|
||||
- **Routes**: Use `list-routes` with `only_vendor: true` and `action: "Fortify"` to see all registered endpoints
|
||||
- **Actions**: Check `app/Actions/Fortify/` for customizable business logic (user creation, password validation, etc.)
|
||||
- **Config**: See `config/fortify.php` for all options including features, guards, rate limiters, and username field
|
||||
- **Contracts**: Look in `Laravel\Fortify\Contracts\` for overridable response classes (`LoginResponse`, `LogoutResponse`, etc.)
|
||||
- **Views**: All view callbacks are set in `FortifyServiceProvider::boot()` using `Fortify::loginView()`, `Fortify::registerView()`, etc.
|
||||
|
||||
## Available Features
|
||||
|
||||
Enable in `config/fortify.php` features array:
|
||||
|
||||
- `Features::registration()` - User registration
|
||||
- `Features::resetPasswords()` - Password reset via email
|
||||
- `Features::emailVerification()` - Requires User to implement `MustVerifyEmail`
|
||||
- `Features::updateProfileInformation()` - Profile updates
|
||||
- `Features::updatePasswords()` - Password changes
|
||||
- `Features::twoFactorAuthentication()` - 2FA with QR codes and recovery codes
|
||||
|
||||
> Use `search-docs` for feature configuration options and customization patterns.
|
||||
|
||||
## Setup Workflows
|
||||
|
||||
### Two-Factor Authentication Setup
|
||||
|
||||
```
|
||||
- [ ] Add TwoFactorAuthenticatable trait to User model
|
||||
- [ ] Enable feature in config/fortify.php
|
||||
- [ ] If the `*_add_two_factor_columns_to_users_table.php` migration is missing, publish via `php artisan vendor:publish --tag=fortify-migrations` and migrate
|
||||
- [ ] Set up view callbacks in FortifyServiceProvider
|
||||
- [ ] Create 2FA management UI
|
||||
- [ ] Test QR code and recovery codes
|
||||
```
|
||||
|
||||
> Use `search-docs` for TOTP implementation and recovery code handling patterns.
|
||||
|
||||
### Email Verification Setup
|
||||
|
||||
```
|
||||
- [ ] Enable emailVerification feature in config
|
||||
- [ ] Implement MustVerifyEmail interface on User model
|
||||
- [ ] Set up verifyEmailView callback
|
||||
- [ ] Add verified middleware to protected routes
|
||||
- [ ] Test verification email flow
|
||||
```
|
||||
|
||||
> Use `search-docs` for MustVerifyEmail implementation patterns.
|
||||
|
||||
### Password Reset Setup
|
||||
|
||||
```
|
||||
- [ ] Enable resetPasswords feature in config
|
||||
- [ ] Set up requestPasswordResetLinkView callback
|
||||
- [ ] Set up resetPasswordView callback
|
||||
- [ ] Define password.reset named route (if views disabled)
|
||||
- [ ] Test reset email and link flow
|
||||
```
|
||||
|
||||
> Use `search-docs` for custom password reset flow patterns.
|
||||
|
||||
### SPA Authentication Setup
|
||||
|
||||
```
|
||||
- [ ] Set 'views' => false in config/fortify.php
|
||||
- [ ] Install and configure Laravel Sanctum for session-based SPA authentication
|
||||
- [ ] Use the 'web' guard in config/fortify.php (required for session-based authentication)
|
||||
- [ ] Set up CSRF token handling
|
||||
- [ ] Test XHR authentication flows
|
||||
```
|
||||
|
||||
> Use `search-docs` for integration and SPA authentication patterns.
|
||||
|
||||
#### Two-Factor Authentication in SPA Mode
|
||||
|
||||
When `views` is set to `false`, Fortify returns JSON responses instead of redirects.
|
||||
|
||||
If a user attempts to log in and two-factor authentication is enabled, the login request will return a JSON response indicating that a two-factor challenge is required:
|
||||
|
||||
```json
|
||||
{
|
||||
"two_factor": true
|
||||
}
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### Custom Authentication Logic
|
||||
|
||||
Override authentication behavior using `Fortify::authenticateUsing()` for custom user retrieval or `Fortify::authenticateThrough()` to customize the authentication pipeline. Override response contracts in `AppServiceProvider` for custom redirects.
|
||||
|
||||
### Registration Customization
|
||||
|
||||
Modify `app/Actions/Fortify/CreateNewUser.php` to customize user creation logic, validation rules, and additional fields.
|
||||
|
||||
### Rate Limiting
|
||||
|
||||
Configure via `fortify.limiters.login` in config. Default configuration throttles by username + IP combination.
|
||||
|
||||
## Key Endpoints
|
||||
|
||||
| Feature | Method | Endpoint |
|
||||
|------------------------|----------|---------------------------------------------|
|
||||
| Login | POST | `/login` |
|
||||
| Logout | POST | `/logout` |
|
||||
| Register | POST | `/register` |
|
||||
| Password Reset Request | POST | `/forgot-password` |
|
||||
| Password Reset | POST | `/reset-password` |
|
||||
| Email Verify Notice | GET | `/email/verify` |
|
||||
| Resend Verification | POST | `/email/verification-notification` |
|
||||
| Password Confirm | POST | `/user/confirm-password` |
|
||||
| Enable 2FA | POST | `/user/two-factor-authentication` |
|
||||
| Confirm 2FA | POST | `/user/confirmed-two-factor-authentication` |
|
||||
| 2FA Challenge | POST | `/two-factor-challenge` |
|
||||
| Get QR Code | GET | `/user/two-factor-qr-code` |
|
||||
| Recovery Codes | GET/POST | `/user/two-factor-recovery-codes` |
|
||||
|
|
@ -1,361 +0,0 @@
|
|||
---
|
||||
name: inertia-react-development
|
||||
description: "Develops Inertia.js v2 React client-side applications. Activates when creating React pages, forms, or navigation; using <Link>, <Form>, useForm, or router; working with deferred props, prefetching, or polling; or when user mentions React with Inertia, React pages, React forms, or React navigation."
|
||||
license: MIT
|
||||
metadata:
|
||||
author: laravel
|
||||
---
|
||||
|
||||
# Inertia React Development
|
||||
|
||||
## When to Apply
|
||||
|
||||
Activate this skill when:
|
||||
|
||||
- Creating or modifying React page components for Inertia
|
||||
- Working with forms in React (using `<Form>` or `useForm`)
|
||||
- Implementing client-side navigation with `<Link>` or `router`
|
||||
- Using v2 features: deferred props, prefetching, WhenVisible, InfiniteScroll, once props, flash data, or polling
|
||||
- Building React-specific features with the Inertia protocol
|
||||
|
||||
## Documentation
|
||||
|
||||
Use `search-docs` for detailed Inertia v2 React patterns and documentation.
|
||||
|
||||
## Basic Usage
|
||||
|
||||
### Page Components Location
|
||||
|
||||
React page components should be placed in the `resources/js/pages` directory.
|
||||
|
||||
### Page Component Structure
|
||||
|
||||
<!-- Basic React Page Component -->
|
||||
```react
|
||||
export default function UsersIndex({ users }) {
|
||||
return (
|
||||
<div>
|
||||
<h1>Users</h1>
|
||||
<ul>
|
||||
{users.map(user => <li key={user.id}>{user.name}</li>)}
|
||||
</ul>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## Client-Side Navigation
|
||||
|
||||
### Basic Link Component
|
||||
|
||||
Use `<Link>` for client-side navigation instead of traditional `<a>` tags:
|
||||
|
||||
<!-- Inertia React Navigation -->
|
||||
```react
|
||||
import { Link, router } from '@inertiajs/react'
|
||||
|
||||
<Link href="/">Home</Link>
|
||||
<Link href="/users">Users</Link>
|
||||
<Link href={`/users/${user.id}`}>View User</Link>
|
||||
```
|
||||
|
||||
### Link with Method
|
||||
|
||||
<!-- Link with POST Method -->
|
||||
```react
|
||||
import { Link } from '@inertiajs/react'
|
||||
|
||||
<Link href="/logout" method="post" as="button">
|
||||
Logout
|
||||
</Link>
|
||||
```
|
||||
|
||||
### Prefetching
|
||||
|
||||
Prefetch pages to improve perceived performance:
|
||||
|
||||
<!-- Prefetch on Hover -->
|
||||
```react
|
||||
import { Link } from '@inertiajs/react'
|
||||
|
||||
<Link href="/users" prefetch>
|
||||
Users
|
||||
</Link>
|
||||
```
|
||||
|
||||
### Programmatic Navigation
|
||||
|
||||
<!-- Router Visit -->
|
||||
```react
|
||||
import { router } from '@inertiajs/react'
|
||||
|
||||
function handleClick() {
|
||||
router.visit('/users')
|
||||
}
|
||||
|
||||
// Or with options
|
||||
router.visit('/users', {
|
||||
method: 'post',
|
||||
data: { name: 'John' },
|
||||
onSuccess: () => console.log('Success!'),
|
||||
})
|
||||
```
|
||||
|
||||
## Form Handling
|
||||
|
||||
### Form Component (Recommended)
|
||||
|
||||
The recommended way to build forms is with the `<Form>` component:
|
||||
|
||||
<!-- Form Component Example -->
|
||||
```react
|
||||
import { Form } from '@inertiajs/react'
|
||||
|
||||
export default function CreateUser() {
|
||||
return (
|
||||
<Form action="/users" method="post">
|
||||
{({ errors, processing, wasSuccessful }) => (
|
||||
<>
|
||||
<input type="text" name="name" />
|
||||
{errors.name && <div>{errors.name}</div>}
|
||||
|
||||
<input type="email" name="email" />
|
||||
{errors.email && <div>{errors.email}</div>}
|
||||
|
||||
<button type="submit" disabled={processing}>
|
||||
{processing ? 'Creating...' : 'Create User'}
|
||||
</button>
|
||||
|
||||
{wasSuccessful && <div>User created!</div>}
|
||||
</>
|
||||
)}
|
||||
</Form>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### Form Component With All Props
|
||||
|
||||
<!-- Form Component Full Example -->
|
||||
```react
|
||||
import { Form } from '@inertiajs/react'
|
||||
|
||||
<Form action="/users" method="post">
|
||||
{({
|
||||
errors,
|
||||
hasErrors,
|
||||
processing,
|
||||
progress,
|
||||
wasSuccessful,
|
||||
recentlySuccessful,
|
||||
clearErrors,
|
||||
resetAndClearErrors,
|
||||
defaults,
|
||||
isDirty,
|
||||
reset,
|
||||
submit
|
||||
}) => (
|
||||
<>
|
||||
<input type="text" name="name" defaultValue={defaults.name} />
|
||||
{errors.name && <div>{errors.name}</div>}
|
||||
|
||||
<button type="submit" disabled={processing}>
|
||||
{processing ? 'Saving...' : 'Save'}
|
||||
</button>
|
||||
|
||||
{progress && (
|
||||
<progress value={progress.percentage} max="100">
|
||||
{progress.percentage}%
|
||||
</progress>
|
||||
)}
|
||||
|
||||
{wasSuccessful && <div>Saved!</div>}
|
||||
</>
|
||||
)}
|
||||
</Form>
|
||||
```
|
||||
|
||||
### Form Component Reset Props
|
||||
|
||||
The `<Form>` component supports automatic resetting:
|
||||
|
||||
- `resetOnError` - Reset form data when the request fails
|
||||
- `resetOnSuccess` - Reset form data when the request succeeds
|
||||
- `setDefaultsOnSuccess` - Update default values on success
|
||||
|
||||
Use the `search-docs` tool with a query of `form component resetting` for detailed guidance.
|
||||
|
||||
<!-- Form with Reset Props -->
|
||||
```react
|
||||
import { Form } from '@inertiajs/react'
|
||||
|
||||
<Form
|
||||
action="/users"
|
||||
method="post"
|
||||
resetOnSuccess
|
||||
setDefaultsOnSuccess
|
||||
>
|
||||
{({ errors, processing, wasSuccessful }) => (
|
||||
<>
|
||||
<input type="text" name="name" />
|
||||
{errors.name && <div>{errors.name}</div>}
|
||||
|
||||
<button type="submit" disabled={processing}>
|
||||
Submit
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</Form>
|
||||
```
|
||||
|
||||
Forms can also be built using the `useForm` helper for more programmatic control. Use the `search-docs` tool with a query of `useForm helper` for guidance.
|
||||
|
||||
### `useForm` Hook
|
||||
|
||||
For more programmatic control or to follow existing conventions, use the `useForm` hook:
|
||||
|
||||
<!-- useForm Hook Example -->
|
||||
```react
|
||||
import { useForm } from '@inertiajs/react'
|
||||
|
||||
export default function CreateUser() {
|
||||
const { data, setData, post, processing, errors, reset } = useForm({
|
||||
name: '',
|
||||
email: '',
|
||||
password: '',
|
||||
})
|
||||
|
||||
function submit(e) {
|
||||
e.preventDefault()
|
||||
post('/users', {
|
||||
onSuccess: () => reset('password'),
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<form onSubmit={submit}>
|
||||
<input
|
||||
type="text"
|
||||
value={data.name}
|
||||
onChange={e => setData('name', e.target.value)}
|
||||
/>
|
||||
{errors.name && <div>{errors.name}</div>}
|
||||
|
||||
<input
|
||||
type="email"
|
||||
value={data.email}
|
||||
onChange={e => setData('email', e.target.value)}
|
||||
/>
|
||||
{errors.email && <div>{errors.email}</div>}
|
||||
|
||||
<input
|
||||
type="password"
|
||||
value={data.password}
|
||||
onChange={e => setData('password', e.target.value)}
|
||||
/>
|
||||
{errors.password && <div>{errors.password}</div>}
|
||||
|
||||
<button type="submit" disabled={processing}>
|
||||
Create User
|
||||
</button>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## Inertia v2 Features
|
||||
|
||||
### Deferred Props
|
||||
|
||||
Use deferred props to load data after initial page render:
|
||||
|
||||
<!-- Deferred Props with Empty State -->
|
||||
```react
|
||||
export default function UsersIndex({ users }) {
|
||||
// users will be undefined initially, then populated
|
||||
return (
|
||||
<div>
|
||||
<h1>Users</h1>
|
||||
{!users ? (
|
||||
<div className="animate-pulse">
|
||||
<div className="h-4 bg-gray-200 rounded w-3/4 mb-2"></div>
|
||||
<div className="h-4 bg-gray-200 rounded w-1/2"></div>
|
||||
</div>
|
||||
) : (
|
||||
<ul>
|
||||
{users.map(user => (
|
||||
<li key={user.id}>{user.name}</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### Polling
|
||||
|
||||
Automatically refresh data at intervals:
|
||||
|
||||
<!-- Polling Example -->
|
||||
```react
|
||||
import { router } from '@inertiajs/react'
|
||||
import { useEffect } from 'react'
|
||||
|
||||
export default function Dashboard({ stats }) {
|
||||
useEffect(() => {
|
||||
const interval = setInterval(() => {
|
||||
router.reload({ only: ['stats'] })
|
||||
}, 5000) // Poll every 5 seconds
|
||||
|
||||
return () => clearInterval(interval)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1>Dashboard</h1>
|
||||
<div>Active Users: {stats.activeUsers}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### WhenVisible
|
||||
|
||||
Lazy-load a prop when an element scrolls into view. Useful for deferring expensive data that sits below the fold:
|
||||
|
||||
<!-- WhenVisible Example -->
|
||||
```react
|
||||
import { WhenVisible } from '@inertiajs/react'
|
||||
|
||||
export default function Dashboard({ stats }) {
|
||||
return (
|
||||
<div>
|
||||
<h1>Dashboard</h1>
|
||||
|
||||
{/* stats prop is loaded only when this section scrolls into view */}
|
||||
<WhenVisible data="stats" buffer={200} fallback={<div className="animate-pulse">Loading stats...</div>}>
|
||||
{({ fetching }) => (
|
||||
<div>
|
||||
<p>Total Users: {stats.total_users}</p>
|
||||
<p>Revenue: {stats.revenue}</p>
|
||||
{fetching && <span>Refreshing...</span>}
|
||||
</div>
|
||||
)}
|
||||
</WhenVisible>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## Server-Side Patterns
|
||||
|
||||
Server-side patterns (Inertia::render, props, middleware) are covered in inertia-laravel guidelines.
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
- Using traditional `<a>` links instead of Inertia's `<Link>` component (breaks SPA behavior)
|
||||
- Forgetting to add loading states (skeleton screens) when using deferred props
|
||||
- Not handling the `undefined` state of deferred props before data loads
|
||||
- Using `<form>` without preventing default submission (use `<Form>` component or `e.preventDefault()`)
|
||||
- Forgetting to check if `<Form>` component is available in your Inertia version
|
||||
|
|
@ -1,77 +0,0 @@
|
|||
---
|
||||
name: pennant-development
|
||||
description: "Use when working with Laravel Pennant the official Laravel feature flag package. Trigger whenever the query mentions Pennant by name or involves feature flags or feature toggles in a Laravel project. Tasks include defining feature flags checking whether features are active creating class based features in `app/Features` using Blade `@feature` directives scoping flags to users or teams building custom Pennant storage drivers protecting routes with feature flags testing feature flags with Pest or PHPUnit and implementing A B testing or gradual rollouts with feature flags. Do not trigger for generic Laravel configuration authorization policies authentication or non Pennant feature management systems."
|
||||
license: MIT
|
||||
metadata:
|
||||
author: laravel
|
||||
---
|
||||
|
||||
# Pennant Features
|
||||
|
||||
## When to Apply
|
||||
|
||||
Activate this skill when:
|
||||
|
||||
- Creating or checking feature flags
|
||||
- Managing feature rollouts
|
||||
- Implementing A/B testing
|
||||
|
||||
## Documentation
|
||||
|
||||
Use `search-docs` for detailed Pennant patterns and documentation.
|
||||
|
||||
## Basic Usage
|
||||
|
||||
### Defining Features
|
||||
|
||||
<!-- Defining Features -->
|
||||
```php
|
||||
use Laravel\Pennant\Feature;
|
||||
|
||||
Feature::define('new-dashboard', function (User $user) {
|
||||
return $user->isAdmin();
|
||||
});
|
||||
```
|
||||
|
||||
### Checking Features
|
||||
|
||||
<!-- Checking Features -->
|
||||
```php
|
||||
if (Feature::active('new-dashboard')) {
|
||||
// Feature is active
|
||||
}
|
||||
|
||||
// With scope
|
||||
if (Feature::for($user)->active('new-dashboard')) {
|
||||
// Feature is active for this user
|
||||
}
|
||||
```
|
||||
|
||||
### Blade Directive
|
||||
|
||||
<!-- Blade Directive -->
|
||||
```blade
|
||||
@feature('new-dashboard')
|
||||
<x-new-dashboard />
|
||||
@else
|
||||
<x-old-dashboard />
|
||||
@endfeature
|
||||
```
|
||||
|
||||
### Activating / Deactivating
|
||||
|
||||
<!-- Activating Features -->
|
||||
```php
|
||||
Feature::activate('new-dashboard');
|
||||
Feature::for($user)->activate('new-dashboard');
|
||||
```
|
||||
|
||||
## Verification
|
||||
|
||||
1. Check feature flag is defined
|
||||
2. Test with different scopes/users
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
- Forgetting to scope features for specific users/entities
|
||||
- Not following existing naming conventions
|
||||
|
|
@ -1,157 +0,0 @@
|
|||
---
|
||||
name: pest-testing
|
||||
description: "Use this skill for Pest PHP testing in Laravel projects only. Trigger whenever any test is being written, edited, fixed, or refactored — including fixing tests that broke after a code change, adding assertions, converting PHPUnit to Pest, adding datasets, and TDD workflows. Always activate when the user asks how to write something in Pest, mentions test files or directories (tests/Feature, tests/Unit, tests/Browser), or needs browser testing, smoke testing multiple pages for JS errors, or architecture tests. Covers: it()/expect() syntax, datasets, mocking, browser testing (visit/click/fill), smoke testing, arch(), Livewire component tests, RefreshDatabase, and all Pest 4 features. Do not use for factories, seeders, migrations, controllers, models, or non-test PHP code."
|
||||
license: MIT
|
||||
metadata:
|
||||
author: laravel
|
||||
---
|
||||
|
||||
# Pest Testing 4
|
||||
|
||||
## Documentation
|
||||
|
||||
Use `search-docs` for detailed Pest 4 patterns and documentation.
|
||||
|
||||
## Basic Usage
|
||||
|
||||
### Creating Tests
|
||||
|
||||
All tests must be written using Pest. Use `php artisan make:test --pest {name}`.
|
||||
|
||||
### Test Organization
|
||||
|
||||
- Unit/Feature tests: `tests/Feature` and `tests/Unit` directories.
|
||||
- Browser tests: `tests/Browser/` directory.
|
||||
- Do NOT remove tests without approval - these are core application code.
|
||||
|
||||
### Basic Test Structure
|
||||
|
||||
<!-- Basic Pest Test Example -->
|
||||
```php
|
||||
it('is true', function () {
|
||||
expect(true)->toBeTrue();
|
||||
});
|
||||
```
|
||||
|
||||
### Running Tests
|
||||
|
||||
- Run minimal tests with filter before finalizing: `php artisan test --compact --filter=testName`.
|
||||
- Run all tests: `php artisan test --compact`.
|
||||
- Run file: `php artisan test --compact tests/Feature/ExampleTest.php`.
|
||||
|
||||
## Assertions
|
||||
|
||||
Use specific assertions (`assertSuccessful()`, `assertNotFound()`) instead of `assertStatus()`:
|
||||
|
||||
<!-- Pest Response Assertion -->
|
||||
```php
|
||||
it('returns all', function () {
|
||||
$this->postJson('/api/docs', [])->assertSuccessful();
|
||||
});
|
||||
```
|
||||
|
||||
| Use | Instead of |
|
||||
|-----|------------|
|
||||
| `assertSuccessful()` | `assertStatus(200)` |
|
||||
| `assertNotFound()` | `assertStatus(404)` |
|
||||
| `assertForbidden()` | `assertStatus(403)` |
|
||||
|
||||
## Mocking
|
||||
|
||||
Import mock function before use: `use function Pest\Laravel\mock;`
|
||||
|
||||
## Datasets
|
||||
|
||||
Use datasets for repetitive tests (validation rules, etc.):
|
||||
|
||||
<!-- Pest Dataset Example -->
|
||||
```php
|
||||
it('has emails', function (string $email) {
|
||||
expect($email)->not->toBeEmpty();
|
||||
})->with([
|
||||
'james' => 'james@laravel.com',
|
||||
'taylor' => 'taylor@laravel.com',
|
||||
]);
|
||||
```
|
||||
|
||||
## Pest 4 Features
|
||||
|
||||
| Feature | Purpose |
|
||||
|---------|---------|
|
||||
| Browser Testing | Full integration tests in real browsers |
|
||||
| Smoke Testing | Validate multiple pages quickly |
|
||||
| Visual Regression | Compare screenshots for visual changes |
|
||||
| Test Sharding | Parallel CI runs |
|
||||
| Architecture Testing | Enforce code conventions |
|
||||
|
||||
### Browser Test Example
|
||||
|
||||
Browser tests run in real browsers for full integration testing:
|
||||
|
||||
- Browser tests live in `tests/Browser/`.
|
||||
- Use Laravel features like `Event::fake()`, `assertAuthenticated()`, and model factories.
|
||||
- Use `RefreshDatabase` for clean state per test.
|
||||
- Interact with page: click, type, scroll, select, submit, drag-and-drop, touch gestures.
|
||||
- Test on multiple browsers (Chrome, Firefox, Safari) if requested.
|
||||
- Test on different devices/viewports (iPhone 14 Pro, tablets) if requested.
|
||||
- Switch color schemes (light/dark mode) when appropriate.
|
||||
- Take screenshots or pause tests for debugging.
|
||||
|
||||
<!-- Pest Browser Test Example -->
|
||||
```php
|
||||
it('may reset the password', function () {
|
||||
Notification::fake();
|
||||
|
||||
$this->actingAs(User::factory()->create());
|
||||
|
||||
$page = visit('/sign-in');
|
||||
|
||||
$page->assertSee('Sign In')
|
||||
->assertNoJavaScriptErrors()
|
||||
->click('Forgot Password?')
|
||||
->fill('email', 'nuno@laravel.com')
|
||||
->click('Send Reset Link')
|
||||
->assertSee('We have emailed your password reset link!');
|
||||
|
||||
Notification::assertSent(ResetPassword::class);
|
||||
});
|
||||
```
|
||||
|
||||
### Smoke Testing
|
||||
|
||||
Quickly validate multiple pages have no JavaScript errors:
|
||||
|
||||
<!-- Pest Smoke Testing Example -->
|
||||
```php
|
||||
$pages = visit(['/', '/about', '/contact']);
|
||||
|
||||
$pages->assertNoJavaScriptErrors()->assertNoConsoleLogs();
|
||||
```
|
||||
|
||||
### Visual Regression Testing
|
||||
|
||||
Capture and compare screenshots to detect visual changes.
|
||||
|
||||
### Test Sharding
|
||||
|
||||
Split tests across parallel processes for faster CI runs.
|
||||
|
||||
### Architecture Testing
|
||||
|
||||
Pest 4 includes architecture testing (from Pest 3):
|
||||
|
||||
<!-- Architecture Test Example -->
|
||||
```php
|
||||
arch('controllers')
|
||||
->expect('App\Http\Controllers')
|
||||
->toExtendNothing()
|
||||
->toHaveSuffix('Controller');
|
||||
```
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
- Not importing `use function Pest\Laravel\mock;` before using mock
|
||||
- Using `assertStatus(200)` instead of `assertSuccessful()`
|
||||
- Forgetting datasets for repetitive validation tests
|
||||
- Deleting tests without approval
|
||||
- Forgetting `assertNoJavaScriptErrors()` in browser tests
|
||||
|
|
@ -1,54 +0,0 @@
|
|||
---
|
||||
name: querying-the-database
|
||||
description: "Query the local or production database from the CLI via the `agent:db` artisan command. Activates when the user asks to inspect, count, look up, or run a query against the database; asks 'how many X', 'what's in prod', 'check the prod DB', 'query the database'; or mentions production data, the prod database, or running SQL."
|
||||
metadata:
|
||||
author: whisper-money
|
||||
---
|
||||
|
||||
# Querying the Database
|
||||
|
||||
## When to Apply
|
||||
|
||||
Activate this skill whenever you need to read data from the database to answer a
|
||||
question — especially anything about **production** data. Prefer this command over
|
||||
the `tinker`, `database-query`, or `database-schema` Boost tools when the user asks
|
||||
about prod, since those default to the local connection.
|
||||
|
||||
## The `agent:db` command
|
||||
|
||||
Runs a query (`SELECT`, `SHOW`, `DESCRIBE`, `DELETE`, etc.) and prints the result.
|
||||
|
||||
```bash
|
||||
php artisan agent:db "<query>"
|
||||
```
|
||||
|
||||
### Options
|
||||
|
||||
- `--format=json` (default) — pretty-printed JSON, best for parsing the result yourself.
|
||||
- `--format=table` — classic console table, best when showing the result to the user.
|
||||
- `--prod` — run against the **production** database (the `prod` connection backed by
|
||||
`PROD_DB_URL`). Without this flag the query runs against the local DB.
|
||||
|
||||
### Examples
|
||||
|
||||
```bash
|
||||
# Local, JSON (default)
|
||||
php artisan agent:db "select id, email from users limit 5"
|
||||
|
||||
# Local, human-readable table
|
||||
php artisan agent:db --format=table "select count(*) as total from transactions"
|
||||
|
||||
# Production
|
||||
php artisan agent:db --prod "select count(*) from users"
|
||||
php artisan agent:db --prod --format=table "select status, count(*) from subscriptions group by status"
|
||||
```
|
||||
|
||||
## Guidelines
|
||||
|
||||
- **Be careful with `--prod`**: this is live customer data. Only run prod queries the
|
||||
user explicitly asked for, keep them scoped (add `LIMIT`, filter by id), and never
|
||||
dump large or sensitive datasets unprompted. This app is privacy-first.
|
||||
- Use `--format=json` when you need to read the values to continue working; use
|
||||
`--format=table` when presenting results back to the user.
|
||||
- Inspect schema first with `database-schema` (local) when you're unsure of column
|
||||
names before writing a query.
|
||||
|
|
@ -1,119 +0,0 @@
|
|||
---
|
||||
name: tailwindcss-development
|
||||
description: "Always invoke when the user's message includes 'tailwind' in any form. Also invoke for: building responsive grid layouts (multi-column card grids, product grids), flex/grid page structures (dashboards with sidebars, fixed topbars, mobile-toggle navs), styling UI components (cards, tables, navbars, pricing sections, forms, inputs, badges), adding dark mode variants, fixing spacing or typography, and Tailwind v3/v4 work. The core use case: writing or fixing Tailwind utility classes in HTML templates (Blade, JSX, Vue). Skip for backend PHP logic, database queries, API routes, JavaScript with no HTML/CSS component, CSS file audits, build tool configuration, and vanilla CSS."
|
||||
license: MIT
|
||||
metadata:
|
||||
author: laravel
|
||||
---
|
||||
|
||||
# Tailwind CSS Development
|
||||
|
||||
## Documentation
|
||||
|
||||
Use `search-docs` for detailed Tailwind CSS v4 patterns and documentation.
|
||||
|
||||
## Basic Usage
|
||||
|
||||
- Use Tailwind CSS classes to style HTML. Check and follow existing Tailwind conventions in the project before introducing new patterns.
|
||||
- Offer to extract repeated patterns into components that match the project's conventions (e.g., Blade, JSX, Vue).
|
||||
- Consider class placement, order, priority, and defaults. Remove redundant classes, add classes to parent or child elements carefully to reduce repetition, and group elements logically.
|
||||
|
||||
## Tailwind CSS v4 Specifics
|
||||
|
||||
- Always use Tailwind CSS v4 and avoid deprecated utilities.
|
||||
- `corePlugins` is not supported in Tailwind v4.
|
||||
|
||||
### CSS-First Configuration
|
||||
|
||||
In Tailwind v4, configuration is CSS-first using the `@theme` directive — no separate `tailwind.config.js` file is needed:
|
||||
|
||||
<!-- CSS-First Config -->
|
||||
```css
|
||||
@theme {
|
||||
--color-brand: oklch(0.72 0.11 178);
|
||||
}
|
||||
```
|
||||
|
||||
### Import Syntax
|
||||
|
||||
In Tailwind v4, import Tailwind with a regular CSS `@import` statement instead of the `@tailwind` directives used in v3:
|
||||
|
||||
<!-- v4 Import Syntax -->
|
||||
```diff
|
||||
- @tailwind base;
|
||||
- @tailwind components;
|
||||
- @tailwind utilities;
|
||||
+ @import "tailwindcss";
|
||||
```
|
||||
|
||||
### Replaced Utilities
|
||||
|
||||
Tailwind v4 removed deprecated utilities. Use the replacements shown below. Opacity values remain numeric.
|
||||
|
||||
| Deprecated | Replacement |
|
||||
|------------|-------------|
|
||||
| bg-opacity-* | bg-black/* |
|
||||
| text-opacity-* | text-black/* |
|
||||
| border-opacity-* | border-black/* |
|
||||
| divide-opacity-* | divide-black/* |
|
||||
| ring-opacity-* | ring-black/* |
|
||||
| placeholder-opacity-* | placeholder-black/* |
|
||||
| flex-shrink-* | shrink-* |
|
||||
| flex-grow-* | grow-* |
|
||||
| overflow-ellipsis | text-ellipsis |
|
||||
| decoration-slice | box-decoration-slice |
|
||||
| decoration-clone | box-decoration-clone |
|
||||
|
||||
## Spacing
|
||||
|
||||
Use `gap` utilities instead of margins for spacing between siblings:
|
||||
|
||||
<!-- Gap Utilities -->
|
||||
```html
|
||||
<div class="flex gap-8">
|
||||
<div>Item 1</div>
|
||||
<div>Item 2</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
## Dark Mode
|
||||
|
||||
If existing pages and components support dark mode, new pages and components must support it the same way, typically using the `dark:` variant:
|
||||
|
||||
<!-- Dark Mode -->
|
||||
```html
|
||||
<div class="bg-white dark:bg-gray-900 text-gray-900 dark:text-white">
|
||||
Content adapts to color scheme
|
||||
</div>
|
||||
```
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Flexbox Layout
|
||||
|
||||
<!-- Flexbox Layout -->
|
||||
```html
|
||||
<div class="flex items-center justify-between gap-4">
|
||||
<div>Left content</div>
|
||||
<div>Right content</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
### Grid Layout
|
||||
|
||||
<!-- Grid Layout -->
|
||||
```html
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
<div>Card 1</div>
|
||||
<div>Card 2</div>
|
||||
<div>Card 3</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
- Using deprecated v3 utilities (bg-opacity-*, flex-shrink-*, etc.)
|
||||
- Using `@tailwind` directives instead of `@import "tailwindcss"`
|
||||
- Trying to use `tailwind.config.js` instead of CSS `@theme` directive
|
||||
- Using margins for spacing between siblings instead of gap utilities
|
||||
- Forgetting to add dark mode variants when the project uses dark mode
|
||||
|
|
@ -1,80 +0,0 @@
|
|||
---
|
||||
name: wayfinder-development
|
||||
description: "Activates whenever referencing backend routes in frontend components. Use when importing from @/actions or @/routes, calling Laravel routes from TypeScript, or working with Wayfinder route functions."
|
||||
license: MIT
|
||||
metadata:
|
||||
author: laravel
|
||||
---
|
||||
|
||||
# Wayfinder Development
|
||||
|
||||
## Documentation
|
||||
|
||||
Use `search-docs` for detailed Wayfinder patterns and documentation.
|
||||
|
||||
## Quick Reference
|
||||
|
||||
### Generate Routes
|
||||
|
||||
Run after route changes if Vite plugin isn't installed:
|
||||
```bash
|
||||
php artisan wayfinder:generate --no-interaction
|
||||
```
|
||||
For form helpers, use `--with-form` flag:
|
||||
```bash
|
||||
php artisan wayfinder:generate --with-form --no-interaction
|
||||
```
|
||||
|
||||
### Import Patterns
|
||||
|
||||
<!-- Controller Action Imports -->
|
||||
```typescript
|
||||
// Named imports for tree-shaking (preferred)...
|
||||
import { show, store, update } from '@/actions/App/Http/Controllers/PostController'
|
||||
|
||||
// Named route imports...
|
||||
import { show as postShow } from '@/routes/post'
|
||||
```
|
||||
|
||||
### Common Methods
|
||||
|
||||
<!-- Wayfinder Methods -->
|
||||
```typescript
|
||||
// Get route object...
|
||||
show(1) // { url: "/posts/1", method: "get" }
|
||||
|
||||
// Get URL string...
|
||||
show.url(1) // "/posts/1"
|
||||
|
||||
// Specific HTTP methods...
|
||||
show.get(1)
|
||||
store.post()
|
||||
update.patch(1)
|
||||
destroy.delete(1)
|
||||
|
||||
// Form attributes for HTML forms...
|
||||
store.form() // { action: "/posts", method: "post" }
|
||||
|
||||
// Query parameters...
|
||||
show(1, { query: { page: 1 } }) // "/posts/1?page=1"
|
||||
```
|
||||
|
||||
## Wayfinder + Inertia
|
||||
|
||||
Use Wayfinder with the `<Form>` component:
|
||||
<!-- Wayfinder Form (React) -->
|
||||
```typescript
|
||||
<Form {...store.form()}><input name="title" /></Form>
|
||||
```
|
||||
|
||||
## Verification
|
||||
|
||||
1. Run `php artisan wayfinder:generate` to regenerate routes if Vite plugin isn't installed
|
||||
2. Check TypeScript imports resolve correctly
|
||||
3. Verify route URLs match expected paths
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
- Using default imports instead of named imports (breaks tree-shaking)
|
||||
- Forgetting to regenerate after route changes
|
||||
- Not using type-safe parameter objects for route model binding
|
||||
|
|
@ -1 +0,0 @@
|
|||
../.agents/skills
|
||||
|
|
@ -6,9 +6,6 @@
|
|||
"artisan",
|
||||
"boost:mcp"
|
||||
]
|
||||
},
|
||||
"sentry": {
|
||||
"url": "https://mcp.sentry.dev/mcp"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -6,10 +6,9 @@ alwaysApply: true
|
|||
|
||||
# Laravel Boost Guidelines
|
||||
|
||||
The Laravel Boost guidelines are specifically curated by Laravel maintainers for this application. These guidelines should be followed closely to ensure the best experience when building Laravel applications.
|
||||
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.4.1
|
||||
|
|
@ -17,7 +16,6 @@ This application is a Laravel application and its main Laravel ecosystems packag
|
|||
- laravel/cashier (CASHIER) - v16
|
||||
- laravel/fortify (FORTIFY) - v1
|
||||
- laravel/framework (LARAVEL) - v12
|
||||
- laravel/pennant (PENNANT) - v1
|
||||
- laravel/prompts (PROMPTS) - v0
|
||||
- laravel/wayfinder (WAYFINDER) - v0
|
||||
- laravel/mcp (MCP) - v0
|
||||
|
|
@ -32,96 +30,77 @@ This application is a Laravel application and its main Laravel ecosystems packag
|
|||
- eslint (ESLINT) - v9
|
||||
- prettier (PRETTIER) - v3
|
||||
|
||||
## Skills Activation
|
||||
|
||||
This project has domain-specific skills available. You MUST activate the relevant skill whenever you work in that domain—don't wait until you're stuck.
|
||||
|
||||
- `pennant-development` — Manages feature flags with Laravel Pennant. Activates when creating, checking, or toggling feature flags; showing or hiding features conditionally; implementing A/B testing; working with @feature directive; or when the user mentions feature flags, feature toggles, Pennant, conditional features, rollouts, or gradually enabling features.
|
||||
- `wayfinder-development` — Activates whenever referencing backend routes in frontend components. Use when importing from @/actions or @/routes, calling Laravel routes from TypeScript, or working with Wayfinder route functions.
|
||||
- `pest-testing` — Tests applications using the Pest 4 PHP framework. Activates when writing tests, creating unit or feature tests, adding assertions, testing Livewire components, browser testing, debugging test failures, working with datasets or mocking; or when the user mentions test, spec, TDD, expects, assertion, coverage, or needs to verify functionality works.
|
||||
- `inertia-react-development` — Develops Inertia.js v2 React client-side applications. Activates when creating React pages, forms, or navigation; using <Link>, <Form>, useForm, or router; working with deferred props, prefetching, or polling; or when user mentions React with Inertia, React pages, React forms, or React navigation.
|
||||
- `tailwindcss-development` — Styles applications using Tailwind CSS v4 utilities. Activates when adding styles, restyling components, working with gradients, spacing, layout, flex, grid, responsive design, dark mode, colors, typography, or borders; or when the user mentions CSS, styling, classes, Tailwind, restyle, hero section, cards, buttons, or any visual/UI changes.
|
||||
|
||||
## 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, and naming.
|
||||
- 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 they work. Unit and feature tests are more important.
|
||||
- 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.
|
||||
- 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.
|
||||
|
||||
## Documentation Files
|
||||
## 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.
|
||||
|
||||
## Replies
|
||||
|
||||
- Be concise in your explanations - focus on what's important rather than explaining obvious details.
|
||||
|
||||
=== boost rules ===
|
||||
|
||||
# Laravel Boost
|
||||
|
||||
## 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.
|
||||
- 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.
|
||||
- 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 trying other approaches when working with Laravel or Laravel ecosystem packages. This tool automatically passes a list of installed packages and their versions to the remote Boost API, so it returns only version-specific documentation for the user's circumstance. You should pass an array of packages to filter on if you know you need docs for particular packages.
|
||||
- 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 at once. For example: `['rate limiting', 'routing rate limiting', 'routing']`. The most relevant results will be returned first.
|
||||
- Do not add package names to queries; package information is already shared. For example, use `test resource table`, not `filament 4 test resource table`.
|
||||
- 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
|
||||
|
||||
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
|
||||
## PHP
|
||||
|
||||
- Always use curly braces for control structures, even for single-line bodies.
|
||||
|
||||
## Constructors
|
||||
- Always use curly braces for control structures, even if it has one line.
|
||||
|
||||
### Constructors
|
||||
- Use PHP 8 constructor property promotion in `__construct()`.
|
||||
- <code-snippet>public function __construct(public GitHub $github) { }</code-snippet>
|
||||
- Do not allow empty `__construct()` methods with zero parameters unless the constructor is private.
|
||||
|
||||
## Type Declarations
|
||||
- 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.
|
||||
|
||||
|
|
@ -132,172 +111,411 @@ protected function isAccessible(User $user, ?string $path = null): bool
|
|||
}
|
||||
</code-snippet>
|
||||
|
||||
## Enums
|
||||
|
||||
- Typically, keys in an Enum should be TitleCase. For example: `FavoritePerson`, `BestLake`, `Monthly`.
|
||||
|
||||
## Comments
|
||||
|
||||
- Prefer PHPDoc blocks over inline comments. Never use comments within the code itself unless the logic is exceptionally complex.
|
||||
- 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.
|
||||
|
||||
- Add useful array shape type definitions when appropriate.
|
||||
## Enums
|
||||
- Typically, keys in an Enum should be TitleCase. For example: `FavoritePerson`, `BestLake`, `Monthly`.
|
||||
|
||||
=== tests rules ===
|
||||
|
||||
# Test Enforcement
|
||||
|
||||
- Every change must be programmatically tested. Write a new test or update an existing test, then run the affected tests to make sure they pass.
|
||||
- Run the minimum number of tests needed to ensure code quality and speed. Use `php artisan test --compact` with a specific filename or filter.
|
||||
|
||||
=== inertia-laravel/core rules ===
|
||||
|
||||
# Inertia
|
||||
## Inertia Core
|
||||
|
||||
- Inertia.js components should be placed in the `resources/js/Pages` directory unless specified differently in the JS bundler (vite.config.js).
|
||||
- Use `Inertia::render()` for server-side routing instead of traditional Blade views.
|
||||
- Use `search-docs` for accurate guidance on all things Inertia.
|
||||
|
||||
<code-snippet lang="php" name="Inertia::render Example">
|
||||
// routes/web.php example
|
||||
Route::get('/users', function () {
|
||||
return Inertia::render('Users/Index', [
|
||||
'users' => User::all()
|
||||
]);
|
||||
});
|
||||
</code-snippet>
|
||||
|
||||
- Inertia creates fully client-side rendered SPAs without modern SPA complexity, leveraging existing server-side patterns.
|
||||
- Components live in `resources/js/Pages` (unless specified in `vite.config.js`). Use `Inertia::render()` for server-side routing instead of Blade views.
|
||||
- ALWAYS use `search-docs` tool for version-specific Inertia documentation and updated code examples.
|
||||
- IMPORTANT: Activate `inertia-react-development` when working with Inertia client-side patterns.
|
||||
|
||||
=== inertia-laravel/v2 rules ===
|
||||
|
||||
# Inertia v2
|
||||
## Inertia v2
|
||||
|
||||
- Make use of all Inertia features from v1 & v2. Check the documentation before making any changes to ensure we are taking the correct approach.
|
||||
|
||||
### Inertia v2 New Features
|
||||
- Polling
|
||||
- Prefetching
|
||||
- Deferred props
|
||||
- Infinite scrolling using merging props and `WhenVisible`
|
||||
- Lazy loading data on scroll
|
||||
|
||||
### Deferred Props & Empty States
|
||||
- When using deferred props on the frontend, you should add a nice empty state with pulsing / animated skeleton.
|
||||
|
||||
### Inertia Form General Guidance
|
||||
- The recommended way to build forms when using Inertia is with the `<Form>` component - a useful example is below. Use `search-docs` with a query of `form component` for guidance.
|
||||
- Forms can also be built using the `useForm` helper for more programmatic control, or to follow existing conventions. Use `search-docs` with a query of `useForm helper` for guidance.
|
||||
- `resetOnError`, `resetOnSuccess`, and `setDefaultsOnSuccess` are available on the `<Form>` component. Use `search-docs` with a query of 'form component resetting' for guidance.
|
||||
|
||||
- Use all Inertia features from v1 and v2. Check the documentation before making changes to ensure the correct approach.
|
||||
- New features: deferred props, infinite scrolling (merging props + `WhenVisible`), lazy loading on scroll, polling, prefetching.
|
||||
- When using deferred props, add an empty state with a pulsing or animated skeleton.
|
||||
|
||||
=== laravel/core rules ===
|
||||
|
||||
# Do Things the Laravel Way
|
||||
## 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 `php artisan make:class`.
|
||||
- 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
|
||||
|
||||
### 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.
|
||||
- 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
|
||||
|
||||
### 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.
|
||||
|
||||
## 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.
|
||||
|
||||
## Queues
|
||||
|
||||
### Queues
|
||||
- Use queued jobs for time-consuming operations with the `ShouldQueue` interface.
|
||||
|
||||
## Configuration
|
||||
### 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
|
||||
|
||||
### 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] {name}` to create a feature test, and pass `--unit` to create a unit test. Most tests should be feature tests.
|
||||
|
||||
## Vite Error
|
||||
- When creating tests, make use of `php artisan make:test [options] <name>` 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/v12 rules ===
|
||||
|
||||
# Laravel 12
|
||||
## Laravel 12
|
||||
|
||||
- CRITICAL: ALWAYS use `search-docs` tool for version-specific Laravel documentation and updated code examples.
|
||||
- Use the `search-docs` tool to get version specific documentation.
|
||||
- Since Laravel 11, Laravel has a new streamlined file structure which this project uses.
|
||||
|
||||
## Laravel 12 Structure
|
||||
|
||||
- In Laravel 12, middleware are no longer registered in `app/Http/Kernel.php`.
|
||||
- Middleware are configured declaratively in `bootstrap/app.php` using `Application::configure()->withMiddleware()`.
|
||||
### Laravel 12 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.
|
||||
- The `app\Console\Kernel.php` file no longer exists; use `bootstrap/app.php` or `routes/console.php` for console configuration.
|
||||
- Console commands in `app/Console/Commands/` are automatically available and do not require manual registration.
|
||||
|
||||
## Database
|
||||
- **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 12 allows limiting eagerly loaded records natively, without external packages: `$query->latest()->limit(10);`.
|
||||
- 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.
|
||||
|
||||
=== pennant/core rules ===
|
||||
|
||||
# Laravel Pennant
|
||||
|
||||
- This application uses Laravel Pennant for feature flag management, providing a flexible system for controlling feature availability across different organizations and user types.
|
||||
- IMPORTANT: Always use `search-docs` tool for version-specific Pennant documentation and updated code examples.
|
||||
- IMPORTANT: Activate `pennant-development` every time you're working with a Pennant or feature-flag-related task.
|
||||
|
||||
=== wayfinder/core rules ===
|
||||
|
||||
# Laravel Wayfinder
|
||||
## Laravel Wayfinder
|
||||
|
||||
Wayfinder generates TypeScript functions for Laravel routes. Import from `@/actions/` (controllers) or `@/routes/` (named routes).
|
||||
Wayfinder generates TypeScript functions and types for Laravel controllers and routes which you can import into your client side code. It provides type safety and automatic synchronization between backend routes and frontend code.
|
||||
|
||||
### Development Guidelines
|
||||
- Always use `search-docs` to check wayfinder correct usage before implementing any features.
|
||||
- Always Prefer named imports for tree-shaking (e.g., `import { show } from '@/actions/...'`)
|
||||
- Avoid default controller imports (prevents tree-shaking)
|
||||
- Run `wayfinder:generate` after route changes if Vite plugin isn't installed
|
||||
|
||||
### Feature Overview
|
||||
- Form Support: Use `.form()` with `--with-form` flag for HTML form attributes — `<form {...store.form()}>` → `action="/posts" method="post"`
|
||||
- HTTP Methods: Call `.get()`, `.post()`, `.patch()`, `.put()`, `.delete()` for specific methods — `show.head(1)` → `{ url: "/posts/1", method: "head" }`
|
||||
- Invokable Controllers: Import and invoke directly as functions. For example, `import StorePost from '@/actions/.../StorePostController'; StorePost()`
|
||||
- Named Routes: Import from `@/routes/` for non-controller routes. For example, `import { show } from '@/routes/post'; show(1)` for route name `post.show`
|
||||
- Parameter Binding: Detects route keys (e.g., `{post:slug}`) and accepts matching object properties — `show("my-post")` or `show({ slug: "my-post" })`
|
||||
- Query Merging: Use `mergeQuery` to merge with `window.location.search`, set values to `null` to remove — `show(1, { mergeQuery: { page: 2, sort: null } })`
|
||||
- Query Parameters: Pass `{ query: {...} }` in options to append params — `show(1, { query: { page: 1 } })` → `"/posts/1?page=1"`
|
||||
- Route Objects: Functions return `{ url, method }` shaped objects — `show(1)` → `{ url: "/posts/1", method: "get" }`
|
||||
- URL Extraction: Use `.url()` to get URL string — `show.url(1)` → `"/posts/1"`
|
||||
|
||||
### Example Usage
|
||||
|
||||
<code-snippet name="Wayfinder Basic Usage" lang="typescript">
|
||||
// Import controller methods (tree-shakable)
|
||||
import { show, store, update } from '@/actions/App/Http/Controllers/PostController'
|
||||
|
||||
// Get route object with URL and method...
|
||||
show(1) // { url: "/posts/1", method: "get" }
|
||||
|
||||
// Get just the URL...
|
||||
show.url(1) // "/posts/1"
|
||||
|
||||
// Use specific HTTP methods...
|
||||
show.get(1) // { url: "/posts/1", method: "get" }
|
||||
show.head(1) // { url: "/posts/1", method: "head" }
|
||||
|
||||
// Import named routes...
|
||||
import { show as postShow } from '@/routes/post' // For route name 'post.show'
|
||||
postShow(1) // { url: "/posts/1", method: "get" }
|
||||
</code-snippet>
|
||||
|
||||
|
||||
### Wayfinder + Inertia
|
||||
If your application uses the `<Form>` component from Inertia, you can use Wayfinder to generate form action and method automatically.
|
||||
<code-snippet name="Wayfinder Form Component (React)" lang="typescript">
|
||||
|
||||
<Form {...store.form()}><input name="title" /></Form>
|
||||
|
||||
</code-snippet>
|
||||
|
||||
- IMPORTANT: Activate `wayfinder-development` skill whenever referencing backend routes in frontend components.
|
||||
- Invokable Controllers: `import StorePost from '@/actions/.../StorePostController'; StorePost()`.
|
||||
- Parameter Binding: Detects route keys (`{post:slug}`) — `show({ slug: "my-post" })`.
|
||||
- Query Merging: `show(1, { mergeQuery: { page: 2, sort: null } })` merges with current URL, `null` removes params.
|
||||
- Inertia: Use `.form()` with `<Form>` component or `form.submit(store())` with useForm.
|
||||
|
||||
=== pint/core rules ===
|
||||
|
||||
# Laravel Pint Code Formatter
|
||||
## 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.
|
||||
|
||||
|
||||
=== pest/core rules ===
|
||||
|
||||
## Pest
|
||||
|
||||
- This project uses Pest for testing. Create tests: `php artisan make:test --pest {name}`.
|
||||
- Run tests: `php artisan test --compact` or filter: `php artisan test --compact --filter=testName`.
|
||||
- Do NOT delete tests without approval.
|
||||
- CRITICAL: ALWAYS use `search-docs` tool for version-specific Pest documentation and updated code examples.
|
||||
- IMPORTANT: Activate `pest-testing` every time you're working with a Pest or testing-related task.
|
||||
### Testing
|
||||
- If you need to verify a feature is working, write or update a Unit / Feature test.
|
||||
|
||||
### Pest Tests
|
||||
- All tests must be written using Pest. Use `php artisan make:test --pest <name>`.
|
||||
- You must not remove any tests or test files from the tests directory without approval. These are not temporary or helper files - these are core to the application.
|
||||
- Tests should test all of the happy paths, failure paths, and weird paths.
|
||||
- Tests live in the `tests/Feature` and `tests/Unit` directories.
|
||||
- Pest tests look and behave like this:
|
||||
<code-snippet name="Basic Pest Test Example" lang="php">
|
||||
it('is true', function () {
|
||||
expect(true)->toBeTrue();
|
||||
});
|
||||
</code-snippet>
|
||||
|
||||
### Running Tests
|
||||
- Run the minimal number of tests using an appropriate filter before finalizing code edits.
|
||||
- To run all tests: `php artisan test`.
|
||||
- To run all tests in a file: `php artisan test tests/Feature/ExampleTest.php`.
|
||||
- To filter on a particular test name: `php artisan test --filter=testName` (recommended after making a change to a related file).
|
||||
- When the tests relating to your changes are passing, ask the user if they would like to run the entire test suite to ensure everything is still passing.
|
||||
|
||||
### Pest Assertions
|
||||
- When asserting status codes on a response, use the specific method like `assertForbidden` and `assertNotFound` instead of using `assertStatus(403)` or similar, e.g.:
|
||||
<code-snippet name="Pest Example Asserting postJson Response" lang="php">
|
||||
it('returns all', function () {
|
||||
$response = $this->postJson('/api/docs', []);
|
||||
|
||||
$response->assertSuccessful();
|
||||
});
|
||||
</code-snippet>
|
||||
|
||||
### Mocking
|
||||
- Mocking can be very helpful when appropriate.
|
||||
- When mocking, you can use the `Pest\Laravel\mock` Pest function, but always import it via `use function Pest\Laravel\mock;` before using it. Alternatively, you can use `$this->mock()` if existing tests do.
|
||||
- You can also create partial mocks using the same import or self method.
|
||||
|
||||
### Datasets
|
||||
- Use datasets in Pest to simplify tests which have a lot of duplicated data. This is often the case when testing validation rules, so consider going with this solution when writing tests for validation rules.
|
||||
|
||||
<code-snippet name="Pest Dataset Example" lang="php">
|
||||
it('has emails', function (string $email) {
|
||||
expect($email)->not->toBeEmpty();
|
||||
})->with([
|
||||
'james' => 'james@laravel.com',
|
||||
'taylor' => 'taylor@laravel.com',
|
||||
]);
|
||||
</code-snippet>
|
||||
|
||||
|
||||
=== pest/v4 rules ===
|
||||
|
||||
## Pest 4
|
||||
|
||||
- Pest v4 is a huge upgrade to Pest and offers: browser testing, smoke testing, visual regression testing, test sharding, and faster type coverage.
|
||||
- Browser testing is incredibly powerful and useful for this project.
|
||||
- Browser tests should live in `tests/Browser/`.
|
||||
- Use the `search-docs` tool for detailed guidance on utilizing these features.
|
||||
|
||||
### Browser Testing
|
||||
- You can use Laravel features like `Event::fake()`, `assertAuthenticated()`, and model factories within Pest v4 browser tests, as well as `RefreshDatabase` (when needed) to ensure a clean state for each test.
|
||||
- Interact with the page (click, type, scroll, select, submit, drag-and-drop, touch gestures, etc.) when appropriate to complete the test.
|
||||
- If requested, test on multiple browsers (Chrome, Firefox, Safari).
|
||||
- If requested, test on different devices and viewports (like iPhone 14 Pro, tablets, or custom breakpoints).
|
||||
- Switch color schemes (light/dark mode) when appropriate.
|
||||
- Take screenshots or pause tests for debugging when appropriate.
|
||||
|
||||
### Example Tests
|
||||
|
||||
<code-snippet name="Pest Browser Test Example" lang="php">
|
||||
it('may reset the password', function () {
|
||||
Notification::fake();
|
||||
|
||||
$this->actingAs(User::factory()->create());
|
||||
|
||||
$page = visit('/sign-in'); // Visit on a real browser...
|
||||
|
||||
$page->assertSee('Sign In')
|
||||
->assertNoJavascriptErrors() // or ->assertNoConsoleLogs()
|
||||
->click('Forgot Password?')
|
||||
->fill('email', 'nuno@laravel.com')
|
||||
->click('Send Reset Link')
|
||||
->assertSee('We have emailed your password reset link!')
|
||||
|
||||
Notification::assertSent(ResetPassword::class);
|
||||
});
|
||||
</code-snippet>
|
||||
|
||||
<code-snippet name="Pest Smoke Testing Example" lang="php">
|
||||
$pages = visit(['/', '/about', '/contact']);
|
||||
|
||||
$pages->assertNoJavascriptErrors()->assertNoConsoleLogs();
|
||||
</code-snippet>
|
||||
|
||||
|
||||
=== inertia-react/core rules ===
|
||||
|
||||
# Inertia + React
|
||||
## Inertia + React
|
||||
|
||||
- Use `router.visit()` or `<Link>` for navigation instead of traditional links.
|
||||
|
||||
<code-snippet name="Inertia Client Navigation" lang="react">
|
||||
|
||||
import { Link } from '@inertiajs/react'
|
||||
<Link href="/">Home</Link>
|
||||
|
||||
</code-snippet>
|
||||
|
||||
|
||||
=== inertia-react/v2/forms rules ===
|
||||
|
||||
## Inertia + React Forms
|
||||
|
||||
<code-snippet name="`<Form>` Component Example" lang="react">
|
||||
|
||||
import { Form } from '@inertiajs/react'
|
||||
|
||||
export default () => (
|
||||
<Form action="/users" method="post">
|
||||
{({
|
||||
errors,
|
||||
hasErrors,
|
||||
processing,
|
||||
wasSuccessful,
|
||||
recentlySuccessful,
|
||||
clearErrors,
|
||||
resetAndClearErrors,
|
||||
defaults
|
||||
}) => (
|
||||
<>
|
||||
<input type="text" name="name" />
|
||||
|
||||
{errors.name && <div>{errors.name}</div>}
|
||||
|
||||
<button type="submit" disabled={processing}>
|
||||
{processing ? 'Creating...' : 'Create User'}
|
||||
</button>
|
||||
|
||||
{wasSuccessful && <div>User created successfully!</div>}
|
||||
</>
|
||||
)}
|
||||
</Form>
|
||||
)
|
||||
|
||||
</code-snippet>
|
||||
|
||||
- IMPORTANT: Activate `inertia-react-development` when working with Inertia React client-side patterns.
|
||||
|
||||
=== tailwindcss/core rules ===
|
||||
|
||||
# Tailwind CSS
|
||||
## 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.
|
||||
|
||||
<code-snippet name="Valid Flex Gap Spacing Example" lang="html">
|
||||
<div class="flex gap-8">
|
||||
<div>Superior</div>
|
||||
<div>Michigan</div>
|
||||
<div>Erie</div>
|
||||
</div>
|
||||
</code-snippet>
|
||||
|
||||
|
||||
### 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/v4 rules ===
|
||||
|
||||
## Tailwind 4
|
||||
|
||||
- Always use Tailwind CSS v4 - do not use the deprecated utilities.
|
||||
- `corePlugins` is not supported in Tailwind v4.
|
||||
- In Tailwind v4, configuration is CSS-first using the `@theme` directive — no separate `tailwind.config.js` file is needed.
|
||||
<code-snippet name="Extending Theme in CSS" lang="css">
|
||||
@theme {
|
||||
--color-brand: oklch(0.72 0.11 178);
|
||||
}
|
||||
</code-snippet>
|
||||
|
||||
- In Tailwind v4, you import Tailwind using a regular CSS `@import` statement, not using the `@tailwind` directives used in v3:
|
||||
|
||||
<code-snippet name="Tailwind v4 Import Tailwind Diff" lang="diff">
|
||||
- @tailwind base;
|
||||
- @tailwind components;
|
||||
- @tailwind utilities;
|
||||
+ @import "tailwindcss";
|
||||
</code-snippet>
|
||||
|
||||
|
||||
### Replaced Utilities
|
||||
- Tailwind v4 removed deprecated utilities. Do not use the deprecated option - use the replacement.
|
||||
- Opacity values are still numeric.
|
||||
|
||||
| Deprecated | Replacement |
|
||||
|------------+--------------|
|
||||
| bg-opacity-* | bg-black/* |
|
||||
| text-opacity-* | text-black/* |
|
||||
| border-opacity-* | border-black/* |
|
||||
| divide-opacity-* | divide-black/* |
|
||||
| ring-opacity-* | ring-black/* |
|
||||
| placeholder-opacity-* | placeholder-black/* |
|
||||
| flex-shrink-* | shrink-* |
|
||||
| flex-grow-* | grow-* |
|
||||
| overflow-ellipsis | text-ellipsis |
|
||||
| decoration-slice | box-decoration-slice |
|
||||
| decoration-clone | box-decoration-clone |
|
||||
|
||||
|
||||
=== tests rules ===
|
||||
|
||||
## Test Enforcement
|
||||
|
||||
- Every change must be programmatically tested. Write a new test or update an existing test, then run the affected tests to make sure they pass.
|
||||
- Run the minimum number of tests needed to ensure code quality and speed. Use `php artisan test` with a specific filename or filter.
|
||||
|
||||
- Always use existing Tailwind conventions; check project patterns before adding new ones.
|
||||
- IMPORTANT: Always use `search-docs` tool for version-specific Tailwind CSS documentation and updated code examples. Never rely on training data.
|
||||
- IMPORTANT: Activate `tailwindcss-development` every time you're working with a Tailwind CSS or styling-related task.
|
||||
|
||||
=== laravel/fortify rules ===
|
||||
|
||||
|
|
@ -308,20 +526,17 @@ Fortify is a headless authentication backend that provides authentication routes
|
|||
**Before implementing any authentication features, use the `search-docs` tool to get the latest docs for that specific feature.**
|
||||
|
||||
### Configuration & Setup
|
||||
|
||||
- Check `config/fortify.php` to see what's enabled. Use `search-docs` for detailed information on specific features.
|
||||
- Enable features by adding them to the `'features' => []` array: `Features::registration()`, `Features::resetPasswords()`, etc.
|
||||
- To see the all Fortify registered routes, use the `list-routes` tool with the `only_vendor: true` and `action: "Fortify"` parameters.
|
||||
- Fortify includes view routes by default (login, register). Set `'views' => false` in the configuration file to disable them if you're handling views yourself.
|
||||
|
||||
### Customization
|
||||
|
||||
- Views can be customized in `FortifyServiceProvider`'s `boot()` method using `Fortify::loginView()`, `Fortify::registerView()`, etc.
|
||||
- Customize authentication logic with `Fortify::authenticateUsing()` for custom user retrieval / validation.
|
||||
- Actions in `app/Actions/Fortify/` handle business logic (user creation, password reset, etc.). They're fully customizable, so you can modify them to change feature behavior.
|
||||
|
||||
## Available Features
|
||||
|
||||
- `Features::registration()` for user registration.
|
||||
- `Features::emailVerification()` to verify new user emails.
|
||||
- `Features::twoFactorAuthentication()` for 2FA with QR codes and recovery codes.
|
||||
|
|
|
|||
|
|
@ -1,369 +0,0 @@
|
|||
---
|
||||
name: inertia-react-development
|
||||
description: >-
|
||||
Develops Inertia.js v2 React client-side applications. Activates when creating
|
||||
React pages, forms, or navigation; using <Link>, <Form>, useForm, or router;
|
||||
working with deferred props, prefetching, or polling; or when user mentions
|
||||
React with Inertia, React pages, React forms, or React navigation.
|
||||
---
|
||||
|
||||
# Inertia React Development
|
||||
|
||||
## When to Apply
|
||||
|
||||
Activate this skill when:
|
||||
|
||||
- Creating or modifying React page components for Inertia
|
||||
- Working with forms in React (using `<Form>` or `useForm`)
|
||||
- Implementing client-side navigation with `<Link>` or `router`
|
||||
- Using v2 features: deferred props, prefetching, or polling
|
||||
- Building React-specific features with the Inertia protocol
|
||||
|
||||
## Documentation
|
||||
|
||||
Use `search-docs` for detailed Inertia v2 React patterns and documentation.
|
||||
|
||||
## Basic Usage
|
||||
|
||||
### Page Components Location
|
||||
|
||||
React page components should be placed in the `resources/js/Pages` directory.
|
||||
|
||||
### Page Component Structure
|
||||
|
||||
<code-snippet name="Basic React Page Component" lang="react">
|
||||
|
||||
export default function UsersIndex({ users }) {
|
||||
return (
|
||||
<div>
|
||||
<h1>Users</h1>
|
||||
<ul>
|
||||
{users.map(user => <li key={user.id}>{user.name}</li>)}
|
||||
</ul>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
</code-snippet>
|
||||
|
||||
## Client-Side Navigation
|
||||
|
||||
### Basic Link Component
|
||||
|
||||
Use `<Link>` for client-side navigation instead of traditional `<a>` tags:
|
||||
|
||||
<code-snippet name="Inertia React Navigation" lang="react">
|
||||
|
||||
import { Link, router } from '@inertiajs/react'
|
||||
|
||||
<Link href="/">Home</Link>
|
||||
<Link href="/users">Users</Link>
|
||||
<Link href={`/users/${user.id}`}>View User</Link>
|
||||
|
||||
</code-snippet>
|
||||
|
||||
### Link with Method
|
||||
|
||||
<code-snippet name="Link with POST Method" lang="react">
|
||||
|
||||
import { Link } from '@inertiajs/react'
|
||||
|
||||
<Link href="/logout" method="post" as="button">
|
||||
Logout
|
||||
</Link>
|
||||
|
||||
</code-snippet>
|
||||
|
||||
### Prefetching
|
||||
|
||||
Prefetch pages to improve perceived performance:
|
||||
|
||||
<code-snippet name="Prefetch on Hover" lang="react">
|
||||
|
||||
import { Link } from '@inertiajs/react'
|
||||
|
||||
<Link href="/users" prefetch>
|
||||
Users
|
||||
</Link>
|
||||
|
||||
</code-snippet>
|
||||
|
||||
### Programmatic Navigation
|
||||
|
||||
<code-snippet name="Router Visit" lang="react">
|
||||
|
||||
import { router } from '@inertiajs/react'
|
||||
|
||||
function handleClick() {
|
||||
router.visit('/users')
|
||||
}
|
||||
|
||||
// Or with options
|
||||
router.visit('/users', {
|
||||
method: 'post',
|
||||
data: { name: 'John' },
|
||||
onSuccess: () => console.log('Success!'),
|
||||
})
|
||||
|
||||
</code-snippet>
|
||||
|
||||
## Form Handling
|
||||
|
||||
### Form Component (Recommended)
|
||||
|
||||
The recommended way to build forms is with the `<Form>` component:
|
||||
|
||||
<code-snippet name="Form Component Example" lang="react">
|
||||
|
||||
import { Form } from '@inertiajs/react'
|
||||
|
||||
export default function CreateUser() {
|
||||
return (
|
||||
<Form action="/users" method="post">
|
||||
{({ errors, processing, wasSuccessful }) => (
|
||||
<>
|
||||
<input type="text" name="name" />
|
||||
{errors.name && <div>{errors.name}</div>}
|
||||
|
||||
<input type="email" name="email" />
|
||||
{errors.email && <div>{errors.email}</div>}
|
||||
|
||||
<button type="submit" disabled={processing}>
|
||||
{processing ? 'Creating...' : 'Create User'}
|
||||
</button>
|
||||
|
||||
{wasSuccessful && <div>User created!</div>}
|
||||
</>
|
||||
)}
|
||||
</Form>
|
||||
)
|
||||
}
|
||||
|
||||
</code-snippet>
|
||||
|
||||
### Form Component With All Props
|
||||
|
||||
<code-snippet name="Form Component Full Example" lang="react">
|
||||
|
||||
import { Form } from '@inertiajs/react'
|
||||
|
||||
<Form action="/users" method="post">
|
||||
{({
|
||||
errors,
|
||||
hasErrors,
|
||||
processing,
|
||||
progress,
|
||||
wasSuccessful,
|
||||
recentlySuccessful,
|
||||
clearErrors,
|
||||
resetAndClearErrors,
|
||||
defaults,
|
||||
isDirty,
|
||||
reset,
|
||||
submit
|
||||
}) => (
|
||||
<>
|
||||
<input type="text" name="name" defaultValue={defaults.name} />
|
||||
{errors.name && <div>{errors.name}</div>}
|
||||
|
||||
<button type="submit" disabled={processing}>
|
||||
{processing ? 'Saving...' : 'Save'}
|
||||
</button>
|
||||
|
||||
{progress && (
|
||||
<progress value={progress.percentage} max="100">
|
||||
{progress.percentage}%
|
||||
</progress>
|
||||
)}
|
||||
|
||||
{wasSuccessful && <div>Saved!</div>}
|
||||
</>
|
||||
)}
|
||||
</Form>
|
||||
|
||||
</code-snippet>
|
||||
|
||||
### Form Component Reset Props
|
||||
|
||||
The `<Form>` component supports automatic resetting:
|
||||
|
||||
- `resetOnError` - Reset form data when the request fails
|
||||
- `resetOnSuccess` - Reset form data when the request succeeds
|
||||
- `setDefaultsOnSuccess` - Update default values on success
|
||||
|
||||
Use the `search-docs` tool with a query of `form component resetting` for detailed guidance.
|
||||
|
||||
<code-snippet name="Form with Reset Props" lang="react">
|
||||
|
||||
import { Form } from '@inertiajs/react'
|
||||
|
||||
<Form
|
||||
action="/users"
|
||||
method="post"
|
||||
resetOnSuccess
|
||||
setDefaultsOnSuccess
|
||||
>
|
||||
{({ errors, processing, wasSuccessful }) => (
|
||||
<>
|
||||
<input type="text" name="name" />
|
||||
{errors.name && <div>{errors.name}</div>}
|
||||
|
||||
<button type="submit" disabled={processing}>
|
||||
Submit
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</Form>
|
||||
|
||||
</code-snippet>
|
||||
|
||||
Forms can also be built using the `useForm` helper for more programmatic control. Use the `search-docs` tool with a query of `useForm helper` for guidance.
|
||||
|
||||
### `useForm` Hook
|
||||
|
||||
For more programmatic control or to follow existing conventions, use the `useForm` hook:
|
||||
|
||||
<code-snippet name="useForm Hook Example" lang="react">
|
||||
|
||||
import { useForm } from '@inertiajs/react'
|
||||
|
||||
export default function CreateUser() {
|
||||
const { data, setData, post, processing, errors, reset } = useForm({
|
||||
name: '',
|
||||
email: '',
|
||||
password: '',
|
||||
})
|
||||
|
||||
function submit(e) {
|
||||
e.preventDefault()
|
||||
post('/users', {
|
||||
onSuccess: () => reset('password'),
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<form onSubmit={submit}>
|
||||
<input
|
||||
type="text"
|
||||
value={data.name}
|
||||
onChange={e => setData('name', e.target.value)}
|
||||
/>
|
||||
{errors.name && <div>{errors.name}</div>}
|
||||
|
||||
<input
|
||||
type="email"
|
||||
value={data.email}
|
||||
onChange={e => setData('email', e.target.value)}
|
||||
/>
|
||||
{errors.email && <div>{errors.email}</div>}
|
||||
|
||||
<input
|
||||
type="password"
|
||||
value={data.password}
|
||||
onChange={e => setData('password', e.target.value)}
|
||||
/>
|
||||
{errors.password && <div>{errors.password}</div>}
|
||||
|
||||
<button type="submit" disabled={processing}>
|
||||
Create User
|
||||
</button>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
|
||||
</code-snippet>
|
||||
|
||||
## Inertia v2 Features
|
||||
|
||||
### Deferred Props
|
||||
|
||||
Use deferred props to load data after initial page render:
|
||||
|
||||
<code-snippet name="Deferred Props with Empty State" lang="react">
|
||||
|
||||
export default function UsersIndex({ users }) {
|
||||
// users will be undefined initially, then populated
|
||||
return (
|
||||
<div>
|
||||
<h1>Users</h1>
|
||||
{!users ? (
|
||||
<div className="animate-pulse">
|
||||
<div className="h-4 bg-gray-200 rounded w-3/4 mb-2"></div>
|
||||
<div className="h-4 bg-gray-200 rounded w-1/2"></div>
|
||||
</div>
|
||||
) : (
|
||||
<ul>
|
||||
{users.map(user => (
|
||||
<li key={user.id}>{user.name}</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
</code-snippet>
|
||||
|
||||
### Polling
|
||||
|
||||
Automatically refresh data at intervals:
|
||||
|
||||
<code-snippet name="Polling Example" lang="react">
|
||||
|
||||
import { router } from '@inertiajs/react'
|
||||
import { useEffect } from 'react'
|
||||
|
||||
export default function Dashboard({ stats }) {
|
||||
useEffect(() => {
|
||||
const interval = setInterval(() => {
|
||||
router.reload({ only: ['stats'] })
|
||||
}, 5000) // Poll every 5 seconds
|
||||
|
||||
return () => clearInterval(interval)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1>Dashboard</h1>
|
||||
<div>Active Users: {stats.activeUsers}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
</code-snippet>
|
||||
|
||||
### WhenVisible (Infinite Scroll)
|
||||
|
||||
Load more data when user scrolls to a specific element:
|
||||
|
||||
<code-snippet name="Infinite Scroll with WhenVisible" lang="react">
|
||||
|
||||
import { WhenVisible } from '@inertiajs/react'
|
||||
|
||||
export default function UsersList({ users }) {
|
||||
return (
|
||||
<div>
|
||||
{users.data.map(user => (
|
||||
<div key={user.id}>{user.name}</div>
|
||||
))}
|
||||
|
||||
{users.next_page_url && (
|
||||
<WhenVisible
|
||||
data="users"
|
||||
params={{ page: users.current_page + 1 }}
|
||||
fallback={<div>Loading more...</div>}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
</code-snippet>
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
- Using traditional `<a>` links instead of Inertia's `<Link>` component (breaks SPA behavior)
|
||||
- Forgetting to add loading states (skeleton screens) when using deferred props
|
||||
- Not handling the `undefined` state of deferred props before data loads
|
||||
- Using `<form>` without preventing default submission (use `<Form>` component or `e.preventDefault()`)
|
||||
- Forgetting to check if `<Form>` component is available in your Inertia version
|
||||
|
|
@ -1,74 +0,0 @@
|
|||
---
|
||||
name: pennant-development
|
||||
description: >-
|
||||
Manages feature flags with Laravel Pennant. Activates when creating, checking, or toggling
|
||||
feature flags; showing or hiding features conditionally; implementing A/B testing; working with
|
||||
@feature directive; or when the user mentions feature flags, feature toggles, Pennant, conditional
|
||||
features, rollouts, or gradually enabling features.
|
||||
---
|
||||
|
||||
# Pennant Features
|
||||
|
||||
## When to Apply
|
||||
|
||||
Activate this skill when:
|
||||
|
||||
- Creating or checking feature flags
|
||||
- Managing feature rollouts
|
||||
- Implementing A/B testing
|
||||
|
||||
## Documentation
|
||||
|
||||
Use `search-docs` for detailed Pennant patterns and documentation.
|
||||
|
||||
## Basic Usage
|
||||
|
||||
### Defining Features
|
||||
|
||||
<code-snippet name="Defining Features" lang="php">
|
||||
use Laravel\Pennant\Feature;
|
||||
|
||||
Feature::define('new-dashboard', function (User $user) {
|
||||
return $user->isAdmin();
|
||||
});
|
||||
</code-snippet>
|
||||
|
||||
### Checking Features
|
||||
|
||||
<code-snippet name="Checking Features" lang="php">
|
||||
if (Feature::active('new-dashboard')) {
|
||||
// Feature is active
|
||||
}
|
||||
|
||||
// With scope
|
||||
if (Feature::for($user)->active('new-dashboard')) {
|
||||
// Feature is active for this user
|
||||
}
|
||||
</code-snippet>
|
||||
|
||||
### Blade Directive
|
||||
|
||||
<code-snippet name="Blade Directive" lang="blade">
|
||||
@feature('new-dashboard')
|
||||
<x-new-dashboard />
|
||||
@else
|
||||
<x-old-dashboard />
|
||||
@endfeature
|
||||
</code-snippet>
|
||||
|
||||
### Activating / Deactivating
|
||||
|
||||
<code-snippet name="Activating Features" lang="php">
|
||||
Feature::activate('new-dashboard');
|
||||
Feature::for($user)->activate('new-dashboard');
|
||||
</code-snippet>
|
||||
|
||||
## Verification
|
||||
|
||||
1. Check feature flag is defined
|
||||
2. Test with different scopes/users
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
- Forgetting to scope features for specific users/entities
|
||||
- Not following existing naming conventions
|
||||
|
|
@ -1,174 +0,0 @@
|
|||
---
|
||||
name: pest-testing
|
||||
description: >-
|
||||
Tests applications using the Pest 4 PHP framework. Activates when writing tests, creating unit or feature
|
||||
tests, adding assertions, testing Livewire components, browser testing, debugging test failures,
|
||||
working with datasets or mocking; or when the user mentions test, spec, TDD, expects, assertion,
|
||||
coverage, or needs to verify functionality works.
|
||||
---
|
||||
|
||||
# Pest Testing 4
|
||||
|
||||
## When to Apply
|
||||
|
||||
Activate this skill when:
|
||||
|
||||
- Creating new tests (unit, feature, or browser)
|
||||
- Modifying existing tests
|
||||
- Debugging test failures
|
||||
- Working with browser testing or smoke testing
|
||||
- Writing architecture tests or visual regression tests
|
||||
|
||||
## Documentation
|
||||
|
||||
Use `search-docs` for detailed Pest 4 patterns and documentation.
|
||||
|
||||
## Basic Usage
|
||||
|
||||
### Creating Tests
|
||||
|
||||
All tests must be written using Pest. Use `php artisan make:test --pest {name}`.
|
||||
|
||||
### Test Organization
|
||||
|
||||
- Unit/Feature tests: `tests/Feature` and `tests/Unit` directories.
|
||||
- Browser tests: `tests/Browser/` directory.
|
||||
- Do NOT remove tests without approval - these are core application code.
|
||||
|
||||
### Basic Test Structure
|
||||
|
||||
<code-snippet name="Basic Pest Test Example" lang="php">
|
||||
|
||||
it('is true', function () {
|
||||
expect(true)->toBeTrue();
|
||||
});
|
||||
|
||||
</code-snippet>
|
||||
|
||||
### Running Tests
|
||||
|
||||
- Run minimal tests with filter before finalizing: `php artisan test --compact --filter=testName`.
|
||||
- Run all tests: `php artisan test --compact`.
|
||||
- Run file: `php artisan test --compact tests/Feature/ExampleTest.php`.
|
||||
|
||||
## Assertions
|
||||
|
||||
Use specific assertions (`assertSuccessful()`, `assertNotFound()`) instead of `assertStatus()`:
|
||||
|
||||
<code-snippet name="Pest Response Assertion" lang="php">
|
||||
|
||||
it('returns all', function () {
|
||||
$this->postJson('/api/docs', [])->assertSuccessful();
|
||||
});
|
||||
|
||||
</code-snippet>
|
||||
|
||||
| Use | Instead of |
|
||||
|-----|------------|
|
||||
| `assertSuccessful()` | `assertStatus(200)` |
|
||||
| `assertNotFound()` | `assertStatus(404)` |
|
||||
| `assertForbidden()` | `assertStatus(403)` |
|
||||
|
||||
## Mocking
|
||||
|
||||
Import mock function before use: `use function Pest\Laravel\mock;`
|
||||
|
||||
## Datasets
|
||||
|
||||
Use datasets for repetitive tests (validation rules, etc.):
|
||||
|
||||
<code-snippet name="Pest Dataset Example" lang="php">
|
||||
|
||||
it('has emails', function (string $email) {
|
||||
expect($email)->not->toBeEmpty();
|
||||
})->with([
|
||||
'james' => 'james@laravel.com',
|
||||
'taylor' => 'taylor@laravel.com',
|
||||
]);
|
||||
|
||||
</code-snippet>
|
||||
|
||||
## Pest 4 Features
|
||||
|
||||
| Feature | Purpose |
|
||||
|---------|---------|
|
||||
| Browser Testing | Full integration tests in real browsers |
|
||||
| Smoke Testing | Validate multiple pages quickly |
|
||||
| Visual Regression | Compare screenshots for visual changes |
|
||||
| Test Sharding | Parallel CI runs |
|
||||
| Architecture Testing | Enforce code conventions |
|
||||
|
||||
### Browser Test Example
|
||||
|
||||
Browser tests run in real browsers for full integration testing:
|
||||
|
||||
- Browser tests live in `tests/Browser/`.
|
||||
- Use Laravel features like `Event::fake()`, `assertAuthenticated()`, and model factories.
|
||||
- Use `RefreshDatabase` for clean state per test.
|
||||
- Interact with page: click, type, scroll, select, submit, drag-and-drop, touch gestures.
|
||||
- Test on multiple browsers (Chrome, Firefox, Safari) if requested.
|
||||
- Test on different devices/viewports (iPhone 14 Pro, tablets) if requested.
|
||||
- Switch color schemes (light/dark mode) when appropriate.
|
||||
- Take screenshots or pause tests for debugging.
|
||||
|
||||
<code-snippet name="Pest Browser Test Example" lang="php">
|
||||
|
||||
it('may reset the password', function () {
|
||||
Notification::fake();
|
||||
|
||||
$this->actingAs(User::factory()->create());
|
||||
|
||||
$page = visit('/sign-in');
|
||||
|
||||
$page->assertSee('Sign In')
|
||||
->assertNoJavascriptErrors()
|
||||
->click('Forgot Password?')
|
||||
->fill('email', 'nuno@laravel.com')
|
||||
->click('Send Reset Link')
|
||||
->assertSee('We have emailed your password reset link!');
|
||||
|
||||
Notification::assertSent(ResetPassword::class);
|
||||
});
|
||||
|
||||
</code-snippet>
|
||||
|
||||
### Smoke Testing
|
||||
|
||||
Quickly validate multiple pages have no JavaScript errors:
|
||||
|
||||
<code-snippet name="Pest Smoke Testing Example" lang="php">
|
||||
|
||||
$pages = visit(['/', '/about', '/contact']);
|
||||
|
||||
$pages->assertNoJavascriptErrors()->assertNoConsoleLogs();
|
||||
|
||||
</code-snippet>
|
||||
|
||||
### Visual Regression Testing
|
||||
|
||||
Capture and compare screenshots to detect visual changes.
|
||||
|
||||
### Test Sharding
|
||||
|
||||
Split tests across parallel processes for faster CI runs.
|
||||
|
||||
### Architecture Testing
|
||||
|
||||
Pest 4 includes architecture testing (from Pest 3):
|
||||
|
||||
<code-snippet name="Architecture Test Example" lang="php">
|
||||
|
||||
arch('controllers')
|
||||
->expect('App\Http\Controllers')
|
||||
->toExtendNothing()
|
||||
->toHaveSuffix('Controller');
|
||||
|
||||
</code-snippet>
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
- Not importing `use function Pest\Laravel\mock;` before using mock
|
||||
- Using `assertStatus(200)` instead of `assertSuccessful()`
|
||||
- Forgetting datasets for repetitive validation tests
|
||||
- Deleting tests without approval
|
||||
- Forgetting `assertNoJavascriptErrors()` in browser tests
|
||||
|
|
@ -1,124 +0,0 @@
|
|||
---
|
||||
name: tailwindcss-development
|
||||
description: >-
|
||||
Styles applications using Tailwind CSS v4 utilities. Activates when adding styles, restyling components,
|
||||
working with gradients, spacing, layout, flex, grid, responsive design, dark mode, colors,
|
||||
typography, or borders; or when the user mentions CSS, styling, classes, Tailwind, restyle,
|
||||
hero section, cards, buttons, or any visual/UI changes.
|
||||
---
|
||||
|
||||
# Tailwind CSS Development
|
||||
|
||||
## When to Apply
|
||||
|
||||
Activate this skill when:
|
||||
|
||||
- Adding styles to components or pages
|
||||
- Working with responsive design
|
||||
- Implementing dark mode
|
||||
- Extracting repeated patterns into components
|
||||
- Debugging spacing or layout issues
|
||||
|
||||
## Documentation
|
||||
|
||||
Use `search-docs` for detailed Tailwind CSS v4 patterns and documentation.
|
||||
|
||||
## Basic Usage
|
||||
|
||||
- Use Tailwind CSS classes to style HTML. Check and follow existing Tailwind conventions in the project before introducing new patterns.
|
||||
- Offer to extract repeated patterns into components that match the project's conventions (e.g., Blade, JSX, Vue).
|
||||
- Consider class placement, order, priority, and defaults. Remove redundant classes, add classes to parent or child elements carefully to reduce repetition, and group elements logically.
|
||||
|
||||
## Tailwind CSS v4 Specifics
|
||||
|
||||
- Always use Tailwind CSS v4 and avoid deprecated utilities.
|
||||
- `corePlugins` is not supported in Tailwind v4.
|
||||
|
||||
### CSS-First Configuration
|
||||
|
||||
In Tailwind v4, configuration is CSS-first using the `@theme` directive — no separate `tailwind.config.js` file is needed:
|
||||
|
||||
<code-snippet name="CSS-First Config" lang="css">
|
||||
@theme {
|
||||
--color-brand: oklch(0.72 0.11 178);
|
||||
}
|
||||
</code-snippet>
|
||||
|
||||
### Import Syntax
|
||||
|
||||
In Tailwind v4, import Tailwind with a regular CSS `@import` statement instead of the `@tailwind` directives used in v3:
|
||||
|
||||
<code-snippet name="v4 Import Syntax" lang="diff">
|
||||
- @tailwind base;
|
||||
- @tailwind components;
|
||||
- @tailwind utilities;
|
||||
+ @import "tailwindcss";
|
||||
</code-snippet>
|
||||
|
||||
### Replaced Utilities
|
||||
|
||||
Tailwind v4 removed deprecated utilities. Use the replacements shown below. Opacity values remain numeric.
|
||||
|
||||
| Deprecated | Replacement |
|
||||
|------------|-------------|
|
||||
| bg-opacity-* | bg-black/* |
|
||||
| text-opacity-* | text-black/* |
|
||||
| border-opacity-* | border-black/* |
|
||||
| divide-opacity-* | divide-black/* |
|
||||
| ring-opacity-* | ring-black/* |
|
||||
| placeholder-opacity-* | placeholder-black/* |
|
||||
| flex-shrink-* | shrink-* |
|
||||
| flex-grow-* | grow-* |
|
||||
| overflow-ellipsis | text-ellipsis |
|
||||
| decoration-slice | box-decoration-slice |
|
||||
| decoration-clone | box-decoration-clone |
|
||||
|
||||
## Spacing
|
||||
|
||||
Use `gap` utilities instead of margins for spacing between siblings:
|
||||
|
||||
<code-snippet name="Gap Utilities" lang="html">
|
||||
<div class="flex gap-8">
|
||||
<div>Item 1</div>
|
||||
<div>Item 2</div>
|
||||
</div>
|
||||
</code-snippet>
|
||||
|
||||
## Dark Mode
|
||||
|
||||
If existing pages and components support dark mode, new pages and components must support it the same way, typically using the `dark:` variant:
|
||||
|
||||
<code-snippet name="Dark Mode" lang="html">
|
||||
<div class="bg-white dark:bg-gray-900 text-gray-900 dark:text-white">
|
||||
Content adapts to color scheme
|
||||
</div>
|
||||
</code-snippet>
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Flexbox Layout
|
||||
|
||||
<code-snippet name="Flexbox Layout" lang="html">
|
||||
<div class="flex items-center justify-between gap-4">
|
||||
<div>Left content</div>
|
||||
<div>Right content</div>
|
||||
</div>
|
||||
</code-snippet>
|
||||
|
||||
### Grid Layout
|
||||
|
||||
<code-snippet name="Grid Layout" lang="html">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
<div>Card 1</div>
|
||||
<div>Card 2</div>
|
||||
<div>Card 3</div>
|
||||
</div>
|
||||
</code-snippet>
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
- Using deprecated v3 utilities (bg-opacity-*, flex-shrink-*, etc.)
|
||||
- Using `@tailwind` directives instead of `@import "tailwindcss"`
|
||||
- Trying to use `tailwind.config.js` instead of CSS `@theme` directive
|
||||
- Using margins for spacing between siblings instead of gap utilities
|
||||
- Forgetting to add dark mode variants when the project uses dark mode
|
||||
|
|
@ -1,89 +0,0 @@
|
|||
---
|
||||
name: wayfinder-development
|
||||
description: >-
|
||||
Activates whenever referencing backend routes in frontend components. Use when
|
||||
importing from @/actions or @/routes, calling Laravel routes from TypeScript,
|
||||
or working with Wayfinder route functions.
|
||||
---
|
||||
|
||||
# Wayfinder Development
|
||||
|
||||
## When to Apply
|
||||
|
||||
Activate whenever referencing backend routes in frontend components:
|
||||
- Importing from `@/actions/` or `@/routes/`
|
||||
- Calling Laravel routes from TypeScript/JavaScript
|
||||
- Creating links or navigation to backend endpoints
|
||||
|
||||
## Documentation
|
||||
|
||||
Use `search-docs` for detailed Wayfinder patterns and documentation.
|
||||
|
||||
## Quick Reference
|
||||
|
||||
### Generate Routes
|
||||
|
||||
Run after route changes if Vite plugin isn't installed:
|
||||
|
||||
php artisan wayfinder:generate --no-interaction
|
||||
|
||||
For form helpers, use `--with-form` flag:
|
||||
|
||||
php artisan wayfinder:generate --with-form --no-interaction
|
||||
|
||||
### Import Patterns
|
||||
|
||||
<code-snippet name="Controller Action Imports" lang="typescript">
|
||||
|
||||
// Named imports for tree-shaking (preferred)...
|
||||
import { show, store, update } from '@/actions/App/Http/Controllers/PostController'
|
||||
|
||||
// Named route imports...
|
||||
import { show as postShow } from '@/routes/post'
|
||||
|
||||
</code-snippet>
|
||||
|
||||
### Common Methods
|
||||
|
||||
<code-snippet name="Wayfinder Methods" lang="typescript">
|
||||
|
||||
// Get route object...
|
||||
show(1) // { url: "/posts/1", method: "get" }
|
||||
|
||||
// Get URL string...
|
||||
show.url(1) // "/posts/1"
|
||||
|
||||
// Specific HTTP methods...
|
||||
show.get(1)
|
||||
store.post()
|
||||
update.patch(1)
|
||||
destroy.delete(1)
|
||||
|
||||
// Form attributes for HTML forms...
|
||||
store.form() // { action: "/posts", method: "post" }
|
||||
|
||||
// Query parameters...
|
||||
show(1, { query: { page: 1 } }) // "/posts/1?page=1"
|
||||
|
||||
</code-snippet>
|
||||
|
||||
## Wayfinder + Inertia
|
||||
|
||||
Use Wayfinder with the `<Form>` component:
|
||||
<code-snippet name="Wayfinder Form (React)" lang="typescript">
|
||||
|
||||
<Form {...store.form()}><input name="title" /></Form>
|
||||
|
||||
</code-snippet>
|
||||
|
||||
## Verification
|
||||
|
||||
1. Run `php artisan wayfinder:generate` to regenerate routes if Vite plugin isn't installed
|
||||
2. Check TypeScript imports resolve correctly
|
||||
3. Verify route URLs match expected paths
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
- Using default imports instead of named imports (breaks tree-shaking)
|
||||
- Forgetting to regenerate after route changes
|
||||
- Not using type-safe parameter objects for route model binding
|
||||
|
|
@ -1,32 +0,0 @@
|
|||
.git
|
||||
.github
|
||||
.agents
|
||||
.claude
|
||||
.cursor
|
||||
.opencode
|
||||
.pi
|
||||
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
!.env.production.example
|
||||
|
||||
node_modules
|
||||
vendor
|
||||
|
||||
storage/app/*
|
||||
storage/framework/cache/*
|
||||
storage/framework/sessions/*
|
||||
storage/framework/views/*
|
||||
storage/logs/*
|
||||
!storage/app/.gitignore
|
||||
!storage/framework/cache/.gitignore
|
||||
!storage/framework/sessions/.gitignore
|
||||
!storage/framework/views/.gitignore
|
||||
!storage/logs/.gitignore
|
||||
|
||||
Dockerfile
|
||||
Dockerfile.*
|
||||
docker-compose*.yml
|
||||
|
||||
screenshots
|
||||
119
.env.example
119
.env.example
|
|
@ -1,25 +1,13 @@
|
|||
APP_NAME="Whisper Money"
|
||||
APP_NAME=Laravel
|
||||
APP_ENV=local
|
||||
APP_KEY=
|
||||
APP_DEBUG=true
|
||||
APP_URL=https://whisper.money.localhost
|
||||
APP_URL=https://whispermoney.test
|
||||
|
||||
APP_LOCALE=en
|
||||
APP_FALLBACK_LOCALE=en
|
||||
APP_FAKER_LOCALE=en_US
|
||||
|
||||
# Passport OAuth signing keys for the MCP OAuth server. Leave blank to use the
|
||||
# keys generated by `php artisan passport:keys` under storage/. Set both (PEM
|
||||
# contents) to share one key pair across multiple app instances.
|
||||
PASSPORT_PRIVATE_KEY=
|
||||
PASSPORT_PUBLIC_KEY=
|
||||
|
||||
# OAuth authorization server host for the MCP OAuth server (RFC 8414 issuer).
|
||||
# Set to a dedicated host OUTSIDE the PWA's manifest scope so mobile OAuth links
|
||||
# (e.g. ChatGPT on Android) open in the browser instead of being captured by the
|
||||
# installed PWA. Leave blank to use the app URL. Production: https://oauth.whisper.money
|
||||
MCP_AUTHORIZATION_SERVER=
|
||||
|
||||
APP_MAINTENANCE_DRIVER=file
|
||||
# APP_MAINTENANCE_STORE=database
|
||||
|
||||
|
|
@ -34,7 +22,7 @@ LOG_LEVEL=debug
|
|||
|
||||
DB_CONNECTION=mysql
|
||||
DB_HOST=127.0.0.1
|
||||
DB_PORT=3307
|
||||
DB_PORT=3306
|
||||
DB_DATABASE=whisper_money
|
||||
DB_USERNAME=root
|
||||
DB_PASSWORD=
|
||||
|
|
@ -54,52 +42,35 @@ CACHE_STORE=database
|
|||
|
||||
MEMCACHED_HOST=127.0.0.1
|
||||
|
||||
REDIS_CLIENT=phpredis
|
||||
REDIS_HOST=127.0.0.1
|
||||
REDIS_PASSWORD=null
|
||||
REDIS_PORT=6380
|
||||
REDIS_PORT=6379
|
||||
|
||||
MAIL_MAILER=smtp
|
||||
MAIL_HOST=127.0.0.1
|
||||
MAIL_PORT=1025
|
||||
MAIL_PORT=2525
|
||||
MAIL_USERNAME=null
|
||||
MAIL_PASSWORD=null
|
||||
MAIL_FROM_ADDRESS="no-reply@whisper.money"
|
||||
MAIL_FROM_ADDRESS="hi@whisper.money"
|
||||
MAIL_FROM_NAME="Whisper Money"
|
||||
MAIL_DRIP_FROM_ADDRESS="hi@whisper.money"
|
||||
MAIL_DRIP_FROM_NAME="Álvaro and Víctor"
|
||||
ADMIN_EMAIL=
|
||||
|
||||
# Resend contact sync (optional, not used for sending email)
|
||||
# Resend Email Service (set MAIL_MAILER=resend to use in production)
|
||||
RESEND_API_KEY=
|
||||
|
||||
# Drip Emails (welcome, onboarding, promo codes, etc.)
|
||||
DRIP_EMAILS_ENABLED=true
|
||||
|
||||
# Email Verification (disable in local to auto-verify new users)
|
||||
EMAIL_VERIFICATION_ENABLED=false
|
||||
|
||||
# Amazon SES Email Service (set MAIL_MAILER=ses to use in production)
|
||||
AWS_ACCESS_KEY_ID=
|
||||
AWS_SECRET_ACCESS_KEY=
|
||||
AWS_DEFAULT_REGION=us-east-1
|
||||
AWS_SESSION_TOKEN=
|
||||
AWS_BUCKET=
|
||||
AWS_USE_PATH_STYLE_ENDPOINT=false
|
||||
|
||||
# Optional. Defaults to true when unset, so existing installs keep public sign-ups.
|
||||
# Set to false to close public sign-ups while keeping login open (invite/admin-created
|
||||
# accounts only): the /register routes return a 403 and every registration CTA
|
||||
# is hidden, while /login stays fully available.
|
||||
# REGISTRATION_ENABLED=false
|
||||
LEAD_REDIRECT_URL=https://google.es
|
||||
HIDE_AUTH_BUTTONS=false
|
||||
|
||||
VITE_APP_NAME="${APP_NAME}"
|
||||
|
||||
# PostHog Analytics
|
||||
VITE_POSTHOG_ENABLED=false
|
||||
VITE_POSTHOG_API_KEY=
|
||||
VITE_POSTHOG_HOST=https://t.whisper.money
|
||||
VITE_POSTHOG_UI_HOST=https://eu.posthog.com
|
||||
|
||||
# Stripe Configuration
|
||||
STRIPE_KEY=
|
||||
STRIPE_SECRET=
|
||||
|
|
@ -107,81 +78,15 @@ STRIPE_WEBHOOK_SECRET=
|
|||
|
||||
# Subscriptions
|
||||
SUBSCRIPTIONS_ENABLED=false
|
||||
# Run `php artisan stripe:sync-prices` to create/update Stripe prices from config.
|
||||
# Override lookup keys only if you need to use different keys per environment.
|
||||
STRIPE_PRO_MONTHLY_LOOKUP_KEY=whisper_pro_monthly
|
||||
STRIPE_PRO_YEARLY_LOOKUP_KEY=whisper_pro_yearly
|
||||
STRIPE_PRO_MONTHLY_PRICE_ID=
|
||||
STRIPE_PRO_YEARLY_PRICE_ID=
|
||||
|
||||
# Sentry / Bugsink Error Tracking
|
||||
SENTRY_LARAVEL_DSN=
|
||||
SENTRY_TRACES_SAMPLE_RATE=1.0
|
||||
SENTRY_PROFILES_SAMPLE_RATE=0
|
||||
SENTRY_ENABLE_LOGS=false
|
||||
|
||||
# Docker Service Ports (forwarded to host)
|
||||
FORWARD_DB_PORT=3307
|
||||
FORWARD_REDIS_PORT=6380
|
||||
FORWARD_MAILHOG_PORT=1025
|
||||
FORWARD_MAILHOG_DASHBOARD_PORT=8025
|
||||
|
||||
# Dev Mode (set to false for full Docker deployment via setup.sh)
|
||||
DEV_MODE=true
|
||||
|
||||
# EnableBanking (Open Banking)
|
||||
ENABLEBANKING_APP_ID=
|
||||
ENABLEBANKING_PRIVATE_KEY_PATH=
|
||||
ENABLEBANKING_REDIRECT_URL="${APP_URL}/open-banking/callback"
|
||||
|
||||
# Demo Account Configuration
|
||||
DEMO_EMAIL=demo@whisper.money
|
||||
DEMO_PASSWORD=demo
|
||||
DEMO_ENCRYPTION_KEY=demo
|
||||
|
||||
# AI features (transaction categorization + rule suggestions, powered by laravel/ai)
|
||||
#
|
||||
# Provider is configurable independently of the model and defaults to Gemini.
|
||||
# Set AI_PROVIDER to switch every AI feature at once (e.g. "ollama" for fully
|
||||
# local, private processing); the per-feature vars below override it. Accepts
|
||||
# any laravel/ai provider ("gemini", "ollama", "openai", ...); an unknown value
|
||||
# fails fast.
|
||||
# AI_PROVIDER=gemini
|
||||
# AI_SUGGESTIONS_PROVIDER=gemini
|
||||
# AI_CATEGORIZATION_PROVIDER=gemini
|
||||
|
||||
# --- Gemini (default provider) ---
|
||||
GEMINI_API_KEY=
|
||||
GEMINI_BASE_URL=
|
||||
# GEMINI_BASE_URL is optional — leave empty to use Google AI Studio
|
||||
# (https://generativelanguage.googleapis.com/v1beta). Only set it for a
|
||||
# proxy or Vertex AI gateway.
|
||||
|
||||
# --- Ollama (local, self-hosted models — no API key needed) ---
|
||||
# Point OLLAMA_URL at your Ollama server and set the model vars below to a
|
||||
# pulled model (e.g. gemma3:12b). OLLAMA_API_KEY is only needed behind an
|
||||
# authenticating proxy.
|
||||
# OLLAMA_URL=http://localhost:11434
|
||||
# OLLAMA_API_KEY=
|
||||
|
||||
# Model is overridable without a deploy; defaults to a Flash-tier Gemini model.
|
||||
# When using Ollama, set these to a locally available model (e.g. gemma3:12b).
|
||||
AI_SUGGESTIONS_MODEL=gemini-flash-latest
|
||||
# AI_CATEGORIZATION_MODEL=gemini-flash-latest
|
||||
AI_SUGGESTIONS_MIN_GROUP_COUNT=2
|
||||
AI_SUGGESTIONS_MAX_GROUPS=40
|
||||
AI_SUGGESTIONS_CONFIDENCE_FLOOR=0.3
|
||||
AI_SUGGESTIONS_AUTO_SELECT=0.6
|
||||
AI_SUGGESTIONS_OVERBROAD_FRACTION=0.4
|
||||
AI_SUGGESTIONS_MIN_TRANSACTIONS=50
|
||||
AI_SUGGESTIONS_THROTTLE_DAYS=30
|
||||
AI_SUGGESTIONS_CONSENT_VERSION=1
|
||||
|
||||
# AI Suggestions cohort report (stats:ai-cohort-report)
|
||||
AI_SUGGESTIONS_REPORT_WEEKS=16
|
||||
# Comma-separated staff/test emails to exclude from cohort metrics (e.g. the
|
||||
# account that plants the release-anchor consent on deploy).
|
||||
AI_SUGGESTIONS_REPORT_EXCLUDED_EMAILS=
|
||||
|
||||
# Discord webhooks
|
||||
DISCORD_WEBHOOK_URL=
|
||||
# Optional dedicated channel for the AI cohort report; falls back to DISCORD_WEBHOOK_URL.
|
||||
DISCORD_AI_COHORT_WEBHOOK_URL=
|
||||
|
|
|
|||
|
|
@ -14,18 +14,9 @@ APP_LOCALE=en
|
|||
APP_FALLBACK_LOCALE=en
|
||||
APP_FAKER_LOCALE=en_US
|
||||
|
||||
# Trusted Proxies
|
||||
# Comma-separated proxy IPs/CIDRs Laravel trusts for X-Forwarded-* headers (no spaces).
|
||||
# When unset the app trusts all proxies (*), which keeps small private self-hosted
|
||||
# setups working behind any proxy out of the box. On a publicly exposed deployment set
|
||||
# this to the actual proxy range: otherwise clients can spoof X-Forwarded-For to forge
|
||||
# their IP (defeating per-IP rate limiting and poisoning logs). The private ranges below
|
||||
# cover the Docker network Coolify/Traefik connects from.
|
||||
TRUSTED_PROXIES=10.0.0.0/8,172.16.0.0/12,192.168.0.0/16
|
||||
|
||||
# Logging
|
||||
LOG_CHANNEL=stack
|
||||
LOG_STACK=single,sentry_logs
|
||||
LOG_STACK=single
|
||||
LOG_LEVEL=error
|
||||
|
||||
# Database (MySQL)
|
||||
|
|
@ -45,13 +36,6 @@ SESSION_ENCRYPT=true
|
|||
# Queue
|
||||
QUEUE_CONNECTION=database
|
||||
|
||||
# Sentry Error Tracking
|
||||
SENTRY_LARAVEL_DSN=
|
||||
# SENTRY_RELEASE is injected by CI as whisper-money@<git-sha> during production builds.
|
||||
SENTRY_TRACES_SAMPLE_RATE=1.0
|
||||
SENTRY_PROFILES_SAMPLE_RATE=1.0
|
||||
SENTRY_ENABLE_LOGS=true
|
||||
|
||||
# Redis (internal to container - no configuration needed)
|
||||
REDIS_HOST=127.0.0.1
|
||||
REDIS_PORT=6379
|
||||
|
|
@ -59,13 +43,14 @@ REDIS_PORT=6379
|
|||
# Email - Resend (Recommended for production)
|
||||
MAIL_MAILER=resend
|
||||
RESEND_API_KEY=your-resend-api-key
|
||||
MAIL_FROM_ADDRESS=no-reply@your-domain.com
|
||||
MAIL_FROM_ADDRESS=hi@your-domain.com
|
||||
MAIL_FROM_NAME="Whisper Money"
|
||||
MAIL_DRIP_FROM_ADDRESS=hi@your-domain.com
|
||||
MAIL_DRIP_FROM_NAME="Álvaro and Víctor"
|
||||
|
||||
# Stripe (Optional - for subscriptions)
|
||||
STRIPE_KEY=
|
||||
STRIPE_SECRET=
|
||||
STRIPE_WEBHOOK_SECRET=
|
||||
SUBSCRIPTIONS_ENABLED=false
|
||||
|
||||
# UI Configuration
|
||||
HIDE_AUTH_BUTTONS=false
|
||||
|
|
|
|||
|
|
@ -1,58 +0,0 @@
|
|||
name: Bug Report
|
||||
description: Report a bug or unexpected behavior
|
||||
labels: ["bug"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Thanks for taking the time to report a bug! Please fill out the sections below to help us reproduce and fix the issue.
|
||||
|
||||
- type: textarea
|
||||
id: environment
|
||||
attributes:
|
||||
label: Environment
|
||||
description: Provide details about your environment.
|
||||
placeholder: |
|
||||
OS: macOS 15.2 / Ubuntu 24.04 / Windows 11
|
||||
Docker version: 28.5.2
|
||||
Docker Compose version: v2.40.3
|
||||
Whisper Money version: (commit hash or release tag)
|
||||
Setup method: docker-compose / local
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: steps
|
||||
attributes:
|
||||
label: Steps to reproduce
|
||||
description: List the steps to reproduce the issue.
|
||||
placeholder: |
|
||||
1. Clone the repo and checkout main branch.
|
||||
2. Run `docker compose up -d`
|
||||
3. ...
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: expected
|
||||
attributes:
|
||||
label: Expected behavior
|
||||
description: What did you expect to happen?
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: actual
|
||||
attributes:
|
||||
label: Actual behavior
|
||||
description: What actually happened? Include any error messages or logs.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: additional
|
||||
attributes:
|
||||
label: Additional context
|
||||
description: Add any other context, screenshots, or log output that might help.
|
||||
validations:
|
||||
required: false
|
||||
|
|
@ -1,30 +0,0 @@
|
|||
name: Setup Bun & Node Deps
|
||||
description: |
|
||||
Sets up Bun and installs Node dependencies, caching both the global
|
||||
Bun install cache and `node_modules/`. On a `node_modules/` cache hit
|
||||
`bun install` is skipped entirely.
|
||||
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- name: Setup Bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
|
||||
- name: Cache bun global cache
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.bun/install/cache
|
||||
key: bun-${{ runner.os }}-${{ hashFiles('bun.lock') }}
|
||||
restore-keys: bun-${{ runner.os }}-
|
||||
|
||||
- name: Cache node_modules
|
||||
id: node-modules-cache
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: node_modules
|
||||
key: node-modules-${{ runner.os }}-${{ hashFiles('bun.lock') }}
|
||||
|
||||
- name: Install Node Dependencies
|
||||
if: steps.node-modules-cache.outputs.cache-hit != 'true'
|
||||
shell: bash
|
||||
run: bun install --frozen-lockfile
|
||||
|
|
@ -1,57 +0,0 @@
|
|||
name: Setup PHP & Composer Deps
|
||||
description: |
|
||||
Sets up PHP 8.4 and installs Composer dependencies with two layers of cache:
|
||||
the global Composer cache directory and the resolved `vendor/` directory
|
||||
itself. On a `vendor/` cache hit we skip `composer install` entirely.
|
||||
|
||||
inputs:
|
||||
php-version:
|
||||
description: PHP version to install
|
||||
required: false
|
||||
default: '8.4'
|
||||
tools:
|
||||
description: Tools to install via shivammathur/setup-php
|
||||
required: false
|
||||
default: 'composer:v2'
|
||||
coverage:
|
||||
description: Coverage driver (xdebug, pcov, none)
|
||||
required: false
|
||||
default: 'none'
|
||||
composer-args:
|
||||
description: Extra args for `composer install` when running
|
||||
required: false
|
||||
default: '--no-interaction --prefer-dist --optimize-autoloader'
|
||||
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- name: Setup PHP
|
||||
uses: shivammathur/setup-php@v2
|
||||
with:
|
||||
php-version: ${{ inputs.php-version }}
|
||||
tools: ${{ inputs.tools }}
|
||||
coverage: ${{ inputs.coverage }}
|
||||
|
||||
- name: Get composer cache directory
|
||||
id: composer-cache
|
||||
shell: bash
|
||||
run: echo "dir=$(composer config cache-files-dir)" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Cache composer global cache
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ${{ steps.composer-cache.outputs.dir }}
|
||||
key: composer-${{ runner.os }}-php${{ inputs.php-version }}-${{ hashFiles('composer.lock') }}
|
||||
restore-keys: composer-${{ runner.os }}-php${{ inputs.php-version }}-
|
||||
|
||||
- name: Cache vendor/
|
||||
id: vendor-cache
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: vendor
|
||||
key: vendor-${{ runner.os }}-php${{ inputs.php-version }}-${{ hashFiles('composer.lock') }}
|
||||
|
||||
- name: Install PHP Dependencies
|
||||
if: steps.vendor-cache.outputs.cache-hit != 'true'
|
||||
shell: bash
|
||||
run: composer install ${{ inputs.composer-args }}
|
||||
|
|
@ -1,329 +0,0 @@
|
|||
<laravel-boost-guidelines>
|
||||
=== 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 ensure the best experience when 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.4.17
|
||||
- inertiajs/inertia-laravel (INERTIA) - v2
|
||||
- laravel/cashier (CASHIER) - v16
|
||||
- laravel/fortify (FORTIFY) - v1
|
||||
- laravel/framework (LARAVEL) - v12
|
||||
- laravel/pennant (PENNANT) - v1
|
||||
- laravel/prompts (PROMPTS) - v0
|
||||
- laravel/wayfinder (WAYFINDER) - v0
|
||||
- laravel/mcp (MCP) - v0
|
||||
- laravel/pint (PINT) - v1
|
||||
- laravel/sail (SAIL) - v1
|
||||
- pestphp/pest (PEST) - v4
|
||||
- phpunit/phpunit (PHPUNIT) - v12
|
||||
- @inertiajs/react (INERTIA) - v2
|
||||
- react (REACT) - v19
|
||||
- tailwindcss (TAILWINDCSS) - v4
|
||||
- @laravel/vite-plugin-wayfinder (WAYFINDER) - v0
|
||||
- eslint (ESLINT) - v9
|
||||
- prettier (PRETTIER) - v3
|
||||
|
||||
## Skills Activation
|
||||
|
||||
This project has domain-specific skills available. You MUST activate the relevant skill whenever you work in that domain—don't wait until you're stuck.
|
||||
|
||||
- `pennant-development` — Manages feature flags with Laravel Pennant. Activates when creating, checking, or toggling feature flags; showing or hiding features conditionally; implementing A/B testing; working with @feature directive; or when the user mentions feature flags, feature toggles, Pennant, conditional features, rollouts, or gradually enabling features.
|
||||
- `wayfinder-development` — Activates whenever referencing backend routes in frontend components. Use when importing from @/actions or @/routes, calling Laravel routes from TypeScript, or working with Wayfinder route functions.
|
||||
- `pest-testing` — Tests applications using the Pest 4 PHP framework. Activates when writing tests, creating unit or feature tests, adding assertions, testing Livewire components, browser testing, debugging test failures, working with datasets or mocking; or when the user mentions test, spec, TDD, expects, assertion, coverage, or needs to verify functionality works.
|
||||
- `inertia-react-development` — Develops Inertia.js v2 React client-side applications. Activates when creating React pages, forms, or navigation; using <Link>, <Form>, useForm, or router; working with deferred props, prefetching, or polling; or when user mentions React with Inertia, React pages, React forms, or React navigation.
|
||||
- `tailwindcss-development` — Styles applications using Tailwind CSS v4 utilities. Activates when adding styles, restyling components, working with gradients, spacing, layout, flex, grid, responsive design, dark mode, colors, typography, or borders; or when the user mentions CSS, styling, classes, Tailwind, restyle, hero section, cards, buttons, or any visual/UI changes.
|
||||
|
||||
## 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, and 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 they work. 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.
|
||||
|
||||
## Documentation Files
|
||||
|
||||
- You must only create documentation files if explicitly requested by the user.
|
||||
|
||||
## Replies
|
||||
|
||||
- Be concise in your explanations - focus on what's important rather than explaining obvious details.
|
||||
|
||||
=== 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 trying other approaches when working with Laravel or Laravel ecosystem packages. This tool automatically passes a list of installed packages and their versions to the remote Boost API, so it returns only version-specific documentation for the user's circumstance. You should pass an array of packages to filter on if you know you need docs for particular packages.
|
||||
- Search the documentation before making code changes to ensure we are taking the correct approach.
|
||||
- Use multiple, broad, simple, topic-based queries at once. For example: `['rate limiting', 'routing rate limiting', 'routing']`. The most relevant results will be returned first.
|
||||
- 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
|
||||
|
||||
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 for single-line bodies.
|
||||
|
||||
## Constructors
|
||||
|
||||
- Use PHP 8 constructor property promotion in `__construct()`.
|
||||
- <code-snippet>public function __construct(public GitHub $github) { }</code-snippet>
|
||||
- Do not allow empty `__construct()` methods with zero parameters unless the constructor is private.
|
||||
|
||||
## Type Declarations
|
||||
|
||||
- Always use explicit return type declarations for methods and functions.
|
||||
- Use appropriate PHP type hints for method parameters.
|
||||
|
||||
<code-snippet name="Explicit Return Types and Method Params" lang="php">
|
||||
protected function isAccessible(User $user, ?string $path = null): bool
|
||||
{
|
||||
...
|
||||
}
|
||||
</code-snippet>
|
||||
|
||||
## Enums
|
||||
|
||||
- Typically, keys in an Enum should be TitleCase. For example: `FavoritePerson`, `BestLake`, `Monthly`.
|
||||
|
||||
## Comments
|
||||
|
||||
- Prefer PHPDoc blocks over inline comments. Never use comments within the code itself unless the logic is exceptionally complex.
|
||||
|
||||
## PHPDoc Blocks
|
||||
|
||||
- Add useful array shape type definitions when appropriate.
|
||||
|
||||
=== tests rules ===
|
||||
|
||||
# Test Enforcement
|
||||
|
||||
- Every change must be programmatically tested. Write a new test or update an existing test, then run the affected tests to make sure they pass.
|
||||
- Run the minimum number of tests needed to ensure code quality and speed. Use `php artisan test --compact` with a specific filename or filter.
|
||||
|
||||
=== inertia-laravel/core rules ===
|
||||
|
||||
# Inertia
|
||||
|
||||
- Inertia creates fully client-side rendered SPAs without modern SPA complexity, leveraging existing server-side patterns.
|
||||
- Components live in `resources/js/Pages` (unless specified in `vite.config.js`). Use `Inertia::render()` for server-side routing instead of Blade views.
|
||||
- ALWAYS use `search-docs` tool for version-specific Inertia documentation and updated code examples.
|
||||
- IMPORTANT: Activate `inertia-react-development` when working with Inertia client-side patterns.
|
||||
|
||||
=== inertia-laravel/v2 rules ===
|
||||
|
||||
# Inertia v2
|
||||
|
||||
- Use all Inertia features from v1 and v2. Check the documentation before making changes to ensure the correct approach.
|
||||
- New features: deferred props, infinite scrolling (merging props + `WhenVisible`), lazy loading on scroll, polling, prefetching.
|
||||
- When using deferred props, add an empty state with a pulsing or animated skeleton.
|
||||
|
||||
=== 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 `php 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.
|
||||
|
||||
## 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.
|
||||
|
||||
## Queues
|
||||
|
||||
- Use queued jobs for time-consuming operations with the `ShouldQueue` interface.
|
||||
|
||||
## 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] {name}` 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/v12 rules ===
|
||||
|
||||
# Laravel 12
|
||||
|
||||
- CRITICAL: ALWAYS use `search-docs` tool for version-specific Laravel documentation and updated code examples.
|
||||
- Since Laravel 11, Laravel has a new streamlined file structure which this project uses.
|
||||
|
||||
## Laravel 12 Structure
|
||||
|
||||
- In Laravel 12, middleware are no longer registered in `app/Http/Kernel.php`.
|
||||
- Middleware are configured declaratively in `bootstrap/app.php` using `Application::configure()->withMiddleware()`.
|
||||
- `bootstrap/app.php` is the file to register middleware, exceptions, and routing files.
|
||||
- `bootstrap/providers.php` contains application specific service providers.
|
||||
- The `app\Console\Kernel.php` file no longer exists; use `bootstrap/app.php` or `routes/console.php` for console configuration.
|
||||
- Console commands 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 12 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.
|
||||
|
||||
=== pennant/core rules ===
|
||||
|
||||
# Laravel Pennant
|
||||
|
||||
- This application uses Laravel Pennant for feature flag management, providing a flexible system for controlling feature availability across different organizations and user types.
|
||||
- IMPORTANT: Always use `search-docs` tool for version-specific Pennant documentation and updated code examples.
|
||||
- IMPORTANT: Activate `pennant-development` every time you're working with a Pennant or feature-flag-related task.
|
||||
|
||||
=== wayfinder/core rules ===
|
||||
|
||||
# Laravel Wayfinder
|
||||
|
||||
Wayfinder generates TypeScript functions for Laravel routes. Import from `@/actions/` (controllers) or `@/routes/` (named routes).
|
||||
|
||||
- IMPORTANT: Activate `wayfinder-development` skill whenever referencing backend routes in frontend components.
|
||||
- Invokable Controllers: `import StorePost from '@/actions/.../StorePostController'; StorePost()`.
|
||||
- Parameter Binding: Detects route keys (`{post:slug}`) — `show({ slug: "my-post" })`.
|
||||
- Query Merging: `show(1, { mergeQuery: { page: 2, sort: null } })` merges with current URL, `null` removes params.
|
||||
- Inertia: Use `.form()` with `<Form>` component or `form.submit(store())` with useForm.
|
||||
|
||||
=== 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.
|
||||
|
||||
=== pest/core rules ===
|
||||
|
||||
## Pest
|
||||
|
||||
- This project uses Pest for testing. Create tests: `php artisan make:test --pest {name}`.
|
||||
- Run tests: `php artisan test --compact` or filter: `php artisan test --compact --filter=testName`.
|
||||
- Do NOT delete tests without approval.
|
||||
- CRITICAL: ALWAYS use `search-docs` tool for version-specific Pest documentation and updated code examples.
|
||||
- IMPORTANT: Activate `pest-testing` every time you're working with a Pest or testing-related task.
|
||||
|
||||
=== inertia-react/core rules ===
|
||||
|
||||
# Inertia + React
|
||||
|
||||
- IMPORTANT: Activate `inertia-react-development` when working with Inertia React client-side patterns.
|
||||
|
||||
=== tailwindcss/core rules ===
|
||||
|
||||
# Tailwind CSS
|
||||
|
||||
- Always use existing Tailwind conventions; check project patterns before adding new ones.
|
||||
- IMPORTANT: Always use `search-docs` tool for version-specific Tailwind CSS documentation and updated code examples. Never rely on training data.
|
||||
- IMPORTANT: Activate `tailwindcss-development` every time you're working with a Tailwind CSS or styling-related task.
|
||||
|
||||
=== laravel/fortify rules ===
|
||||
|
||||
## Laravel Fortify
|
||||
|
||||
Fortify is a headless authentication backend that provides authentication routes and controllers for Laravel applications.
|
||||
|
||||
**Before implementing any authentication features, use the `search-docs` tool to get the latest docs for that specific feature.**
|
||||
|
||||
### Configuration & Setup
|
||||
|
||||
- Check `config/fortify.php` to see what's enabled. Use `search-docs` for detailed information on specific features.
|
||||
- Enable features by adding them to the `'features' => []` array: `Features::registration()`, `Features::resetPasswords()`, etc.
|
||||
- To see the all Fortify registered routes, use the `list-routes` tool with the `only_vendor: true` and `action: "Fortify"` parameters.
|
||||
- Fortify includes view routes by default (login, register). Set `'views' => false` in the configuration file to disable them if you're handling views yourself.
|
||||
|
||||
### Customization
|
||||
|
||||
- Views can be customized in `FortifyServiceProvider`'s `boot()` method using `Fortify::loginView()`, `Fortify::registerView()`, etc.
|
||||
- Customize authentication logic with `Fortify::authenticateUsing()` for custom user retrieval / validation.
|
||||
- Actions in `app/Actions/Fortify/` handle business logic (user creation, password reset, etc.). They're fully customizable, so you can modify them to change feature behavior.
|
||||
|
||||
## Available Features
|
||||
|
||||
- `Features::registration()` for user registration.
|
||||
- `Features::emailVerification()` to verify new user emails.
|
||||
- `Features::twoFactorAuthentication()` for 2FA with QR codes and recovery codes.
|
||||
- Add options: `['confirmPassword' => true, 'confirm' => true]` to require password confirmation and OTP confirmation before enabling 2FA.
|
||||
- `Features::updateProfileInformation()` to let users update their profile.
|
||||
- `Features::updatePasswords()` to let users change their passwords.
|
||||
- `Features::resetPasswords()` for password reset via email.
|
||||
</laravel-boost-guidelines>
|
||||
|
|
@ -1,369 +0,0 @@
|
|||
---
|
||||
name: inertia-react-development
|
||||
description: >-
|
||||
Develops Inertia.js v2 React client-side applications. Activates when creating
|
||||
React pages, forms, or navigation; using <Link>, <Form>, useForm, or router;
|
||||
working with deferred props, prefetching, or polling; or when user mentions
|
||||
React with Inertia, React pages, React forms, or React navigation.
|
||||
---
|
||||
|
||||
# Inertia React Development
|
||||
|
||||
## When to Apply
|
||||
|
||||
Activate this skill when:
|
||||
|
||||
- Creating or modifying React page components for Inertia
|
||||
- Working with forms in React (using `<Form>` or `useForm`)
|
||||
- Implementing client-side navigation with `<Link>` or `router`
|
||||
- Using v2 features: deferred props, prefetching, or polling
|
||||
- Building React-specific features with the Inertia protocol
|
||||
|
||||
## Documentation
|
||||
|
||||
Use `search-docs` for detailed Inertia v2 React patterns and documentation.
|
||||
|
||||
## Basic Usage
|
||||
|
||||
### Page Components Location
|
||||
|
||||
React page components should be placed in the `resources/js/Pages` directory.
|
||||
|
||||
### Page Component Structure
|
||||
|
||||
<code-snippet name="Basic React Page Component" lang="react">
|
||||
|
||||
export default function UsersIndex({ users }) {
|
||||
return (
|
||||
<div>
|
||||
<h1>Users</h1>
|
||||
<ul>
|
||||
{users.map(user => <li key={user.id}>{user.name}</li>)}
|
||||
</ul>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
</code-snippet>
|
||||
|
||||
## Client-Side Navigation
|
||||
|
||||
### Basic Link Component
|
||||
|
||||
Use `<Link>` for client-side navigation instead of traditional `<a>` tags:
|
||||
|
||||
<code-snippet name="Inertia React Navigation" lang="react">
|
||||
|
||||
import { Link, router } from '@inertiajs/react'
|
||||
|
||||
<Link href="/">Home</Link>
|
||||
<Link href="/users">Users</Link>
|
||||
<Link href={`/users/${user.id}`}>View User</Link>
|
||||
|
||||
</code-snippet>
|
||||
|
||||
### Link with Method
|
||||
|
||||
<code-snippet name="Link with POST Method" lang="react">
|
||||
|
||||
import { Link } from '@inertiajs/react'
|
||||
|
||||
<Link href="/logout" method="post" as="button">
|
||||
Logout
|
||||
</Link>
|
||||
|
||||
</code-snippet>
|
||||
|
||||
### Prefetching
|
||||
|
||||
Prefetch pages to improve perceived performance:
|
||||
|
||||
<code-snippet name="Prefetch on Hover" lang="react">
|
||||
|
||||
import { Link } from '@inertiajs/react'
|
||||
|
||||
<Link href="/users" prefetch>
|
||||
Users
|
||||
</Link>
|
||||
|
||||
</code-snippet>
|
||||
|
||||
### Programmatic Navigation
|
||||
|
||||
<code-snippet name="Router Visit" lang="react">
|
||||
|
||||
import { router } from '@inertiajs/react'
|
||||
|
||||
function handleClick() {
|
||||
router.visit('/users')
|
||||
}
|
||||
|
||||
// Or with options
|
||||
router.visit('/users', {
|
||||
method: 'post',
|
||||
data: { name: 'John' },
|
||||
onSuccess: () => console.log('Success!'),
|
||||
})
|
||||
|
||||
</code-snippet>
|
||||
|
||||
## Form Handling
|
||||
|
||||
### Form Component (Recommended)
|
||||
|
||||
The recommended way to build forms is with the `<Form>` component:
|
||||
|
||||
<code-snippet name="Form Component Example" lang="react">
|
||||
|
||||
import { Form } from '@inertiajs/react'
|
||||
|
||||
export default function CreateUser() {
|
||||
return (
|
||||
<Form action="/users" method="post">
|
||||
{({ errors, processing, wasSuccessful }) => (
|
||||
<>
|
||||
<input type="text" name="name" />
|
||||
{errors.name && <div>{errors.name}</div>}
|
||||
|
||||
<input type="email" name="email" />
|
||||
{errors.email && <div>{errors.email}</div>}
|
||||
|
||||
<button type="submit" disabled={processing}>
|
||||
{processing ? 'Creating...' : 'Create User'}
|
||||
</button>
|
||||
|
||||
{wasSuccessful && <div>User created!</div>}
|
||||
</>
|
||||
)}
|
||||
</Form>
|
||||
)
|
||||
}
|
||||
|
||||
</code-snippet>
|
||||
|
||||
### Form Component With All Props
|
||||
|
||||
<code-snippet name="Form Component Full Example" lang="react">
|
||||
|
||||
import { Form } from '@inertiajs/react'
|
||||
|
||||
<Form action="/users" method="post">
|
||||
{({
|
||||
errors,
|
||||
hasErrors,
|
||||
processing,
|
||||
progress,
|
||||
wasSuccessful,
|
||||
recentlySuccessful,
|
||||
clearErrors,
|
||||
resetAndClearErrors,
|
||||
defaults,
|
||||
isDirty,
|
||||
reset,
|
||||
submit
|
||||
}) => (
|
||||
<>
|
||||
<input type="text" name="name" defaultValue={defaults.name} />
|
||||
{errors.name && <div>{errors.name}</div>}
|
||||
|
||||
<button type="submit" disabled={processing}>
|
||||
{processing ? 'Saving...' : 'Save'}
|
||||
</button>
|
||||
|
||||
{progress && (
|
||||
<progress value={progress.percentage} max="100">
|
||||
{progress.percentage}%
|
||||
</progress>
|
||||
)}
|
||||
|
||||
{wasSuccessful && <div>Saved!</div>}
|
||||
</>
|
||||
)}
|
||||
</Form>
|
||||
|
||||
</code-snippet>
|
||||
|
||||
### Form Component Reset Props
|
||||
|
||||
The `<Form>` component supports automatic resetting:
|
||||
|
||||
- `resetOnError` - Reset form data when the request fails
|
||||
- `resetOnSuccess` - Reset form data when the request succeeds
|
||||
- `setDefaultsOnSuccess` - Update default values on success
|
||||
|
||||
Use the `search-docs` tool with a query of `form component resetting` for detailed guidance.
|
||||
|
||||
<code-snippet name="Form with Reset Props" lang="react">
|
||||
|
||||
import { Form } from '@inertiajs/react'
|
||||
|
||||
<Form
|
||||
action="/users"
|
||||
method="post"
|
||||
resetOnSuccess
|
||||
setDefaultsOnSuccess
|
||||
>
|
||||
{({ errors, processing, wasSuccessful }) => (
|
||||
<>
|
||||
<input type="text" name="name" />
|
||||
{errors.name && <div>{errors.name}</div>}
|
||||
|
||||
<button type="submit" disabled={processing}>
|
||||
Submit
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</Form>
|
||||
|
||||
</code-snippet>
|
||||
|
||||
Forms can also be built using the `useForm` helper for more programmatic control. Use the `search-docs` tool with a query of `useForm helper` for guidance.
|
||||
|
||||
### `useForm` Hook
|
||||
|
||||
For more programmatic control or to follow existing conventions, use the `useForm` hook:
|
||||
|
||||
<code-snippet name="useForm Hook Example" lang="react">
|
||||
|
||||
import { useForm } from '@inertiajs/react'
|
||||
|
||||
export default function CreateUser() {
|
||||
const { data, setData, post, processing, errors, reset } = useForm({
|
||||
name: '',
|
||||
email: '',
|
||||
password: '',
|
||||
})
|
||||
|
||||
function submit(e) {
|
||||
e.preventDefault()
|
||||
post('/users', {
|
||||
onSuccess: () => reset('password'),
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<form onSubmit={submit}>
|
||||
<input
|
||||
type="text"
|
||||
value={data.name}
|
||||
onChange={e => setData('name', e.target.value)}
|
||||
/>
|
||||
{errors.name && <div>{errors.name}</div>}
|
||||
|
||||
<input
|
||||
type="email"
|
||||
value={data.email}
|
||||
onChange={e => setData('email', e.target.value)}
|
||||
/>
|
||||
{errors.email && <div>{errors.email}</div>}
|
||||
|
||||
<input
|
||||
type="password"
|
||||
value={data.password}
|
||||
onChange={e => setData('password', e.target.value)}
|
||||
/>
|
||||
{errors.password && <div>{errors.password}</div>}
|
||||
|
||||
<button type="submit" disabled={processing}>
|
||||
Create User
|
||||
</button>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
|
||||
</code-snippet>
|
||||
|
||||
## Inertia v2 Features
|
||||
|
||||
### Deferred Props
|
||||
|
||||
Use deferred props to load data after initial page render:
|
||||
|
||||
<code-snippet name="Deferred Props with Empty State" lang="react">
|
||||
|
||||
export default function UsersIndex({ users }) {
|
||||
// users will be undefined initially, then populated
|
||||
return (
|
||||
<div>
|
||||
<h1>Users</h1>
|
||||
{!users ? (
|
||||
<div className="animate-pulse">
|
||||
<div className="h-4 bg-gray-200 rounded w-3/4 mb-2"></div>
|
||||
<div className="h-4 bg-gray-200 rounded w-1/2"></div>
|
||||
</div>
|
||||
) : (
|
||||
<ul>
|
||||
{users.map(user => (
|
||||
<li key={user.id}>{user.name}</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
</code-snippet>
|
||||
|
||||
### Polling
|
||||
|
||||
Automatically refresh data at intervals:
|
||||
|
||||
<code-snippet name="Polling Example" lang="react">
|
||||
|
||||
import { router } from '@inertiajs/react'
|
||||
import { useEffect } from 'react'
|
||||
|
||||
export default function Dashboard({ stats }) {
|
||||
useEffect(() => {
|
||||
const interval = setInterval(() => {
|
||||
router.reload({ only: ['stats'] })
|
||||
}, 5000) // Poll every 5 seconds
|
||||
|
||||
return () => clearInterval(interval)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1>Dashboard</h1>
|
||||
<div>Active Users: {stats.activeUsers}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
</code-snippet>
|
||||
|
||||
### WhenVisible (Infinite Scroll)
|
||||
|
||||
Load more data when user scrolls to a specific element:
|
||||
|
||||
<code-snippet name="Infinite Scroll with WhenVisible" lang="react">
|
||||
|
||||
import { WhenVisible } from '@inertiajs/react'
|
||||
|
||||
export default function UsersList({ users }) {
|
||||
return (
|
||||
<div>
|
||||
{users.data.map(user => (
|
||||
<div key={user.id}>{user.name}</div>
|
||||
))}
|
||||
|
||||
{users.next_page_url && (
|
||||
<WhenVisible
|
||||
data="users"
|
||||
params={{ page: users.current_page + 1 }}
|
||||
fallback={<div>Loading more...</div>}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
</code-snippet>
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
- Using traditional `<a>` links instead of Inertia's `<Link>` component (breaks SPA behavior)
|
||||
- Forgetting to add loading states (skeleton screens) when using deferred props
|
||||
- Not handling the `undefined` state of deferred props before data loads
|
||||
- Using `<form>` without preventing default submission (use `<Form>` component or `e.preventDefault()`)
|
||||
- Forgetting to check if `<Form>` component is available in your Inertia version
|
||||
|
|
@ -1,74 +0,0 @@
|
|||
---
|
||||
name: pennant-development
|
||||
description: >-
|
||||
Manages feature flags with Laravel Pennant. Activates when creating, checking, or toggling
|
||||
feature flags; showing or hiding features conditionally; implementing A/B testing; working with
|
||||
@feature directive; or when the user mentions feature flags, feature toggles, Pennant, conditional
|
||||
features, rollouts, or gradually enabling features.
|
||||
---
|
||||
|
||||
# Pennant Features
|
||||
|
||||
## When to Apply
|
||||
|
||||
Activate this skill when:
|
||||
|
||||
- Creating or checking feature flags
|
||||
- Managing feature rollouts
|
||||
- Implementing A/B testing
|
||||
|
||||
## Documentation
|
||||
|
||||
Use `search-docs` for detailed Pennant patterns and documentation.
|
||||
|
||||
## Basic Usage
|
||||
|
||||
### Defining Features
|
||||
|
||||
<code-snippet name="Defining Features" lang="php">
|
||||
use Laravel\Pennant\Feature;
|
||||
|
||||
Feature::define('new-dashboard', function (User $user) {
|
||||
return $user->isAdmin();
|
||||
});
|
||||
</code-snippet>
|
||||
|
||||
### Checking Features
|
||||
|
||||
<code-snippet name="Checking Features" lang="php">
|
||||
if (Feature::active('new-dashboard')) {
|
||||
// Feature is active
|
||||
}
|
||||
|
||||
// With scope
|
||||
if (Feature::for($user)->active('new-dashboard')) {
|
||||
// Feature is active for this user
|
||||
}
|
||||
</code-snippet>
|
||||
|
||||
### Blade Directive
|
||||
|
||||
<code-snippet name="Blade Directive" lang="blade">
|
||||
@feature('new-dashboard')
|
||||
<x-new-dashboard />
|
||||
@else
|
||||
<x-old-dashboard />
|
||||
@endfeature
|
||||
</code-snippet>
|
||||
|
||||
### Activating / Deactivating
|
||||
|
||||
<code-snippet name="Activating Features" lang="php">
|
||||
Feature::activate('new-dashboard');
|
||||
Feature::for($user)->activate('new-dashboard');
|
||||
</code-snippet>
|
||||
|
||||
## Verification
|
||||
|
||||
1. Check feature flag is defined
|
||||
2. Test with different scopes/users
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
- Forgetting to scope features for specific users/entities
|
||||
- Not following existing naming conventions
|
||||
|
|
@ -1,174 +0,0 @@
|
|||
---
|
||||
name: pest-testing
|
||||
description: >-
|
||||
Tests applications using the Pest 4 PHP framework. Activates when writing tests, creating unit or feature
|
||||
tests, adding assertions, testing Livewire components, browser testing, debugging test failures,
|
||||
working with datasets or mocking; or when the user mentions test, spec, TDD, expects, assertion,
|
||||
coverage, or needs to verify functionality works.
|
||||
---
|
||||
|
||||
# Pest Testing 4
|
||||
|
||||
## When to Apply
|
||||
|
||||
Activate this skill when:
|
||||
|
||||
- Creating new tests (unit, feature, or browser)
|
||||
- Modifying existing tests
|
||||
- Debugging test failures
|
||||
- Working with browser testing or smoke testing
|
||||
- Writing architecture tests or visual regression tests
|
||||
|
||||
## Documentation
|
||||
|
||||
Use `search-docs` for detailed Pest 4 patterns and documentation.
|
||||
|
||||
## Basic Usage
|
||||
|
||||
### Creating Tests
|
||||
|
||||
All tests must be written using Pest. Use `php artisan make:test --pest {name}`.
|
||||
|
||||
### Test Organization
|
||||
|
||||
- Unit/Feature tests: `tests/Feature` and `tests/Unit` directories.
|
||||
- Browser tests: `tests/Browser/` directory.
|
||||
- Do NOT remove tests without approval - these are core application code.
|
||||
|
||||
### Basic Test Structure
|
||||
|
||||
<code-snippet name="Basic Pest Test Example" lang="php">
|
||||
|
||||
it('is true', function () {
|
||||
expect(true)->toBeTrue();
|
||||
});
|
||||
|
||||
</code-snippet>
|
||||
|
||||
### Running Tests
|
||||
|
||||
- Run minimal tests with filter before finalizing: `php artisan test --compact --filter=testName`.
|
||||
- Run all tests: `php artisan test --compact`.
|
||||
- Run file: `php artisan test --compact tests/Feature/ExampleTest.php`.
|
||||
|
||||
## Assertions
|
||||
|
||||
Use specific assertions (`assertSuccessful()`, `assertNotFound()`) instead of `assertStatus()`:
|
||||
|
||||
<code-snippet name="Pest Response Assertion" lang="php">
|
||||
|
||||
it('returns all', function () {
|
||||
$this->postJson('/api/docs', [])->assertSuccessful();
|
||||
});
|
||||
|
||||
</code-snippet>
|
||||
|
||||
| Use | Instead of |
|
||||
|-----|------------|
|
||||
| `assertSuccessful()` | `assertStatus(200)` |
|
||||
| `assertNotFound()` | `assertStatus(404)` |
|
||||
| `assertForbidden()` | `assertStatus(403)` |
|
||||
|
||||
## Mocking
|
||||
|
||||
Import mock function before use: `use function Pest\Laravel\mock;`
|
||||
|
||||
## Datasets
|
||||
|
||||
Use datasets for repetitive tests (validation rules, etc.):
|
||||
|
||||
<code-snippet name="Pest Dataset Example" lang="php">
|
||||
|
||||
it('has emails', function (string $email) {
|
||||
expect($email)->not->toBeEmpty();
|
||||
})->with([
|
||||
'james' => 'james@laravel.com',
|
||||
'taylor' => 'taylor@laravel.com',
|
||||
]);
|
||||
|
||||
</code-snippet>
|
||||
|
||||
## Pest 4 Features
|
||||
|
||||
| Feature | Purpose |
|
||||
|---------|---------|
|
||||
| Browser Testing | Full integration tests in real browsers |
|
||||
| Smoke Testing | Validate multiple pages quickly |
|
||||
| Visual Regression | Compare screenshots for visual changes |
|
||||
| Test Sharding | Parallel CI runs |
|
||||
| Architecture Testing | Enforce code conventions |
|
||||
|
||||
### Browser Test Example
|
||||
|
||||
Browser tests run in real browsers for full integration testing:
|
||||
|
||||
- Browser tests live in `tests/Browser/`.
|
||||
- Use Laravel features like `Event::fake()`, `assertAuthenticated()`, and model factories.
|
||||
- Use `RefreshDatabase` for clean state per test.
|
||||
- Interact with page: click, type, scroll, select, submit, drag-and-drop, touch gestures.
|
||||
- Test on multiple browsers (Chrome, Firefox, Safari) if requested.
|
||||
- Test on different devices/viewports (iPhone 14 Pro, tablets) if requested.
|
||||
- Switch color schemes (light/dark mode) when appropriate.
|
||||
- Take screenshots or pause tests for debugging.
|
||||
|
||||
<code-snippet name="Pest Browser Test Example" lang="php">
|
||||
|
||||
it('may reset the password', function () {
|
||||
Notification::fake();
|
||||
|
||||
$this->actingAs(User::factory()->create());
|
||||
|
||||
$page = visit('/sign-in');
|
||||
|
||||
$page->assertSee('Sign In')
|
||||
->assertNoJavascriptErrors()
|
||||
->click('Forgot Password?')
|
||||
->fill('email', 'nuno@laravel.com')
|
||||
->click('Send Reset Link')
|
||||
->assertSee('We have emailed your password reset link!');
|
||||
|
||||
Notification::assertSent(ResetPassword::class);
|
||||
});
|
||||
|
||||
</code-snippet>
|
||||
|
||||
### Smoke Testing
|
||||
|
||||
Quickly validate multiple pages have no JavaScript errors:
|
||||
|
||||
<code-snippet name="Pest Smoke Testing Example" lang="php">
|
||||
|
||||
$pages = visit(['/', '/about', '/contact']);
|
||||
|
||||
$pages->assertNoJavascriptErrors()->assertNoConsoleLogs();
|
||||
|
||||
</code-snippet>
|
||||
|
||||
### Visual Regression Testing
|
||||
|
||||
Capture and compare screenshots to detect visual changes.
|
||||
|
||||
### Test Sharding
|
||||
|
||||
Split tests across parallel processes for faster CI runs.
|
||||
|
||||
### Architecture Testing
|
||||
|
||||
Pest 4 includes architecture testing (from Pest 3):
|
||||
|
||||
<code-snippet name="Architecture Test Example" lang="php">
|
||||
|
||||
arch('controllers')
|
||||
->expect('App\Http\Controllers')
|
||||
->toExtendNothing()
|
||||
->toHaveSuffix('Controller');
|
||||
|
||||
</code-snippet>
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
- Not importing `use function Pest\Laravel\mock;` before using mock
|
||||
- Using `assertStatus(200)` instead of `assertSuccessful()`
|
||||
- Forgetting datasets for repetitive validation tests
|
||||
- Deleting tests without approval
|
||||
- Forgetting `assertNoJavascriptErrors()` in browser tests
|
||||
|
|
@ -1,124 +0,0 @@
|
|||
---
|
||||
name: tailwindcss-development
|
||||
description: >-
|
||||
Styles applications using Tailwind CSS v4 utilities. Activates when adding styles, restyling components,
|
||||
working with gradients, spacing, layout, flex, grid, responsive design, dark mode, colors,
|
||||
typography, or borders; or when the user mentions CSS, styling, classes, Tailwind, restyle,
|
||||
hero section, cards, buttons, or any visual/UI changes.
|
||||
---
|
||||
|
||||
# Tailwind CSS Development
|
||||
|
||||
## When to Apply
|
||||
|
||||
Activate this skill when:
|
||||
|
||||
- Adding styles to components or pages
|
||||
- Working with responsive design
|
||||
- Implementing dark mode
|
||||
- Extracting repeated patterns into components
|
||||
- Debugging spacing or layout issues
|
||||
|
||||
## Documentation
|
||||
|
||||
Use `search-docs` for detailed Tailwind CSS v4 patterns and documentation.
|
||||
|
||||
## Basic Usage
|
||||
|
||||
- Use Tailwind CSS classes to style HTML. Check and follow existing Tailwind conventions in the project before introducing new patterns.
|
||||
- Offer to extract repeated patterns into components that match the project's conventions (e.g., Blade, JSX, Vue).
|
||||
- Consider class placement, order, priority, and defaults. Remove redundant classes, add classes to parent or child elements carefully to reduce repetition, and group elements logically.
|
||||
|
||||
## Tailwind CSS v4 Specifics
|
||||
|
||||
- Always use Tailwind CSS v4 and avoid deprecated utilities.
|
||||
- `corePlugins` is not supported in Tailwind v4.
|
||||
|
||||
### CSS-First Configuration
|
||||
|
||||
In Tailwind v4, configuration is CSS-first using the `@theme` directive — no separate `tailwind.config.js` file is needed:
|
||||
|
||||
<code-snippet name="CSS-First Config" lang="css">
|
||||
@theme {
|
||||
--color-brand: oklch(0.72 0.11 178);
|
||||
}
|
||||
</code-snippet>
|
||||
|
||||
### Import Syntax
|
||||
|
||||
In Tailwind v4, import Tailwind with a regular CSS `@import` statement instead of the `@tailwind` directives used in v3:
|
||||
|
||||
<code-snippet name="v4 Import Syntax" lang="diff">
|
||||
- @tailwind base;
|
||||
- @tailwind components;
|
||||
- @tailwind utilities;
|
||||
+ @import "tailwindcss";
|
||||
</code-snippet>
|
||||
|
||||
### Replaced Utilities
|
||||
|
||||
Tailwind v4 removed deprecated utilities. Use the replacements shown below. Opacity values remain numeric.
|
||||
|
||||
| Deprecated | Replacement |
|
||||
|------------|-------------|
|
||||
| bg-opacity-* | bg-black/* |
|
||||
| text-opacity-* | text-black/* |
|
||||
| border-opacity-* | border-black/* |
|
||||
| divide-opacity-* | divide-black/* |
|
||||
| ring-opacity-* | ring-black/* |
|
||||
| placeholder-opacity-* | placeholder-black/* |
|
||||
| flex-shrink-* | shrink-* |
|
||||
| flex-grow-* | grow-* |
|
||||
| overflow-ellipsis | text-ellipsis |
|
||||
| decoration-slice | box-decoration-slice |
|
||||
| decoration-clone | box-decoration-clone |
|
||||
|
||||
## Spacing
|
||||
|
||||
Use `gap` utilities instead of margins for spacing between siblings:
|
||||
|
||||
<code-snippet name="Gap Utilities" lang="html">
|
||||
<div class="flex gap-8">
|
||||
<div>Item 1</div>
|
||||
<div>Item 2</div>
|
||||
</div>
|
||||
</code-snippet>
|
||||
|
||||
## Dark Mode
|
||||
|
||||
If existing pages and components support dark mode, new pages and components must support it the same way, typically using the `dark:` variant:
|
||||
|
||||
<code-snippet name="Dark Mode" lang="html">
|
||||
<div class="bg-white dark:bg-gray-900 text-gray-900 dark:text-white">
|
||||
Content adapts to color scheme
|
||||
</div>
|
||||
</code-snippet>
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Flexbox Layout
|
||||
|
||||
<code-snippet name="Flexbox Layout" lang="html">
|
||||
<div class="flex items-center justify-between gap-4">
|
||||
<div>Left content</div>
|
||||
<div>Right content</div>
|
||||
</div>
|
||||
</code-snippet>
|
||||
|
||||
### Grid Layout
|
||||
|
||||
<code-snippet name="Grid Layout" lang="html">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
<div>Card 1</div>
|
||||
<div>Card 2</div>
|
||||
<div>Card 3</div>
|
||||
</div>
|
||||
</code-snippet>
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
- Using deprecated v3 utilities (bg-opacity-*, flex-shrink-*, etc.)
|
||||
- Using `@tailwind` directives instead of `@import "tailwindcss"`
|
||||
- Trying to use `tailwind.config.js` instead of CSS `@theme` directive
|
||||
- Using margins for spacing between siblings instead of gap utilities
|
||||
- Forgetting to add dark mode variants when the project uses dark mode
|
||||
|
|
@ -1,89 +0,0 @@
|
|||
---
|
||||
name: wayfinder-development
|
||||
description: >-
|
||||
Activates whenever referencing backend routes in frontend components. Use when
|
||||
importing from @/actions or @/routes, calling Laravel routes from TypeScript,
|
||||
or working with Wayfinder route functions.
|
||||
---
|
||||
|
||||
# Wayfinder Development
|
||||
|
||||
## When to Apply
|
||||
|
||||
Activate whenever referencing backend routes in frontend components:
|
||||
- Importing from `@/actions/` or `@/routes/`
|
||||
- Calling Laravel routes from TypeScript/JavaScript
|
||||
- Creating links or navigation to backend endpoints
|
||||
|
||||
## Documentation
|
||||
|
||||
Use `search-docs` for detailed Wayfinder patterns and documentation.
|
||||
|
||||
## Quick Reference
|
||||
|
||||
### Generate Routes
|
||||
|
||||
Run after route changes if Vite plugin isn't installed:
|
||||
|
||||
php artisan wayfinder:generate --no-interaction
|
||||
|
||||
For form helpers, use `--with-form` flag:
|
||||
|
||||
php artisan wayfinder:generate --with-form --no-interaction
|
||||
|
||||
### Import Patterns
|
||||
|
||||
<code-snippet name="Controller Action Imports" lang="typescript">
|
||||
|
||||
// Named imports for tree-shaking (preferred)...
|
||||
import { show, store, update } from '@/actions/App/Http/Controllers/PostController'
|
||||
|
||||
// Named route imports...
|
||||
import { show as postShow } from '@/routes/post'
|
||||
|
||||
</code-snippet>
|
||||
|
||||
### Common Methods
|
||||
|
||||
<code-snippet name="Wayfinder Methods" lang="typescript">
|
||||
|
||||
// Get route object...
|
||||
show(1) // { url: "/posts/1", method: "get" }
|
||||
|
||||
// Get URL string...
|
||||
show.url(1) // "/posts/1"
|
||||
|
||||
// Specific HTTP methods...
|
||||
show.get(1)
|
||||
store.post()
|
||||
update.patch(1)
|
||||
destroy.delete(1)
|
||||
|
||||
// Form attributes for HTML forms...
|
||||
store.form() // { action: "/posts", method: "post" }
|
||||
|
||||
// Query parameters...
|
||||
show(1, { query: { page: 1 } }) // "/posts/1?page=1"
|
||||
|
||||
</code-snippet>
|
||||
|
||||
## Wayfinder + Inertia
|
||||
|
||||
Use Wayfinder with the `<Form>` component:
|
||||
<code-snippet name="Wayfinder Form (React)" lang="typescript">
|
||||
|
||||
<Form {...store.form()}><input name="title" /></Form>
|
||||
|
||||
</code-snippet>
|
||||
|
||||
## Verification
|
||||
|
||||
1. Run `php artisan wayfinder:generate` to regenerate routes if Vite plugin isn't installed
|
||||
2. Check TypeScript imports resolve correctly
|
||||
3. Verify route URLs match expected paths
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
- Using default imports instead of named imports (breaks tree-shaking)
|
||||
- Forgetting to regenerate after route changes
|
||||
- Not using type-safe parameter objects for route model binding
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 112 KiB |
|
|
@ -1,108 +0,0 @@
|
|||
---
|
||||
name: cashier-stripe-development
|
||||
description: "Handles Laravel Cashier Stripe integration including subscriptions, webhooks, Stripe Checkout, invoices, charges, refunds, trials, coupons, metered billing, and payment failure handling. Triggered when a user mentions Cashier, Billable, IncompletePayment, stripe_id, newSubscription, Stripe subscriptions, or billing. Also applies when setting up webhooks, handling SCA/3DS payment failures, testing with Stripe test cards, or troubleshooting incomplete subscriptions, CSRF webhook errors, or migration publish issues."
|
||||
license: MIT
|
||||
metadata:
|
||||
author: laravel
|
||||
---
|
||||
|
||||
# Cashier Stripe Development
|
||||
|
||||
## When to Apply
|
||||
|
||||
Activate this skill when:
|
||||
|
||||
- Installing or configuring Laravel Cashier Stripe
|
||||
- Setting up subscriptions, trials, quantities, or plan swapping
|
||||
- Handling webhooks or SCA/3DS payment failures
|
||||
- Working with Stripe Checkout, invoices, or charges
|
||||
- Testing billing scenarios with Stripe test cards or tokens
|
||||
|
||||
## Documentation
|
||||
|
||||
Use `search-docs` for detailed Cashier patterns and documentation covering subscriptions, webhooks, Stripe Checkout, invoices, payment methods, and testing.
|
||||
|
||||
For deeper guidance on specific topics, read the relevant reference file before implementing:
|
||||
|
||||
- `references/subscriptions.md` covers subscription creation, status checks, swapping, trials, quantities, and multiple products
|
||||
- `references/webhooks.md` covers webhook setup, custom handlers, CSRF exclusion, and local development with the Stripe CLI
|
||||
- `references/testing.md` covers Stripe test cards, payment method tokens, and feature test patterns
|
||||
|
||||
## Basic Usage
|
||||
|
||||
### Installation
|
||||
|
||||
```bash
|
||||
php artisan vendor:publish --tag="cashier-migrations"
|
||||
php artisan migrate
|
||||
php artisan vendor:publish --tag="cashier-config"
|
||||
```
|
||||
|
||||
### Environment Variables
|
||||
|
||||
```
|
||||
STRIPE_KEY=pk_test_...
|
||||
STRIPE_SECRET=sk_test_...
|
||||
STRIPE_WEBHOOK_SECRET=whsec_...
|
||||
CASHIER_CURRENCY=usd
|
||||
CASHIER_CURRENCY_LOCALE=en_US
|
||||
```
|
||||
|
||||
### Billable Model
|
||||
|
||||
<!-- Add Billable Trait -->
|
||||
```php
|
||||
use Laravel\Cashier\Billable;
|
||||
|
||||
class User extends Authenticatable
|
||||
{
|
||||
use Billable;
|
||||
}
|
||||
```
|
||||
|
||||
For a non-User model, register it in a service provider:
|
||||
|
||||
<!-- Custom Billable Model -->
|
||||
```php
|
||||
// In AppServiceProvider::boot()
|
||||
Cashier::useCustomerModel(Team::class);
|
||||
```
|
||||
|
||||
### Creating a Subscription
|
||||
|
||||
<!-- Create Subscription -->
|
||||
```php
|
||||
use Laravel\Cashier\Exceptions\IncompletePayment;
|
||||
|
||||
try {
|
||||
$user->newSubscription('default', 'price_xxxx')->create($paymentMethodId);
|
||||
} catch (IncompletePayment $e) {
|
||||
return redirect()->route('cashier.payment', [$e->payment->id, 'redirect' => route('home')]);
|
||||
}
|
||||
```
|
||||
|
||||
Always wrap subscription creation in a try/catch for `IncompletePayment`. When a card requires 3DS authentication, Cashier throws this exception. The `cashier.payment` route is auto-registered and handles the confirmation flow.
|
||||
|
||||
## Verification
|
||||
|
||||
1. Run migrations and confirm `stripe_id`, `pm_type`, `pm_last_four`, and `trial_ends_at` columns exist on the billable model table
|
||||
2. Test the webhook endpoint with `stripe listen --forward-to localhost/stripe/webhook` if you use the default path, or swap `stripe` for your configured `CASHIER_PATH`
|
||||
3. Confirm `$user->subscribed('default')` returns the expected value for active and incomplete subscriptions
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
- The migration publish tag is `cashier-migrations`, not `cashier`. Running `migrate` before publishing results in missing columns and tables.
|
||||
- `CASHIER_CURRENCY` must be set explicitly. It defaults to USD, which silently breaks non-US apps.
|
||||
- The Stripe CLI generates its own webhook signing secret. It is different from the Dashboard endpoint secret. Using the wrong one causes signature verification failures.
|
||||
- The webhook route must be excluded from CSRF verification using your configured `cashier.path`. If you change `CASHIER_PATH` from `stripe` to `billing`, exclude `billing/*`, not `stripe/*`.
|
||||
- `canceled()` returns true as soon as `cancel()` is called, but the user still has access during the grace period. Use `ended()` to confirm access is fully revoked.
|
||||
- `subscribed()` returns true during the grace period even though the subscription is canceled.
|
||||
- `subscribed()` returns false for `incomplete` and `past_due` subscriptions by default.
|
||||
- Prices cannot be swapped and quantity cannot be updated while a subscription has an incomplete payment.
|
||||
- When extending `WebhookController`, call `Cashier::ignoreRoutes()` in a service provider and re-register both `cashier.payment` and `cashier.webhook` under the configured `cashier.path`.
|
||||
- Use `Cashier::useCustomerModel()` in a service provider to set a custom billable model. There is no `CASHIER_MODEL` env var.
|
||||
- `trial_ends_at` is a local database column synced via webhooks. It will be stale if webhooks are not configured in production.
|
||||
- In MySQL, the `stripe_id` column must use `utf8_bin` collation to avoid case-sensitivity issues.
|
||||
- `noProrate()` has no effect when combined with `swapAndInvoice()`. That method always prorates.
|
||||
- Methods like `withPromotionCode()` require the Stripe API ID such as `promo_xxxx`, not the customer-facing code. Use `findPromotionCode()` to resolve a code to its ID.
|
||||
- Always use `search-docs` for the latest Cashier documentation rather than relying on this skill alone.
|
||||
|
|
@ -1,108 +0,0 @@
|
|||
# Subscriptions Reference
|
||||
|
||||
Use `search-docs` for authoritative documentation on subscriptions.
|
||||
|
||||
## Status Checks
|
||||
|
||||
| Method | Returns true when |
|
||||
|---|---|
|
||||
| `$user->subscribed('default')` | Active or on grace period |
|
||||
| `->onTrial()` | Trial period active |
|
||||
| `->onGracePeriod()` | Canceled, period not yet ended |
|
||||
| `->canceled()` | `ends_at` is set, may still have access |
|
||||
| `->ended()` | Canceled and grace period expired |
|
||||
| `->incomplete()` | Awaiting SCA/3DS confirmation |
|
||||
| `->pastDue()` | Payment overdue |
|
||||
| `->recurring()` | Active and not on trial |
|
||||
|
||||
Check by product or price:
|
||||
|
||||
```php
|
||||
$user->subscribedToProduct('prod_premium', 'default');
|
||||
$user->subscribedToPrice('price_monthly', 'default');
|
||||
```
|
||||
|
||||
## Swapping Plans
|
||||
|
||||
```php
|
||||
$user->subscription('default')->swap('price_new');
|
||||
$user->subscription('default')->noProrate()->swap('price_new');
|
||||
$user->subscription('default')->swapAndInvoice('price_new');
|
||||
$user->subscription('default')->skipTrial()->swap('price_new');
|
||||
```
|
||||
|
||||
## Quantity
|
||||
|
||||
```php
|
||||
$user->subscription('default')->incrementQuantity();
|
||||
$user->subscription('default')->decrementQuantity();
|
||||
$user->subscription('default')->updateQuantity(10);
|
||||
$user->subscription('default')->noProrate()->updateQuantity(10);
|
||||
```
|
||||
|
||||
## Trials
|
||||
|
||||
```php
|
||||
$user->newSubscription('default', 'price_xxxx')
|
||||
->trialDays(14)
|
||||
->create($paymentMethodId);
|
||||
|
||||
$subscription->extendTrial(now()->addDays(7));
|
||||
```
|
||||
|
||||
## Multiple Products on One Subscription
|
||||
|
||||
```php
|
||||
$user->newSubscription('default', ['price_monthly', 'price_chat'])
|
||||
->quantity(5, 'price_chat')
|
||||
->create($paymentMethod);
|
||||
|
||||
$user->subscription('default')->addPrice('price_chat');
|
||||
$user->subscription('default')->removePrice('price_chat');
|
||||
$user->subscription('default')->swap(['price_pro', 'price_chat']);
|
||||
```
|
||||
|
||||
## Multiple Subscriptions
|
||||
|
||||
```php
|
||||
$user->newSubscription('swimming', 'price_swimming_monthly')->create($pm);
|
||||
$user->newSubscription('gym', 'price_gym_monthly')->create($pm);
|
||||
|
||||
$user->subscription('swimming')->swap('price_swimming_yearly');
|
||||
$user->subscription('gym')->cancel();
|
||||
```
|
||||
|
||||
## Cancellation and Resumption
|
||||
|
||||
```php
|
||||
$user->subscription('default')->cancel(); // At end of billing period
|
||||
$user->subscription('default')->cancelNow(); // Immediately
|
||||
$user->subscription('default')->resume(); // During grace period only
|
||||
```
|
||||
|
||||
## Incomplete Payment Handling
|
||||
|
||||
```php
|
||||
if ($user->hasIncompletePayment('default')) {
|
||||
$paymentId = $user->subscription('default')->latestPayment()->id;
|
||||
return redirect()->route('cashier.payment', $paymentId);
|
||||
}
|
||||
```
|
||||
|
||||
Opt out of default deactivation behavior:
|
||||
|
||||
```php
|
||||
Cashier::keepPastDueSubscriptionsActive();
|
||||
Cashier::keepIncompleteSubscriptionsActive();
|
||||
```
|
||||
|
||||
## Metered / Usage-Based Billing
|
||||
|
||||
```php
|
||||
$user->newSubscription('default')
|
||||
->meteredPrice('price_metered')
|
||||
->create($paymentMethodId);
|
||||
|
||||
$user->reportMeterEvent('emails-sent');
|
||||
$user->reportMeterEvent('emails-sent', quantity: 15);
|
||||
```
|
||||
|
|
@ -1,52 +0,0 @@
|
|||
# Testing Reference
|
||||
|
||||
Use `search-docs` for authoritative documentation on testing Cashier integrations.
|
||||
|
||||
## Test Cards and Tokens
|
||||
|
||||
Use card numbers for browser-based flows (Stripe.js / Checkout). Use `pm_card_*` tokens directly in feature tests that call the Stripe API.
|
||||
|
||||
| Card Number | Token | Behavior |
|
||||
|---|---|---|
|
||||
| `4242 4242 4242 4242` | `pm_card_visa` | Succeeds immediately |
|
||||
| `4000 0025 0000 3155` | `pm_card_threeDSecure2Required` | Requires SCA/3DS |
|
||||
| `4000 0027 6000 3184` | `pm_card_authenticationRequired` | Requires authentication |
|
||||
| `4000 0000 0000 9995` | `pm_card_chargeDeclinedInsufficientFunds` | Declined, insufficient funds |
|
||||
| `4000 0000 0000 0002` | `pm_card_chargeDeclined` | Declined |
|
||||
|
||||
Use expiry `12/34`, any CVC, any ZIP for card number inputs.
|
||||
|
||||
## Feature Test Example
|
||||
|
||||
Feature tests that hit the real Stripe test API use `pm_card_*` tokens:
|
||||
|
||||
```php
|
||||
public function test_user_can_subscribe(): void
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
|
||||
$user->newSubscription('default', 'price_xxxx')
|
||||
->create('pm_card_visa');
|
||||
|
||||
$this->assertTrue($user->subscribed('default'));
|
||||
}
|
||||
|
||||
public function test_incomplete_payment_is_handled(): void
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
|
||||
try {
|
||||
$user->newSubscription('default', 'price_xxxx')
|
||||
->create('pm_card_threeDSecure2Required');
|
||||
} catch (\Laravel\Cashier\Exceptions\IncompletePayment $e) {
|
||||
$this->assertTrue($user->subscription('default')->incomplete());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Setup Notes
|
||||
|
||||
- Use Stripe test mode keys (`sk_test_...`, `pk_test_...`) in your test environment
|
||||
- Cashier does not ship a global `fake()` helper. Tests hit the real Stripe test API by default.
|
||||
- Refer to `tests/Feature/` in the Cashier package itself for integration test patterns covering subscription creation, payment methods, and webhook handling
|
||||
- Use `search-docs` for current guidance on mocking Stripe HTTP calls or using Stripe's test clock feature for time-sensitive scenarios
|
||||
|
|
@ -1,132 +0,0 @@
|
|||
# Webhooks Reference
|
||||
|
||||
Use `search-docs` for authoritative documentation on webhooks.
|
||||
|
||||
## Auto-Registered Routes
|
||||
|
||||
Cashier registers two routes automatically under the `cashier.path` prefix (`config('cashier.path')`, default `stripe`):
|
||||
|
||||
- `POST /{cashier.path}/webhook` named `cashier.webhook`
|
||||
- `GET /{cashier.path}/payment/{id}` named `cashier.payment`
|
||||
|
||||
With the default config these are `/stripe/webhook` and `/stripe/payment/{id}`. If you set `CASHIER_PATH=billing`, they become `/billing/webhook` and `/billing/payment/{id}`.
|
||||
|
||||
## CSRF Exclusion
|
||||
|
||||
Use the same path prefix you configured for Cashier here. If `CASHIER_PATH=billing`, exclude `billing/*` instead of `stripe/*`.
|
||||
|
||||
**Laravel 11+ (`bootstrap/app.php`, default path example):**
|
||||
|
||||
```php
|
||||
->withMiddleware(function (Middleware $middleware) {
|
||||
$middleware->validateCsrfTokens(except: ['stripe/*']);
|
||||
})
|
||||
```
|
||||
|
||||
**Laravel 10 (`app/Http/Middleware/VerifyCsrfToken.php`, default path example):**
|
||||
|
||||
```php
|
||||
protected $except = [
|
||||
'stripe/*',
|
||||
];
|
||||
```
|
||||
|
||||
## Local Development with Stripe CLI
|
||||
|
||||
If you changed `cashier.path`, forward Stripe CLI events to that URL instead of `/stripe/webhook`.
|
||||
|
||||
```bash
|
||||
stripe login
|
||||
stripe listen --forward-to your-app.test/stripe/webhook
|
||||
stripe trigger invoice.payment_succeeded
|
||||
```
|
||||
|
||||
The CLI outputs a `whsec_...` signing secret specific to that session. Set it as `STRIPE_WEBHOOK_SECRET` locally. It is not the same as the Dashboard endpoint secret.
|
||||
|
||||
## Registering Events in the Stripe Dashboard
|
||||
|
||||
Use the Artisan command to create the endpoint automatically with all required events:
|
||||
|
||||
```bash
|
||||
php artisan cashier:webhook
|
||||
```
|
||||
|
||||
Cashier's `cashier:webhook` command registers these events by default:
|
||||
|
||||
- `customer.subscription.created`
|
||||
- `customer.subscription.updated`
|
||||
- `customer.subscription.deleted`
|
||||
- `customer.updated` / `customer.deleted`
|
||||
- `invoice.payment_action_required`
|
||||
- `invoice.payment_succeeded`
|
||||
- `payment_method.automatically_updated`
|
||||
|
||||
Cashier's `WebhookController` has built-in handlers for all of the above except `invoice.payment_succeeded`. For renewal hooks, prefer `WebhookReceived` / `WebhookHandled` listeners unless you intentionally add your own controller method.
|
||||
|
||||
## Custom Handlers: Extending WebhookController
|
||||
|
||||
Method name pattern: `handle` + StudlyCase of event type with dots replaced by underscores.
|
||||
|
||||
`customer.subscription.created` becomes `handleCustomerSubscriptionCreated`.
|
||||
|
||||
```php
|
||||
use Laravel\Cashier\Http\Controllers\WebhookController as CashierController;
|
||||
|
||||
class StripeWebhookController extends CashierController
|
||||
{
|
||||
public function handleCustomerSubscriptionCreated(array $payload)
|
||||
{
|
||||
$response = parent::handleCustomerSubscriptionCreated($payload);
|
||||
|
||||
// your logic after Cashier syncs the subscription
|
||||
|
||||
return $response;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
If you add a method for an event Cashier does not handle internally, such as `invoice.payment_succeeded`, do not call `parent::handle...()` unless the base controller actually defines that method.
|
||||
|
||||
In a service provider, disable auto-registration and re-register both Cashier routes so the incomplete-payment flow and `cashier:webhook` command keep working:
|
||||
|
||||
```php
|
||||
Cashier::ignoreRoutes();
|
||||
```
|
||||
|
||||
```php
|
||||
// routes/web.php
|
||||
use App\Http\Controllers\StripeWebhookController;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use Laravel\Cashier\Http\Controllers\PaymentController;
|
||||
|
||||
Route::prefix(config('cashier.path'))
|
||||
->name('cashier.')
|
||||
->group(function () {
|
||||
Route::get('payment/{id}', [PaymentController::class, 'show'])->name('payment');
|
||||
Route::post('webhook', [StripeWebhookController::class, 'handleWebhook'])->name('webhook');
|
||||
});
|
||||
```
|
||||
|
||||
Keep the `cashier.webhook` route name unless you plan to pass `--url` explicitly to `php artisan cashier:webhook`.
|
||||
|
||||
## Custom Handlers: Listening to Events
|
||||
|
||||
The simpler option when you do not need to replace Cashier's internal logic, or when you want to react to events such as `invoice.payment_succeeded` that Cashier does not process itself:
|
||||
|
||||
```php
|
||||
use Laravel\Cashier\Events\WebhookReceived;
|
||||
use Laravel\Cashier\Events\WebhookHandled;
|
||||
|
||||
// WebhookReceived fires for every event before Cashier processes it
|
||||
// WebhookHandled fires after Cashier processes it
|
||||
|
||||
Event::listen(WebhookReceived::class, function (WebhookReceived $event) {
|
||||
if ($event->payload['type'] === 'invoice.payment_succeeded') {
|
||||
// handle renewal
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
## Signature Verification
|
||||
|
||||
`VerifyWebhookSignature` middleware is applied automatically when `cashier.webhook.secret` is set. No extra wiring is needed.
|
||||
|
|
@ -1,128 +0,0 @@
|
|||
---
|
||||
name: fortify-development
|
||||
description: Laravel Fortify headless authentication backend development. Activate when implementing authentication features including login, registration, password reset, email verification, two-factor authentication (2FA/TOTP), profile updates, headless auth, authentication scaffolding, or auth guards in Laravel applications.
|
||||
---
|
||||
|
||||
# Laravel Fortify Development
|
||||
|
||||
Fortify is a headless authentication backend that provides authentication routes and controllers for Laravel applications.
|
||||
|
||||
## Documentation
|
||||
|
||||
Use `search-docs` for detailed Laravel Fortify patterns and documentation.
|
||||
|
||||
## Usage
|
||||
|
||||
- **Routes**: Use `list-routes` with `only_vendor: true` and `action: "Fortify"` to see all registered endpoints
|
||||
- **Actions**: Check `app/Actions/Fortify/` for customizable business logic (user creation, password validation, etc.)
|
||||
- **Config**: See `config/fortify.php` for all options including features, guards, rate limiters, and username field
|
||||
- **Contracts**: Look in `Laravel\Fortify\Contracts\` for overridable response classes (`LoginResponse`, `LogoutResponse`, etc.)
|
||||
- **Views**: All view callbacks are set in `FortifyServiceProvider::boot()` using `Fortify::loginView()`, `Fortify::registerView()`, etc.
|
||||
|
||||
## Available Features
|
||||
|
||||
Enable in `config/fortify.php` features array:
|
||||
|
||||
- `Features::registration()` - User registration
|
||||
- `Features::resetPasswords()` - Password reset via email
|
||||
- `Features::emailVerification()` - Requires User to implement `MustVerifyEmail`
|
||||
- `Features::updateProfileInformation()` - Profile updates
|
||||
- `Features::updatePasswords()` - Password changes
|
||||
- `Features::twoFactorAuthentication()` - 2FA with QR codes and recovery codes
|
||||
|
||||
> Use `search-docs` for feature configuration options and customization patterns.
|
||||
|
||||
## Setup Workflows
|
||||
|
||||
### Two-Factor Authentication Setup
|
||||
|
||||
```
|
||||
- [ ] Add TwoFactorAuthenticatable trait to User model
|
||||
- [ ] Enable feature in config/fortify.php
|
||||
- [ ] If the `*_add_two_factor_columns_to_users_table.php` migration is missing, publish via `php artisan vendor:publish --tag=fortify-migrations` and migrate
|
||||
- [ ] Set up view callbacks in FortifyServiceProvider
|
||||
- [ ] Create 2FA management UI
|
||||
- [ ] Test QR code and recovery codes
|
||||
```
|
||||
|
||||
> Use `search-docs` for TOTP implementation and recovery code handling patterns.
|
||||
|
||||
### Email Verification Setup
|
||||
|
||||
```
|
||||
- [ ] Enable emailVerification feature in config
|
||||
- [ ] Implement MustVerifyEmail interface on User model
|
||||
- [ ] Set up verifyEmailView callback
|
||||
- [ ] Add verified middleware to protected routes
|
||||
- [ ] Test verification email flow
|
||||
```
|
||||
|
||||
> Use `search-docs` for MustVerifyEmail implementation patterns.
|
||||
|
||||
### Password Reset Setup
|
||||
|
||||
```
|
||||
- [ ] Enable resetPasswords feature in config
|
||||
- [ ] Set up requestPasswordResetLinkView callback
|
||||
- [ ] Set up resetPasswordView callback
|
||||
- [ ] Define password.reset named route (if views disabled)
|
||||
- [ ] Test reset email and link flow
|
||||
```
|
||||
|
||||
> Use `search-docs` for custom password reset flow patterns.
|
||||
|
||||
### SPA Authentication Setup
|
||||
|
||||
```
|
||||
- [ ] Set 'views' => false in config/fortify.php
|
||||
- [ ] Install and configure Laravel Sanctum for session-based SPA authentication
|
||||
- [ ] Use the 'web' guard in config/fortify.php (required for session-based authentication)
|
||||
- [ ] Set up CSRF token handling
|
||||
- [ ] Test XHR authentication flows
|
||||
```
|
||||
|
||||
> Use `search-docs` for integration and SPA authentication patterns.
|
||||
|
||||
#### Two-Factor Authentication in SPA Mode
|
||||
|
||||
When `views` is set to `false`, Fortify returns JSON responses instead of redirects.
|
||||
|
||||
If a user attempts to log in and two-factor authentication is enabled, the login request will return a JSON response indicating that a two-factor challenge is required:
|
||||
|
||||
```json
|
||||
{
|
||||
"two_factor": true
|
||||
}
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### Custom Authentication Logic
|
||||
|
||||
Override authentication behavior using `Fortify::authenticateUsing()` for custom user retrieval or `Fortify::authenticateThrough()` to customize the authentication pipeline. Override response contracts in `AppServiceProvider` for custom redirects.
|
||||
|
||||
### Registration Customization
|
||||
|
||||
Modify `app/Actions/Fortify/CreateNewUser.php` to customize user creation logic, validation rules, and additional fields.
|
||||
|
||||
### Rate Limiting
|
||||
|
||||
Configure via `fortify.limiters.login` in config. Default configuration throttles by username + IP combination.
|
||||
|
||||
## Key Endpoints
|
||||
|
||||
| Feature | Method | Endpoint |
|
||||
|------------------------|----------|---------------------------------------------|
|
||||
| Login | POST | `/login` |
|
||||
| Logout | POST | `/logout` |
|
||||
| Register | POST | `/register` |
|
||||
| Password Reset Request | POST | `/forgot-password` |
|
||||
| Password Reset | POST | `/reset-password` |
|
||||
| Email Verify Notice | GET | `/email/verify` |
|
||||
| Resend Verification | POST | `/email/verification-notification` |
|
||||
| Password Confirm | POST | `/user/confirm-password` |
|
||||
| Enable 2FA | POST | `/user/two-factor-authentication` |
|
||||
| Confirm 2FA | POST | `/user/confirmed-two-factor-authentication` |
|
||||
| 2FA Challenge | POST | `/two-factor-challenge` |
|
||||
| Get QR Code | GET | `/user/two-factor-qr-code` |
|
||||
| Recovery Codes | GET/POST | `/user/two-factor-recovery-codes` |
|
||||
|
|
@ -1,361 +0,0 @@
|
|||
---
|
||||
name: inertia-react-development
|
||||
description: "Develops Inertia.js v2 React client-side applications. Activates when creating React pages, forms, or navigation; using <Link>, <Form>, useForm, or router; working with deferred props, prefetching, or polling; or when user mentions React with Inertia, React pages, React forms, or React navigation."
|
||||
license: MIT
|
||||
metadata:
|
||||
author: laravel
|
||||
---
|
||||
|
||||
# Inertia React Development
|
||||
|
||||
## When to Apply
|
||||
|
||||
Activate this skill when:
|
||||
|
||||
- Creating or modifying React page components for Inertia
|
||||
- Working with forms in React (using `<Form>` or `useForm`)
|
||||
- Implementing client-side navigation with `<Link>` or `router`
|
||||
- Using v2 features: deferred props, prefetching, WhenVisible, InfiniteScroll, once props, flash data, or polling
|
||||
- Building React-specific features with the Inertia protocol
|
||||
|
||||
## Documentation
|
||||
|
||||
Use `search-docs` for detailed Inertia v2 React patterns and documentation.
|
||||
|
||||
## Basic Usage
|
||||
|
||||
### Page Components Location
|
||||
|
||||
React page components should be placed in the `resources/js/pages` directory.
|
||||
|
||||
### Page Component Structure
|
||||
|
||||
<!-- Basic React Page Component -->
|
||||
```react
|
||||
export default function UsersIndex({ users }) {
|
||||
return (
|
||||
<div>
|
||||
<h1>Users</h1>
|
||||
<ul>
|
||||
{users.map(user => <li key={user.id}>{user.name}</li>)}
|
||||
</ul>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## Client-Side Navigation
|
||||
|
||||
### Basic Link Component
|
||||
|
||||
Use `<Link>` for client-side navigation instead of traditional `<a>` tags:
|
||||
|
||||
<!-- Inertia React Navigation -->
|
||||
```react
|
||||
import { Link, router } from '@inertiajs/react'
|
||||
|
||||
<Link href="/">Home</Link>
|
||||
<Link href="/users">Users</Link>
|
||||
<Link href={`/users/${user.id}`}>View User</Link>
|
||||
```
|
||||
|
||||
### Link with Method
|
||||
|
||||
<!-- Link with POST Method -->
|
||||
```react
|
||||
import { Link } from '@inertiajs/react'
|
||||
|
||||
<Link href="/logout" method="post" as="button">
|
||||
Logout
|
||||
</Link>
|
||||
```
|
||||
|
||||
### Prefetching
|
||||
|
||||
Prefetch pages to improve perceived performance:
|
||||
|
||||
<!-- Prefetch on Hover -->
|
||||
```react
|
||||
import { Link } from '@inertiajs/react'
|
||||
|
||||
<Link href="/users" prefetch>
|
||||
Users
|
||||
</Link>
|
||||
```
|
||||
|
||||
### Programmatic Navigation
|
||||
|
||||
<!-- Router Visit -->
|
||||
```react
|
||||
import { router } from '@inertiajs/react'
|
||||
|
||||
function handleClick() {
|
||||
router.visit('/users')
|
||||
}
|
||||
|
||||
// Or with options
|
||||
router.visit('/users', {
|
||||
method: 'post',
|
||||
data: { name: 'John' },
|
||||
onSuccess: () => console.log('Success!'),
|
||||
})
|
||||
```
|
||||
|
||||
## Form Handling
|
||||
|
||||
### Form Component (Recommended)
|
||||
|
||||
The recommended way to build forms is with the `<Form>` component:
|
||||
|
||||
<!-- Form Component Example -->
|
||||
```react
|
||||
import { Form } from '@inertiajs/react'
|
||||
|
||||
export default function CreateUser() {
|
||||
return (
|
||||
<Form action="/users" method="post">
|
||||
{({ errors, processing, wasSuccessful }) => (
|
||||
<>
|
||||
<input type="text" name="name" />
|
||||
{errors.name && <div>{errors.name}</div>}
|
||||
|
||||
<input type="email" name="email" />
|
||||
{errors.email && <div>{errors.email}</div>}
|
||||
|
||||
<button type="submit" disabled={processing}>
|
||||
{processing ? 'Creating...' : 'Create User'}
|
||||
</button>
|
||||
|
||||
{wasSuccessful && <div>User created!</div>}
|
||||
</>
|
||||
)}
|
||||
</Form>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### Form Component With All Props
|
||||
|
||||
<!-- Form Component Full Example -->
|
||||
```react
|
||||
import { Form } from '@inertiajs/react'
|
||||
|
||||
<Form action="/users" method="post">
|
||||
{({
|
||||
errors,
|
||||
hasErrors,
|
||||
processing,
|
||||
progress,
|
||||
wasSuccessful,
|
||||
recentlySuccessful,
|
||||
clearErrors,
|
||||
resetAndClearErrors,
|
||||
defaults,
|
||||
isDirty,
|
||||
reset,
|
||||
submit
|
||||
}) => (
|
||||
<>
|
||||
<input type="text" name="name" defaultValue={defaults.name} />
|
||||
{errors.name && <div>{errors.name}</div>}
|
||||
|
||||
<button type="submit" disabled={processing}>
|
||||
{processing ? 'Saving...' : 'Save'}
|
||||
</button>
|
||||
|
||||
{progress && (
|
||||
<progress value={progress.percentage} max="100">
|
||||
{progress.percentage}%
|
||||
</progress>
|
||||
)}
|
||||
|
||||
{wasSuccessful && <div>Saved!</div>}
|
||||
</>
|
||||
)}
|
||||
</Form>
|
||||
```
|
||||
|
||||
### Form Component Reset Props
|
||||
|
||||
The `<Form>` component supports automatic resetting:
|
||||
|
||||
- `resetOnError` - Reset form data when the request fails
|
||||
- `resetOnSuccess` - Reset form data when the request succeeds
|
||||
- `setDefaultsOnSuccess` - Update default values on success
|
||||
|
||||
Use the `search-docs` tool with a query of `form component resetting` for detailed guidance.
|
||||
|
||||
<!-- Form with Reset Props -->
|
||||
```react
|
||||
import { Form } from '@inertiajs/react'
|
||||
|
||||
<Form
|
||||
action="/users"
|
||||
method="post"
|
||||
resetOnSuccess
|
||||
setDefaultsOnSuccess
|
||||
>
|
||||
{({ errors, processing, wasSuccessful }) => (
|
||||
<>
|
||||
<input type="text" name="name" />
|
||||
{errors.name && <div>{errors.name}</div>}
|
||||
|
||||
<button type="submit" disabled={processing}>
|
||||
Submit
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</Form>
|
||||
```
|
||||
|
||||
Forms can also be built using the `useForm` helper for more programmatic control. Use the `search-docs` tool with a query of `useForm helper` for guidance.
|
||||
|
||||
### `useForm` Hook
|
||||
|
||||
For more programmatic control or to follow existing conventions, use the `useForm` hook:
|
||||
|
||||
<!-- useForm Hook Example -->
|
||||
```react
|
||||
import { useForm } from '@inertiajs/react'
|
||||
|
||||
export default function CreateUser() {
|
||||
const { data, setData, post, processing, errors, reset } = useForm({
|
||||
name: '',
|
||||
email: '',
|
||||
password: '',
|
||||
})
|
||||
|
||||
function submit(e) {
|
||||
e.preventDefault()
|
||||
post('/users', {
|
||||
onSuccess: () => reset('password'),
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<form onSubmit={submit}>
|
||||
<input
|
||||
type="text"
|
||||
value={data.name}
|
||||
onChange={e => setData('name', e.target.value)}
|
||||
/>
|
||||
{errors.name && <div>{errors.name}</div>}
|
||||
|
||||
<input
|
||||
type="email"
|
||||
value={data.email}
|
||||
onChange={e => setData('email', e.target.value)}
|
||||
/>
|
||||
{errors.email && <div>{errors.email}</div>}
|
||||
|
||||
<input
|
||||
type="password"
|
||||
value={data.password}
|
||||
onChange={e => setData('password', e.target.value)}
|
||||
/>
|
||||
{errors.password && <div>{errors.password}</div>}
|
||||
|
||||
<button type="submit" disabled={processing}>
|
||||
Create User
|
||||
</button>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## Inertia v2 Features
|
||||
|
||||
### Deferred Props
|
||||
|
||||
Use deferred props to load data after initial page render:
|
||||
|
||||
<!-- Deferred Props with Empty State -->
|
||||
```react
|
||||
export default function UsersIndex({ users }) {
|
||||
// users will be undefined initially, then populated
|
||||
return (
|
||||
<div>
|
||||
<h1>Users</h1>
|
||||
{!users ? (
|
||||
<div className="animate-pulse">
|
||||
<div className="h-4 bg-gray-200 rounded w-3/4 mb-2"></div>
|
||||
<div className="h-4 bg-gray-200 rounded w-1/2"></div>
|
||||
</div>
|
||||
) : (
|
||||
<ul>
|
||||
{users.map(user => (
|
||||
<li key={user.id}>{user.name}</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### Polling
|
||||
|
||||
Automatically refresh data at intervals:
|
||||
|
||||
<!-- Polling Example -->
|
||||
```react
|
||||
import { router } from '@inertiajs/react'
|
||||
import { useEffect } from 'react'
|
||||
|
||||
export default function Dashboard({ stats }) {
|
||||
useEffect(() => {
|
||||
const interval = setInterval(() => {
|
||||
router.reload({ only: ['stats'] })
|
||||
}, 5000) // Poll every 5 seconds
|
||||
|
||||
return () => clearInterval(interval)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1>Dashboard</h1>
|
||||
<div>Active Users: {stats.activeUsers}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### WhenVisible
|
||||
|
||||
Lazy-load a prop when an element scrolls into view. Useful for deferring expensive data that sits below the fold:
|
||||
|
||||
<!-- WhenVisible Example -->
|
||||
```react
|
||||
import { WhenVisible } from '@inertiajs/react'
|
||||
|
||||
export default function Dashboard({ stats }) {
|
||||
return (
|
||||
<div>
|
||||
<h1>Dashboard</h1>
|
||||
|
||||
{/* stats prop is loaded only when this section scrolls into view */}
|
||||
<WhenVisible data="stats" buffer={200} fallback={<div className="animate-pulse">Loading stats...</div>}>
|
||||
{({ fetching }) => (
|
||||
<div>
|
||||
<p>Total Users: {stats.total_users}</p>
|
||||
<p>Revenue: {stats.revenue}</p>
|
||||
{fetching && <span>Refreshing...</span>}
|
||||
</div>
|
||||
)}
|
||||
</WhenVisible>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## Server-Side Patterns
|
||||
|
||||
Server-side patterns (Inertia::render, props, middleware) are covered in inertia-laravel guidelines.
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
- Using traditional `<a>` links instead of Inertia's `<Link>` component (breaks SPA behavior)
|
||||
- Forgetting to add loading states (skeleton screens) when using deferred props
|
||||
- Not handling the `undefined` state of deferred props before data loads
|
||||
- Using `<form>` without preventing default submission (use `<Form>` component or `e.preventDefault()`)
|
||||
- Forgetting to check if `<Form>` component is available in your Inertia version
|
||||
|
|
@ -1,77 +0,0 @@
|
|||
---
|
||||
name: pennant-development
|
||||
description: "Use when working with Laravel Pennant the official Laravel feature flag package. Trigger whenever the query mentions Pennant by name or involves feature flags or feature toggles in a Laravel project. Tasks include defining feature flags checking whether features are active creating class based features in `app/Features` using Blade `@feature` directives scoping flags to users or teams building custom Pennant storage drivers protecting routes with feature flags testing feature flags with Pest or PHPUnit and implementing A B testing or gradual rollouts with feature flags. Do not trigger for generic Laravel configuration authorization policies authentication or non Pennant feature management systems."
|
||||
license: MIT
|
||||
metadata:
|
||||
author: laravel
|
||||
---
|
||||
|
||||
# Pennant Features
|
||||
|
||||
## When to Apply
|
||||
|
||||
Activate this skill when:
|
||||
|
||||
- Creating or checking feature flags
|
||||
- Managing feature rollouts
|
||||
- Implementing A/B testing
|
||||
|
||||
## Documentation
|
||||
|
||||
Use `search-docs` for detailed Pennant patterns and documentation.
|
||||
|
||||
## Basic Usage
|
||||
|
||||
### Defining Features
|
||||
|
||||
<!-- Defining Features -->
|
||||
```php
|
||||
use Laravel\Pennant\Feature;
|
||||
|
||||
Feature::define('new-dashboard', function (User $user) {
|
||||
return $user->isAdmin();
|
||||
});
|
||||
```
|
||||
|
||||
### Checking Features
|
||||
|
||||
<!-- Checking Features -->
|
||||
```php
|
||||
if (Feature::active('new-dashboard')) {
|
||||
// Feature is active
|
||||
}
|
||||
|
||||
// With scope
|
||||
if (Feature::for($user)->active('new-dashboard')) {
|
||||
// Feature is active for this user
|
||||
}
|
||||
```
|
||||
|
||||
### Blade Directive
|
||||
|
||||
<!-- Blade Directive -->
|
||||
```blade
|
||||
@feature('new-dashboard')
|
||||
<x-new-dashboard />
|
||||
@else
|
||||
<x-old-dashboard />
|
||||
@endfeature
|
||||
```
|
||||
|
||||
### Activating / Deactivating
|
||||
|
||||
<!-- Activating Features -->
|
||||
```php
|
||||
Feature::activate('new-dashboard');
|
||||
Feature::for($user)->activate('new-dashboard');
|
||||
```
|
||||
|
||||
## Verification
|
||||
|
||||
1. Check feature flag is defined
|
||||
2. Test with different scopes/users
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
- Forgetting to scope features for specific users/entities
|
||||
- Not following existing naming conventions
|
||||
|
|
@ -1,157 +0,0 @@
|
|||
---
|
||||
name: pest-testing
|
||||
description: "Use this skill for Pest PHP testing in Laravel projects only. Trigger whenever any test is being written, edited, fixed, or refactored — including fixing tests that broke after a code change, adding assertions, converting PHPUnit to Pest, adding datasets, and TDD workflows. Always activate when the user asks how to write something in Pest, mentions test files or directories (tests/Feature, tests/Unit, tests/Browser), or needs browser testing, smoke testing multiple pages for JS errors, or architecture tests. Covers: it()/expect() syntax, datasets, mocking, browser testing (visit/click/fill), smoke testing, arch(), Livewire component tests, RefreshDatabase, and all Pest 4 features. Do not use for factories, seeders, migrations, controllers, models, or non-test PHP code."
|
||||
license: MIT
|
||||
metadata:
|
||||
author: laravel
|
||||
---
|
||||
|
||||
# Pest Testing 4
|
||||
|
||||
## Documentation
|
||||
|
||||
Use `search-docs` for detailed Pest 4 patterns and documentation.
|
||||
|
||||
## Basic Usage
|
||||
|
||||
### Creating Tests
|
||||
|
||||
All tests must be written using Pest. Use `php artisan make:test --pest {name}`.
|
||||
|
||||
### Test Organization
|
||||
|
||||
- Unit/Feature tests: `tests/Feature` and `tests/Unit` directories.
|
||||
- Browser tests: `tests/Browser/` directory.
|
||||
- Do NOT remove tests without approval - these are core application code.
|
||||
|
||||
### Basic Test Structure
|
||||
|
||||
<!-- Basic Pest Test Example -->
|
||||
```php
|
||||
it('is true', function () {
|
||||
expect(true)->toBeTrue();
|
||||
});
|
||||
```
|
||||
|
||||
### Running Tests
|
||||
|
||||
- Run minimal tests with filter before finalizing: `php artisan test --compact --filter=testName`.
|
||||
- Run all tests: `php artisan test --compact`.
|
||||
- Run file: `php artisan test --compact tests/Feature/ExampleTest.php`.
|
||||
|
||||
## Assertions
|
||||
|
||||
Use specific assertions (`assertSuccessful()`, `assertNotFound()`) instead of `assertStatus()`:
|
||||
|
||||
<!-- Pest Response Assertion -->
|
||||
```php
|
||||
it('returns all', function () {
|
||||
$this->postJson('/api/docs', [])->assertSuccessful();
|
||||
});
|
||||
```
|
||||
|
||||
| Use | Instead of |
|
||||
|-----|------------|
|
||||
| `assertSuccessful()` | `assertStatus(200)` |
|
||||
| `assertNotFound()` | `assertStatus(404)` |
|
||||
| `assertForbidden()` | `assertStatus(403)` |
|
||||
|
||||
## Mocking
|
||||
|
||||
Import mock function before use: `use function Pest\Laravel\mock;`
|
||||
|
||||
## Datasets
|
||||
|
||||
Use datasets for repetitive tests (validation rules, etc.):
|
||||
|
||||
<!-- Pest Dataset Example -->
|
||||
```php
|
||||
it('has emails', function (string $email) {
|
||||
expect($email)->not->toBeEmpty();
|
||||
})->with([
|
||||
'james' => 'james@laravel.com',
|
||||
'taylor' => 'taylor@laravel.com',
|
||||
]);
|
||||
```
|
||||
|
||||
## Pest 4 Features
|
||||
|
||||
| Feature | Purpose |
|
||||
|---------|---------|
|
||||
| Browser Testing | Full integration tests in real browsers |
|
||||
| Smoke Testing | Validate multiple pages quickly |
|
||||
| Visual Regression | Compare screenshots for visual changes |
|
||||
| Test Sharding | Parallel CI runs |
|
||||
| Architecture Testing | Enforce code conventions |
|
||||
|
||||
### Browser Test Example
|
||||
|
||||
Browser tests run in real browsers for full integration testing:
|
||||
|
||||
- Browser tests live in `tests/Browser/`.
|
||||
- Use Laravel features like `Event::fake()`, `assertAuthenticated()`, and model factories.
|
||||
- Use `RefreshDatabase` for clean state per test.
|
||||
- Interact with page: click, type, scroll, select, submit, drag-and-drop, touch gestures.
|
||||
- Test on multiple browsers (Chrome, Firefox, Safari) if requested.
|
||||
- Test on different devices/viewports (iPhone 14 Pro, tablets) if requested.
|
||||
- Switch color schemes (light/dark mode) when appropriate.
|
||||
- Take screenshots or pause tests for debugging.
|
||||
|
||||
<!-- Pest Browser Test Example -->
|
||||
```php
|
||||
it('may reset the password', function () {
|
||||
Notification::fake();
|
||||
|
||||
$this->actingAs(User::factory()->create());
|
||||
|
||||
$page = visit('/sign-in');
|
||||
|
||||
$page->assertSee('Sign In')
|
||||
->assertNoJavaScriptErrors()
|
||||
->click('Forgot Password?')
|
||||
->fill('email', 'nuno@laravel.com')
|
||||
->click('Send Reset Link')
|
||||
->assertSee('We have emailed your password reset link!');
|
||||
|
||||
Notification::assertSent(ResetPassword::class);
|
||||
});
|
||||
```
|
||||
|
||||
### Smoke Testing
|
||||
|
||||
Quickly validate multiple pages have no JavaScript errors:
|
||||
|
||||
<!-- Pest Smoke Testing Example -->
|
||||
```php
|
||||
$pages = visit(['/', '/about', '/contact']);
|
||||
|
||||
$pages->assertNoJavaScriptErrors()->assertNoConsoleLogs();
|
||||
```
|
||||
|
||||
### Visual Regression Testing
|
||||
|
||||
Capture and compare screenshots to detect visual changes.
|
||||
|
||||
### Test Sharding
|
||||
|
||||
Split tests across parallel processes for faster CI runs.
|
||||
|
||||
### Architecture Testing
|
||||
|
||||
Pest 4 includes architecture testing (from Pest 3):
|
||||
|
||||
<!-- Architecture Test Example -->
|
||||
```php
|
||||
arch('controllers')
|
||||
->expect('App\Http\Controllers')
|
||||
->toExtendNothing()
|
||||
->toHaveSuffix('Controller');
|
||||
```
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
- Not importing `use function Pest\Laravel\mock;` before using mock
|
||||
- Using `assertStatus(200)` instead of `assertSuccessful()`
|
||||
- Forgetting datasets for repetitive validation tests
|
||||
- Deleting tests without approval
|
||||
- Forgetting `assertNoJavaScriptErrors()` in browser tests
|
||||
|
|
@ -1,119 +0,0 @@
|
|||
---
|
||||
name: tailwindcss-development
|
||||
description: "Always invoke when the user's message includes 'tailwind' in any form. Also invoke for: building responsive grid layouts (multi-column card grids, product grids), flex/grid page structures (dashboards with sidebars, fixed topbars, mobile-toggle navs), styling UI components (cards, tables, navbars, pricing sections, forms, inputs, badges), adding dark mode variants, fixing spacing or typography, and Tailwind v3/v4 work. The core use case: writing or fixing Tailwind utility classes in HTML templates (Blade, JSX, Vue). Skip for backend PHP logic, database queries, API routes, JavaScript with no HTML/CSS component, CSS file audits, build tool configuration, and vanilla CSS."
|
||||
license: MIT
|
||||
metadata:
|
||||
author: laravel
|
||||
---
|
||||
|
||||
# Tailwind CSS Development
|
||||
|
||||
## Documentation
|
||||
|
||||
Use `search-docs` for detailed Tailwind CSS v4 patterns and documentation.
|
||||
|
||||
## Basic Usage
|
||||
|
||||
- Use Tailwind CSS classes to style HTML. Check and follow existing Tailwind conventions in the project before introducing new patterns.
|
||||
- Offer to extract repeated patterns into components that match the project's conventions (e.g., Blade, JSX, Vue).
|
||||
- Consider class placement, order, priority, and defaults. Remove redundant classes, add classes to parent or child elements carefully to reduce repetition, and group elements logically.
|
||||
|
||||
## Tailwind CSS v4 Specifics
|
||||
|
||||
- Always use Tailwind CSS v4 and avoid deprecated utilities.
|
||||
- `corePlugins` is not supported in Tailwind v4.
|
||||
|
||||
### CSS-First Configuration
|
||||
|
||||
In Tailwind v4, configuration is CSS-first using the `@theme` directive — no separate `tailwind.config.js` file is needed:
|
||||
|
||||
<!-- CSS-First Config -->
|
||||
```css
|
||||
@theme {
|
||||
--color-brand: oklch(0.72 0.11 178);
|
||||
}
|
||||
```
|
||||
|
||||
### Import Syntax
|
||||
|
||||
In Tailwind v4, import Tailwind with a regular CSS `@import` statement instead of the `@tailwind` directives used in v3:
|
||||
|
||||
<!-- v4 Import Syntax -->
|
||||
```diff
|
||||
- @tailwind base;
|
||||
- @tailwind components;
|
||||
- @tailwind utilities;
|
||||
+ @import "tailwindcss";
|
||||
```
|
||||
|
||||
### Replaced Utilities
|
||||
|
||||
Tailwind v4 removed deprecated utilities. Use the replacements shown below. Opacity values remain numeric.
|
||||
|
||||
| Deprecated | Replacement |
|
||||
|------------|-------------|
|
||||
| bg-opacity-* | bg-black/* |
|
||||
| text-opacity-* | text-black/* |
|
||||
| border-opacity-* | border-black/* |
|
||||
| divide-opacity-* | divide-black/* |
|
||||
| ring-opacity-* | ring-black/* |
|
||||
| placeholder-opacity-* | placeholder-black/* |
|
||||
| flex-shrink-* | shrink-* |
|
||||
| flex-grow-* | grow-* |
|
||||
| overflow-ellipsis | text-ellipsis |
|
||||
| decoration-slice | box-decoration-slice |
|
||||
| decoration-clone | box-decoration-clone |
|
||||
|
||||
## Spacing
|
||||
|
||||
Use `gap` utilities instead of margins for spacing between siblings:
|
||||
|
||||
<!-- Gap Utilities -->
|
||||
```html
|
||||
<div class="flex gap-8">
|
||||
<div>Item 1</div>
|
||||
<div>Item 2</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
## Dark Mode
|
||||
|
||||
If existing pages and components support dark mode, new pages and components must support it the same way, typically using the `dark:` variant:
|
||||
|
||||
<!-- Dark Mode -->
|
||||
```html
|
||||
<div class="bg-white dark:bg-gray-900 text-gray-900 dark:text-white">
|
||||
Content adapts to color scheme
|
||||
</div>
|
||||
```
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Flexbox Layout
|
||||
|
||||
<!-- Flexbox Layout -->
|
||||
```html
|
||||
<div class="flex items-center justify-between gap-4">
|
||||
<div>Left content</div>
|
||||
<div>Right content</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
### Grid Layout
|
||||
|
||||
<!-- Grid Layout -->
|
||||
```html
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
<div>Card 1</div>
|
||||
<div>Card 2</div>
|
||||
<div>Card 3</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
- Using deprecated v3 utilities (bg-opacity-*, flex-shrink-*, etc.)
|
||||
- Using `@tailwind` directives instead of `@import "tailwindcss"`
|
||||
- Trying to use `tailwind.config.js` instead of CSS `@theme` directive
|
||||
- Using margins for spacing between siblings instead of gap utilities
|
||||
- Forgetting to add dark mode variants when the project uses dark mode
|
||||
|
|
@ -1,80 +0,0 @@
|
|||
---
|
||||
name: wayfinder-development
|
||||
description: "Activates whenever referencing backend routes in frontend components. Use when importing from @/actions or @/routes, calling Laravel routes from TypeScript, or working with Wayfinder route functions."
|
||||
license: MIT
|
||||
metadata:
|
||||
author: laravel
|
||||
---
|
||||
|
||||
# Wayfinder Development
|
||||
|
||||
## Documentation
|
||||
|
||||
Use `search-docs` for detailed Wayfinder patterns and documentation.
|
||||
|
||||
## Quick Reference
|
||||
|
||||
### Generate Routes
|
||||
|
||||
Run after route changes if Vite plugin isn't installed:
|
||||
```bash
|
||||
php artisan wayfinder:generate --no-interaction
|
||||
```
|
||||
For form helpers, use `--with-form` flag:
|
||||
```bash
|
||||
php artisan wayfinder:generate --with-form --no-interaction
|
||||
```
|
||||
|
||||
### Import Patterns
|
||||
|
||||
<!-- Controller Action Imports -->
|
||||
```typescript
|
||||
// Named imports for tree-shaking (preferred)...
|
||||
import { show, store, update } from '@/actions/App/Http/Controllers/PostController'
|
||||
|
||||
// Named route imports...
|
||||
import { show as postShow } from '@/routes/post'
|
||||
```
|
||||
|
||||
### Common Methods
|
||||
|
||||
<!-- Wayfinder Methods -->
|
||||
```typescript
|
||||
// Get route object...
|
||||
show(1) // { url: "/posts/1", method: "get" }
|
||||
|
||||
// Get URL string...
|
||||
show.url(1) // "/posts/1"
|
||||
|
||||
// Specific HTTP methods...
|
||||
show.get(1)
|
||||
store.post()
|
||||
update.patch(1)
|
||||
destroy.delete(1)
|
||||
|
||||
// Form attributes for HTML forms...
|
||||
store.form() // { action: "/posts", method: "post" }
|
||||
|
||||
// Query parameters...
|
||||
show(1, { query: { page: 1 } }) // "/posts/1?page=1"
|
||||
```
|
||||
|
||||
## Wayfinder + Inertia
|
||||
|
||||
Use Wayfinder with the `<Form>` component:
|
||||
<!-- Wayfinder Form (React) -->
|
||||
```typescript
|
||||
<Form {...store.form()}><input name="title" /></Form>
|
||||
```
|
||||
|
||||
## Verification
|
||||
|
||||
1. Run `php artisan wayfinder:generate` to regenerate routes if Vite plugin isn't installed
|
||||
2. Check TypeScript imports resolve correctly
|
||||
3. Verify route URLs match expected paths
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
- Using default imports instead of named imports (breaks tree-shaking)
|
||||
- Forgetting to regenerate after route changes
|
||||
- Not using type-safe parameter objects for route model binding
|
||||
|
|
@ -17,29 +17,6 @@ on:
|
|||
default: true
|
||||
|
||||
jobs:
|
||||
build-assets:
|
||||
if: github.event_name != 'pull_request'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup PHP & Composer Deps
|
||||
uses: ./.github/actions/setup-php-deps
|
||||
|
||||
- name: Setup Bun & Node Deps
|
||||
uses: ./.github/actions/setup-bun-deps
|
||||
|
||||
- name: Build Assets
|
||||
run: bun run build
|
||||
|
||||
- name: Upload Build Artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: build-assets
|
||||
path: public/build
|
||||
retention-days: 1
|
||||
|
||||
tests:
|
||||
runs-on: ubuntu-latest
|
||||
services:
|
||||
|
|
@ -55,8 +32,24 @@ jobs:
|
|||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup PHP & Composer Deps
|
||||
uses: ./.github/actions/setup-php-deps
|
||||
- name: Setup PHP
|
||||
uses: shivammathur/setup-php@v2
|
||||
with:
|
||||
php-version: 8.4
|
||||
tools: composer:v2
|
||||
coverage: xdebug
|
||||
|
||||
- name: Setup Bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
|
||||
- name: Install Node Dependencies
|
||||
run: bun install --frozen-lockfile
|
||||
|
||||
- name: Install Dependencies
|
||||
run: composer install --no-interaction --prefer-dist --optimize-autoloader
|
||||
|
||||
- name: Build Assets
|
||||
run: bun run build
|
||||
|
||||
- name: Copy Environment File
|
||||
run: cp .env.example .env
|
||||
|
|
@ -64,18 +57,9 @@ jobs:
|
|||
- name: Generate Application Key
|
||||
run: php artisan key:generate
|
||||
|
||||
- name: Generate Passport Keys
|
||||
run: php artisan passport:keys --force
|
||||
|
||||
- name: Tests
|
||||
# --parallel splits the suite across workers via paratest. Each
|
||||
# worker creates its own "testing_test_<N>" database; the mysql
|
||||
# service runs as root which has CREATE on *.* so this Just Works
|
||||
# without extra grants. Performance suite is excluded because
|
||||
# concurrent load skews its timing-based assertions.
|
||||
run: ./vendor/bin/pest --exclude-testsuite=Browser,Performance --parallel --processes=4
|
||||
run: ./vendor/bin/pest --exclude-testsuite=Browser
|
||||
env:
|
||||
TESTCONTAINERS: false
|
||||
DB_CONNECTION: mysql
|
||||
DB_HOST: 127.0.0.1
|
||||
DB_PORT: 3306
|
||||
|
|
@ -84,25 +68,8 @@ jobs:
|
|||
DB_PASSWORD: password
|
||||
|
||||
browser-tests:
|
||||
if: ${{ !cancelled() && github.event_name == 'pull_request' }}
|
||||
runs-on: ubuntu-latest
|
||||
needs: browser-tests-matrix
|
||||
steps:
|
||||
- name: Aggregate shard results
|
||||
run: |
|
||||
if [ "${{ needs.browser-tests-matrix.result }}" != "success" ]; then
|
||||
echo "One or more browser-tests shards failed: ${{ needs.browser-tests-matrix.result }}"
|
||||
exit 1
|
||||
fi
|
||||
echo "All browser-tests shards passed."
|
||||
|
||||
browser-tests-matrix:
|
||||
if: github.event_name == 'pull_request'
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
shard: [1, 2, 3, 4, 5]
|
||||
services:
|
||||
mysql:
|
||||
image: mysql:8.0
|
||||
|
|
@ -116,27 +83,28 @@ jobs:
|
|||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup PHP & Composer Deps
|
||||
uses: ./.github/actions/setup-php-deps
|
||||
|
||||
- name: Setup Bun & Node Deps
|
||||
uses: ./.github/actions/setup-bun-deps
|
||||
|
||||
- name: Cache Playwright browsers
|
||||
id: playwright-cache
|
||||
uses: actions/cache@v4
|
||||
- name: Setup PHP
|
||||
uses: shivammathur/setup-php@v2
|
||||
with:
|
||||
path: ~/.cache/ms-playwright
|
||||
key: playwright-${{ runner.os }}-${{ hashFiles('bun.lock') }}
|
||||
restore-keys: playwright-${{ runner.os }}-
|
||||
php-version: 8.4
|
||||
tools: composer:v2
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: lts/*
|
||||
|
||||
- name: Setup Bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
|
||||
- name: Install Node Dependencies
|
||||
run: bun install --frozen-lockfile
|
||||
|
||||
- name: Install Playwright Browsers
|
||||
run: npx playwright install --with-deps chromium
|
||||
if: steps.playwright-cache.outputs.cache-hit != 'true'
|
||||
run: npx playwright install --with-deps
|
||||
|
||||
- name: Install Playwright system deps
|
||||
run: npx playwright install-deps chromium
|
||||
if: steps.playwright-cache.outputs.cache-hit == 'true'
|
||||
- name: Install PHP Dependencies
|
||||
run: composer install --no-interaction --prefer-dist --optimize-autoloader
|
||||
|
||||
- name: Build Assets
|
||||
run: bun run build
|
||||
|
|
@ -147,13 +115,9 @@ jobs:
|
|||
- name: Generate Application Key
|
||||
run: php artisan key:generate
|
||||
|
||||
- name: Generate Passport Keys
|
||||
run: php artisan passport:keys --force
|
||||
|
||||
- name: Browser Tests
|
||||
run: ./vendor/bin/pest --testsuite=Browser --ci --shard=${{ matrix.shard }}/6
|
||||
run: ./vendor/bin/pest --testsuite=Browser --ci
|
||||
env:
|
||||
TESTCONTAINERS: false
|
||||
DB_CONNECTION: mysql
|
||||
DB_HOST: 127.0.0.1
|
||||
DB_PORT: 3306
|
||||
|
|
@ -161,109 +125,26 @@ jobs:
|
|||
DB_USERNAME: root
|
||||
DB_PASSWORD: password
|
||||
|
||||
update-browser-shards:
|
||||
if: github.event_name == 'workflow_dispatch' && inputs.build_only == false
|
||||
runs-on: ubuntu-latest
|
||||
services:
|
||||
mysql:
|
||||
image: mysql:8.0
|
||||
env:
|
||||
MYSQL_ROOT_PASSWORD: password
|
||||
MYSQL_DATABASE: testing
|
||||
ports:
|
||||
- 3306:3306
|
||||
options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup PHP & Composer Deps
|
||||
uses: ./.github/actions/setup-php-deps
|
||||
|
||||
- name: Setup Bun & Node Deps
|
||||
uses: ./.github/actions/setup-bun-deps
|
||||
|
||||
- name: Cache Playwright browsers
|
||||
id: playwright-cache
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.cache/ms-playwright
|
||||
key: playwright-${{ runner.os }}-${{ hashFiles('bun.lock') }}
|
||||
restore-keys: playwright-${{ runner.os }}-
|
||||
|
||||
- name: Install Playwright Browsers
|
||||
run: npx playwright install --with-deps chromium
|
||||
if: steps.playwright-cache.outputs.cache-hit != 'true'
|
||||
|
||||
- name: Install Playwright system deps
|
||||
run: npx playwright install-deps chromium
|
||||
if: steps.playwright-cache.outputs.cache-hit == 'true'
|
||||
|
||||
- name: Build Assets
|
||||
run: bun run build
|
||||
|
||||
- name: Copy Environment File
|
||||
run: cp .env.example .env
|
||||
|
||||
- name: Generate Application Key
|
||||
run: php artisan key:generate
|
||||
|
||||
- name: Update Browser Shards
|
||||
run: ./vendor/bin/pest --testsuite=Browser --ci --update-shards
|
||||
env:
|
||||
TESTCONTAINERS: false
|
||||
DB_CONNECTION: mysql
|
||||
DB_HOST: 127.0.0.1
|
||||
DB_PORT: 3306
|
||||
DB_DATABASE: testing
|
||||
DB_USERNAME: root
|
||||
DB_PASSWORD: password
|
||||
|
||||
- name: Upload Browser Shards
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: browser-shards
|
||||
path: tests/.pest/shards.json
|
||||
retention-days: 1
|
||||
|
||||
static-analysis:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Setup PHP & Composer Deps
|
||||
uses: ./.github/actions/setup-php-deps
|
||||
with:
|
||||
composer-args: '-q --no-ansi --no-interaction --no-scripts --no-progress --prefer-dist'
|
||||
|
||||
- name: Run PHPStan
|
||||
run: vendor/bin/phpstan analyse --memory-limit=512M --no-progress
|
||||
|
||||
linter:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: read
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Setup PHP & Composer Deps
|
||||
uses: ./.github/actions/setup-php-deps
|
||||
- name: Setup PHP
|
||||
uses: shivammathur/setup-php@v2
|
||||
with:
|
||||
composer-args: '-q --no-ansi --no-interaction --no-scripts --no-progress --prefer-dist'
|
||||
php-version: '8.4'
|
||||
|
||||
- name: Setup Bun & Node Deps
|
||||
uses: ./.github/actions/setup-bun-deps
|
||||
- name: Setup Bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
|
||||
- name: Cache Pint results
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: .pint.cache
|
||||
key: pint-${{ runner.os }}-${{ hashFiles('composer.lock', 'pint.json') }}
|
||||
restore-keys: pint-${{ runner.os }}-
|
||||
- name: Install Dependencies
|
||||
run: |
|
||||
composer install -q --no-ansi --no-interaction --no-scripts --no-progress --prefer-dist
|
||||
bun install --frozen-lockfile
|
||||
|
||||
- name: Run Pint
|
||||
run: vendor/bin/pint --test --parallel --cache-file=.pint.cache
|
||||
run: vendor/bin/pint --test
|
||||
|
||||
- name: Format Frontend
|
||||
run: bun run format
|
||||
|
|
@ -271,158 +152,22 @@ jobs:
|
|||
- name: Lint Frontend
|
||||
run: bun run lint
|
||||
|
||||
# Advisory for now: the codebase carries a backlog of pre-existing
|
||||
# `tsc --noEmit` errors that predate this step, so it must not gate
|
||||
# merges yet. It still surfaces type regressions in the CI log. Flip
|
||||
# `continue-on-error` to false once the existing backlog is cleared.
|
||||
- name: Type Check Frontend
|
||||
run: bun run types
|
||||
continue-on-error: true
|
||||
|
||||
- name: Frontend Tests
|
||||
run: bun run test
|
||||
|
||||
# Gates merges on a Conventional-Commit PR title. Lives in the linter
|
||||
# job (a required check) so a bad title blocks the merge, unlike the old
|
||||
# standalone workflow whose check wasn't required. Skipped on push since
|
||||
# there's no PR title to validate then.
|
||||
- name: Conventional PR title
|
||||
if: github.event_name == 'pull_request'
|
||||
uses: amannn/action-semantic-pull-request@v5
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
types: |
|
||||
feat
|
||||
fix
|
||||
chore
|
||||
docs
|
||||
style
|
||||
refactor
|
||||
perf
|
||||
test
|
||||
build
|
||||
ci
|
||||
revert
|
||||
requireScope: false
|
||||
subjectPattern: ^(?![A-Z]).+$
|
||||
subjectPatternError: |
|
||||
Subject must not start with uppercase. Use: feat: add checkout, fix(billing): handle retry.
|
||||
|
||||
performance-tests:
|
||||
runs-on: ubuntu-latest
|
||||
services:
|
||||
mysql:
|
||||
image: mysql:8.0
|
||||
env:
|
||||
MYSQL_ROOT_PASSWORD: password
|
||||
MYSQL_DATABASE: testing
|
||||
ports:
|
||||
- 3306:3306
|
||||
options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup PHP & Composer Deps
|
||||
uses: ./.github/actions/setup-php-deps
|
||||
|
||||
- name: Copy Environment File
|
||||
run: cp .env.example .env
|
||||
|
||||
- name: Generate Application Key
|
||||
run: php artisan key:generate
|
||||
|
||||
- name: Performance Tests
|
||||
run: ./vendor/bin/pest --testsuite=Performance
|
||||
env:
|
||||
TESTCONTAINERS: false
|
||||
DB_CONNECTION: mysql
|
||||
DB_HOST: 127.0.0.1
|
||||
DB_PORT: 3306
|
||||
DB_DATABASE: testing
|
||||
DB_USERNAME: root
|
||||
DB_PASSWORD: password
|
||||
|
||||
changes:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
code: ${{ steps.filter.outputs.code }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dorny/paths-filter@v3
|
||||
id: filter
|
||||
with:
|
||||
filters: |
|
||||
code:
|
||||
- 'app/**'
|
||||
- 'bootstrap/**'
|
||||
- 'config/**'
|
||||
- 'database/**'
|
||||
- 'public/**'
|
||||
- 'resources/**'
|
||||
- 'routes/**'
|
||||
- 'storage/**'
|
||||
- 'artisan'
|
||||
- 'composer.json'
|
||||
- 'composer.lock'
|
||||
- 'package.json'
|
||||
- 'package-lock.json'
|
||||
- 'bun.lock'
|
||||
- 'bun.lockb'
|
||||
- 'vite.config.*'
|
||||
- 'tsconfig*.json'
|
||||
- 'eslint.config.*'
|
||||
- '.prettierrc*'
|
||||
- 'phpstan.neon*'
|
||||
- 'phpunit.xml*'
|
||||
- 'pint.json'
|
||||
- 'Dockerfile'
|
||||
- 'Dockerfile.*'
|
||||
- 'docker/**'
|
||||
- '.dockerignore'
|
||||
- '.env.example'
|
||||
- '.env.production.example'
|
||||
- 'docker-compose.production.yml'
|
||||
- 'docker-compose.production.local.yml'
|
||||
- 'templates/coolify/**'
|
||||
- '.github/workflows/ci.yml'
|
||||
|
||||
build-image:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 45
|
||||
needs: [tests, linter, static-analysis, performance-tests, changes]
|
||||
if: ((github.ref == 'refs/heads/main' && github.event_name == 'push') || github.event_name == 'workflow_dispatch') && needs.changes.outputs.code == 'true'
|
||||
outputs:
|
||||
development-digest: ${{ steps.development-image.outputs.digest }}
|
||||
production-digest: ${{ steps.production-image.outputs.digest }}
|
||||
package-version: ${{ steps.package-version.outputs.version }}
|
||||
env:
|
||||
SENTRY_RELEASE: whisper-money@${{ github.sha }}
|
||||
needs: [tests, linter]
|
||||
if: (github.ref == 'refs/heads/main' && github.event_name == 'push') || github.event_name == 'workflow_dispatch'
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Install Sentry CLI
|
||||
run: curl -sL https://sentry.io/get-cli/ | bash
|
||||
|
||||
- name: Create Sentry release
|
||||
env:
|
||||
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||
SENTRY_ORG: ${{ secrets.SENTRY_ORG }}
|
||||
SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }}
|
||||
run: |
|
||||
if ! sentry-cli releases info "$SENTRY_RELEASE" >/dev/null 2>&1; then
|
||||
sentry-cli releases new "$SENTRY_RELEASE"
|
||||
fi
|
||||
|
||||
sentry-cli releases set-commits "$SENTRY_RELEASE" --auto --ignore-missing
|
||||
sentry-cli releases finalize "$SENTRY_RELEASE"
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
|
@ -437,161 +182,49 @@ jobs:
|
|||
- name: Extract metadata
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ghcr.io/${{ github.repository }}
|
||||
tags: |
|
||||
type=sha,prefix=,suffix=-development
|
||||
type=raw,value=development
|
||||
|
||||
- name: Extract package version
|
||||
id: package-version
|
||||
run: node -e "console.log('version=' + require('./package.json').version)" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Extract production metadata
|
||||
id: production-meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ghcr.io/${{ github.repository }}
|
||||
tags: |
|
||||
type=sha,prefix=
|
||||
type=raw,value=latest
|
||||
type=raw,value=production
|
||||
type=raw,value=v${{ steps.package-version.outputs.version }}-production
|
||||
|
||||
- name: Build and push development image
|
||||
id: development-image
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v6
|
||||
timeout-minutes: 20
|
||||
with:
|
||||
context: .
|
||||
platforms: linux/amd64
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
build-args: |
|
||||
SENTRY_RELEASE=${{ env.SENTRY_RELEASE }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
|
||||
- name: Build and push production image
|
||||
id: production-image
|
||||
uses: docker/build-push-action@v6
|
||||
timeout-minutes: 25
|
||||
with:
|
||||
context: .
|
||||
file: Dockerfile.production
|
||||
platforms: linux/amd64
|
||||
push: true
|
||||
tags: ${{ steps.production-meta.outputs.tags }}
|
||||
labels: ${{ steps.production-meta.outputs.labels }}
|
||||
build-args: |
|
||||
SENTRY_RELEASE=${{ env.SENTRY_RELEASE }}
|
||||
cache-from: type=gha,scope=production
|
||||
cache-to: type=gha,mode=max,scope=production
|
||||
|
||||
build-arm64-images:
|
||||
runs-on: ubuntu-24.04-arm
|
||||
timeout-minutes: 75
|
||||
needs: build-image
|
||||
if: needs.build-image.result == 'success'
|
||||
env:
|
||||
SENTRY_RELEASE: whisper-money@${{ github.sha }}
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [tests, linter]
|
||||
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Extract metadata
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ghcr.io/${{ github.repository }}
|
||||
tags: |
|
||||
type=sha,prefix=,suffix=-development
|
||||
type=raw,value=development
|
||||
|
||||
- name: Extract production metadata
|
||||
id: production-meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ghcr.io/${{ github.repository }}
|
||||
tags: |
|
||||
type=sha,prefix=
|
||||
type=raw,value=latest
|
||||
type=raw,value=production
|
||||
type=raw,value=v${{ needs.build-image.outputs.package-version }}-production
|
||||
|
||||
- name: Build and push arm64 development image by digest
|
||||
id: development-arm64
|
||||
uses: docker/build-push-action@v6
|
||||
timeout-minutes: 30
|
||||
with:
|
||||
context: .
|
||||
platforms: linux/arm64
|
||||
tags: ghcr.io/${{ github.repository }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
outputs: type=image,push-by-digest=true,name-canonical=true,push=true
|
||||
build-args: |
|
||||
SENTRY_RELEASE=${{ env.SENTRY_RELEASE }}
|
||||
cache-from: type=gha,scope=arm64
|
||||
cache-to: type=gha,mode=max,scope=arm64
|
||||
|
||||
- name: Publish multi-platform development manifests
|
||||
env:
|
||||
AMD64_DIGEST: ${{ needs.build-image.outputs.development-digest }}
|
||||
ARM64_DIGEST: ${{ steps.development-arm64.outputs.digest }}
|
||||
IMAGE: ghcr.io/${{ github.repository }}
|
||||
TAGS: ${{ steps.meta.outputs.tags }}
|
||||
- name: Trigger deployment
|
||||
run: |
|
||||
while IFS= read -r tag; do
|
||||
[ -n "$tag" ] || continue
|
||||
response=$(curl -s -w "\n%{http_code}" \
|
||||
--connect-timeout 30 \
|
||||
--max-time 120 \
|
||||
--retry 3 \
|
||||
--retry-delay 5 \
|
||||
--retry-connrefused \
|
||||
-H "Authorization: Bearer ${{ secrets.DEPLOYMENT_TOKEN }}" \
|
||||
"http://147.93.126.54:8000/api/v1/deploy?uuid=ww00sswosco8w80k08c0occ8&force=false")
|
||||
|
||||
docker buildx imagetools create \
|
||||
--tag "$tag" \
|
||||
"$IMAGE@$AMD64_DIGEST" \
|
||||
"$IMAGE@$ARM64_DIGEST"
|
||||
done <<< "$TAGS"
|
||||
http_code=$(echo "$response" | tail -n1)
|
||||
body=$(echo "$response" | sed '$d')
|
||||
|
||||
- name: Build and push arm64 production image by digest
|
||||
id: production-arm64
|
||||
uses: docker/build-push-action@v6
|
||||
timeout-minutes: 40
|
||||
with:
|
||||
context: .
|
||||
file: Dockerfile.production
|
||||
platforms: linux/arm64
|
||||
tags: ghcr.io/${{ github.repository }}
|
||||
labels: ${{ steps.production-meta.outputs.labels }}
|
||||
outputs: type=image,push-by-digest=true,name-canonical=true,push=true
|
||||
build-args: |
|
||||
SENTRY_RELEASE=${{ env.SENTRY_RELEASE }}
|
||||
cache-from: type=gha,scope=production-arm64
|
||||
cache-to: type=gha,mode=max,scope=production-arm64
|
||||
echo "Response: $body"
|
||||
echo "HTTP Status: $http_code"
|
||||
|
||||
- name: Publish multi-platform production manifests
|
||||
env:
|
||||
AMD64_DIGEST: ${{ needs.build-image.outputs.production-digest }}
|
||||
ARM64_DIGEST: ${{ steps.production-arm64.outputs.digest }}
|
||||
IMAGE: ghcr.io/${{ github.repository }}
|
||||
TAGS: ${{ steps.production-meta.outputs.tags }}
|
||||
run: |
|
||||
while IFS= read -r tag; do
|
||||
[ -n "$tag" ] || continue
|
||||
if [ "$http_code" -lt 200 ] || [ "$http_code" -ge 300 ]; then
|
||||
echo "Deployment request failed with status $http_code"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Deployment triggered successfully!"
|
||||
|
||||
docker buildx imagetools create \
|
||||
--tag "$tag" \
|
||||
"$IMAGE@$AMD64_DIGEST" \
|
||||
"$IMAGE@$ARM64_DIGEST"
|
||||
done <<< "$TAGS"
|
||||
|
|
|
|||
|
|
@ -1,86 +0,0 @@
|
|||
name: Release
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: "0 8 * * 1" # Mondays 08:00 UTC = 10:00 Madrid (CEST) / 09:00 (CET)
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
increment:
|
||||
description: "Version increment"
|
||||
required: true
|
||||
default: patch
|
||||
type: choice
|
||||
options:
|
||||
- patch
|
||||
- minor
|
||||
- major
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
release:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout main
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: main
|
||||
fetch-depth: 0
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
cache: npm
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Configure git
|
||||
run: |
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||
|
||||
- name: Check for releasable commits
|
||||
id: guard
|
||||
run: |
|
||||
LAST=$(git describe --tags --abbrev=0)
|
||||
if [ -z "$(git log "$LAST"..HEAD --oneline)" ]; then
|
||||
echo "No commits since $LAST; skipping release."
|
||||
echo "skip=true" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "skip=false" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
- name: Create release branch
|
||||
id: branch
|
||||
if: steps.guard.outputs.skip != 'true'
|
||||
run: |
|
||||
BRANCH="release/run-${{ github.run_id }}"
|
||||
git checkout -b "$BRANCH"
|
||||
echo "name=$BRANCH" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Run release-it
|
||||
if: steps.guard.outputs.skip != 'true'
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: npx release-it "${{ inputs.increment || 'patch' }}" --ci
|
||||
|
||||
- name: Read new version
|
||||
id: version
|
||||
if: steps.guard.outputs.skip != 'true'
|
||||
run: echo "value=$(node -p "require('./package.json').version")" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Open pull request
|
||||
if: steps.guard.outputs.skip != 'true'
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
gh pr create \
|
||||
--base main \
|
||||
--head "${{ steps.branch.outputs.name }}" \
|
||||
--title "chore: release v${{ steps.version.outputs.value }}" \
|
||||
--body "Automated release PR for **v${{ steps.version.outputs.value }}**.\n\nTag \`v${{ steps.version.outputs.value }}\` and the GitHub release have already been published. Merge this PR to land the version bump and CHANGELOG on \`main\`."
|
||||
|
|
@ -1,7 +1,6 @@
|
|||
/.phpunit.cache
|
||||
/bootstrap/ssr
|
||||
/node_modules
|
||||
node_modules/
|
||||
/public/build
|
||||
/public/hot
|
||||
/public/storage
|
||||
|
|
@ -9,7 +8,6 @@ node_modules/
|
|||
/resources/js/routes
|
||||
/resources/js/wayfinder
|
||||
/storage/*.key
|
||||
/storage/keys/
|
||||
/storage/pail
|
||||
/vendor
|
||||
.DS_Store
|
||||
|
|
@ -29,7 +27,5 @@ yarn-error.log
|
|||
/.vscode
|
||||
/.zed
|
||||
.php-cs-fixer.cache
|
||||
/traefik/certs/*.pem
|
||||
.claude/settings.local.json
|
||||
.php-version
|
||||
.pint.cache
|
||||
/.playwright-mcp
|
||||
|
|
|
|||
|
|
@ -6,10 +6,6 @@
|
|||
"artisan",
|
||||
"boost:mcp"
|
||||
]
|
||||
},
|
||||
"sentry": {
|
||||
"type": "http",
|
||||
"url": "https://mcp.sentry.dev/mcp"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,369 +0,0 @@
|
|||
---
|
||||
name: inertia-react-development
|
||||
description: >-
|
||||
Develops Inertia.js v2 React client-side applications. Activates when creating
|
||||
React pages, forms, or navigation; using <Link>, <Form>, useForm, or router;
|
||||
working with deferred props, prefetching, or polling; or when user mentions
|
||||
React with Inertia, React pages, React forms, or React navigation.
|
||||
---
|
||||
|
||||
# Inertia React Development
|
||||
|
||||
## When to Apply
|
||||
|
||||
Activate this skill when:
|
||||
|
||||
- Creating or modifying React page components for Inertia
|
||||
- Working with forms in React (using `<Form>` or `useForm`)
|
||||
- Implementing client-side navigation with `<Link>` or `router`
|
||||
- Using v2 features: deferred props, prefetching, or polling
|
||||
- Building React-specific features with the Inertia protocol
|
||||
|
||||
## Documentation
|
||||
|
||||
Use `search-docs` for detailed Inertia v2 React patterns and documentation.
|
||||
|
||||
## Basic Usage
|
||||
|
||||
### Page Components Location
|
||||
|
||||
React page components should be placed in the `resources/js/Pages` directory.
|
||||
|
||||
### Page Component Structure
|
||||
|
||||
<code-snippet name="Basic React Page Component" lang="react">
|
||||
|
||||
export default function UsersIndex({ users }) {
|
||||
return (
|
||||
<div>
|
||||
<h1>Users</h1>
|
||||
<ul>
|
||||
{users.map(user => <li key={user.id}>{user.name}</li>)}
|
||||
</ul>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
</code-snippet>
|
||||
|
||||
## Client-Side Navigation
|
||||
|
||||
### Basic Link Component
|
||||
|
||||
Use `<Link>` for client-side navigation instead of traditional `<a>` tags:
|
||||
|
||||
<code-snippet name="Inertia React Navigation" lang="react">
|
||||
|
||||
import { Link, router } from '@inertiajs/react'
|
||||
|
||||
<Link href="/">Home</Link>
|
||||
<Link href="/users">Users</Link>
|
||||
<Link href={`/users/${user.id}`}>View User</Link>
|
||||
|
||||
</code-snippet>
|
||||
|
||||
### Link with Method
|
||||
|
||||
<code-snippet name="Link with POST Method" lang="react">
|
||||
|
||||
import { Link } from '@inertiajs/react'
|
||||
|
||||
<Link href="/logout" method="post" as="button">
|
||||
Logout
|
||||
</Link>
|
||||
|
||||
</code-snippet>
|
||||
|
||||
### Prefetching
|
||||
|
||||
Prefetch pages to improve perceived performance:
|
||||
|
||||
<code-snippet name="Prefetch on Hover" lang="react">
|
||||
|
||||
import { Link } from '@inertiajs/react'
|
||||
|
||||
<Link href="/users" prefetch>
|
||||
Users
|
||||
</Link>
|
||||
|
||||
</code-snippet>
|
||||
|
||||
### Programmatic Navigation
|
||||
|
||||
<code-snippet name="Router Visit" lang="react">
|
||||
|
||||
import { router } from '@inertiajs/react'
|
||||
|
||||
function handleClick() {
|
||||
router.visit('/users')
|
||||
}
|
||||
|
||||
// Or with options
|
||||
router.visit('/users', {
|
||||
method: 'post',
|
||||
data: { name: 'John' },
|
||||
onSuccess: () => console.log('Success!'),
|
||||
})
|
||||
|
||||
</code-snippet>
|
||||
|
||||
## Form Handling
|
||||
|
||||
### Form Component (Recommended)
|
||||
|
||||
The recommended way to build forms is with the `<Form>` component:
|
||||
|
||||
<code-snippet name="Form Component Example" lang="react">
|
||||
|
||||
import { Form } from '@inertiajs/react'
|
||||
|
||||
export default function CreateUser() {
|
||||
return (
|
||||
<Form action="/users" method="post">
|
||||
{({ errors, processing, wasSuccessful }) => (
|
||||
<>
|
||||
<input type="text" name="name" />
|
||||
{errors.name && <div>{errors.name}</div>}
|
||||
|
||||
<input type="email" name="email" />
|
||||
{errors.email && <div>{errors.email}</div>}
|
||||
|
||||
<button type="submit" disabled={processing}>
|
||||
{processing ? 'Creating...' : 'Create User'}
|
||||
</button>
|
||||
|
||||
{wasSuccessful && <div>User created!</div>}
|
||||
</>
|
||||
)}
|
||||
</Form>
|
||||
)
|
||||
}
|
||||
|
||||
</code-snippet>
|
||||
|
||||
### Form Component With All Props
|
||||
|
||||
<code-snippet name="Form Component Full Example" lang="react">
|
||||
|
||||
import { Form } from '@inertiajs/react'
|
||||
|
||||
<Form action="/users" method="post">
|
||||
{({
|
||||
errors,
|
||||
hasErrors,
|
||||
processing,
|
||||
progress,
|
||||
wasSuccessful,
|
||||
recentlySuccessful,
|
||||
clearErrors,
|
||||
resetAndClearErrors,
|
||||
defaults,
|
||||
isDirty,
|
||||
reset,
|
||||
submit
|
||||
}) => (
|
||||
<>
|
||||
<input type="text" name="name" defaultValue={defaults.name} />
|
||||
{errors.name && <div>{errors.name}</div>}
|
||||
|
||||
<button type="submit" disabled={processing}>
|
||||
{processing ? 'Saving...' : 'Save'}
|
||||
</button>
|
||||
|
||||
{progress && (
|
||||
<progress value={progress.percentage} max="100">
|
||||
{progress.percentage}%
|
||||
</progress>
|
||||
)}
|
||||
|
||||
{wasSuccessful && <div>Saved!</div>}
|
||||
</>
|
||||
)}
|
||||
</Form>
|
||||
|
||||
</code-snippet>
|
||||
|
||||
### Form Component Reset Props
|
||||
|
||||
The `<Form>` component supports automatic resetting:
|
||||
|
||||
- `resetOnError` - Reset form data when the request fails
|
||||
- `resetOnSuccess` - Reset form data when the request succeeds
|
||||
- `setDefaultsOnSuccess` - Update default values on success
|
||||
|
||||
Use the `search-docs` tool with a query of `form component resetting` for detailed guidance.
|
||||
|
||||
<code-snippet name="Form with Reset Props" lang="react">
|
||||
|
||||
import { Form } from '@inertiajs/react'
|
||||
|
||||
<Form
|
||||
action="/users"
|
||||
method="post"
|
||||
resetOnSuccess
|
||||
setDefaultsOnSuccess
|
||||
>
|
||||
{({ errors, processing, wasSuccessful }) => (
|
||||
<>
|
||||
<input type="text" name="name" />
|
||||
{errors.name && <div>{errors.name}</div>}
|
||||
|
||||
<button type="submit" disabled={processing}>
|
||||
Submit
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</Form>
|
||||
|
||||
</code-snippet>
|
||||
|
||||
Forms can also be built using the `useForm` helper for more programmatic control. Use the `search-docs` tool with a query of `useForm helper` for guidance.
|
||||
|
||||
### `useForm` Hook
|
||||
|
||||
For more programmatic control or to follow existing conventions, use the `useForm` hook:
|
||||
|
||||
<code-snippet name="useForm Hook Example" lang="react">
|
||||
|
||||
import { useForm } from '@inertiajs/react'
|
||||
|
||||
export default function CreateUser() {
|
||||
const { data, setData, post, processing, errors, reset } = useForm({
|
||||
name: '',
|
||||
email: '',
|
||||
password: '',
|
||||
})
|
||||
|
||||
function submit(e) {
|
||||
e.preventDefault()
|
||||
post('/users', {
|
||||
onSuccess: () => reset('password'),
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<form onSubmit={submit}>
|
||||
<input
|
||||
type="text"
|
||||
value={data.name}
|
||||
onChange={e => setData('name', e.target.value)}
|
||||
/>
|
||||
{errors.name && <div>{errors.name}</div>}
|
||||
|
||||
<input
|
||||
type="email"
|
||||
value={data.email}
|
||||
onChange={e => setData('email', e.target.value)}
|
||||
/>
|
||||
{errors.email && <div>{errors.email}</div>}
|
||||
|
||||
<input
|
||||
type="password"
|
||||
value={data.password}
|
||||
onChange={e => setData('password', e.target.value)}
|
||||
/>
|
||||
{errors.password && <div>{errors.password}</div>}
|
||||
|
||||
<button type="submit" disabled={processing}>
|
||||
Create User
|
||||
</button>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
|
||||
</code-snippet>
|
||||
|
||||
## Inertia v2 Features
|
||||
|
||||
### Deferred Props
|
||||
|
||||
Use deferred props to load data after initial page render:
|
||||
|
||||
<code-snippet name="Deferred Props with Empty State" lang="react">
|
||||
|
||||
export default function UsersIndex({ users }) {
|
||||
// users will be undefined initially, then populated
|
||||
return (
|
||||
<div>
|
||||
<h1>Users</h1>
|
||||
{!users ? (
|
||||
<div className="animate-pulse">
|
||||
<div className="h-4 bg-gray-200 rounded w-3/4 mb-2"></div>
|
||||
<div className="h-4 bg-gray-200 rounded w-1/2"></div>
|
||||
</div>
|
||||
) : (
|
||||
<ul>
|
||||
{users.map(user => (
|
||||
<li key={user.id}>{user.name}</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
</code-snippet>
|
||||
|
||||
### Polling
|
||||
|
||||
Automatically refresh data at intervals:
|
||||
|
||||
<code-snippet name="Polling Example" lang="react">
|
||||
|
||||
import { router } from '@inertiajs/react'
|
||||
import { useEffect } from 'react'
|
||||
|
||||
export default function Dashboard({ stats }) {
|
||||
useEffect(() => {
|
||||
const interval = setInterval(() => {
|
||||
router.reload({ only: ['stats'] })
|
||||
}, 5000) // Poll every 5 seconds
|
||||
|
||||
return () => clearInterval(interval)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1>Dashboard</h1>
|
||||
<div>Active Users: {stats.activeUsers}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
</code-snippet>
|
||||
|
||||
### WhenVisible (Infinite Scroll)
|
||||
|
||||
Load more data when user scrolls to a specific element:
|
||||
|
||||
<code-snippet name="Infinite Scroll with WhenVisible" lang="react">
|
||||
|
||||
import { WhenVisible } from '@inertiajs/react'
|
||||
|
||||
export default function UsersList({ users }) {
|
||||
return (
|
||||
<div>
|
||||
{users.data.map(user => (
|
||||
<div key={user.id}>{user.name}</div>
|
||||
))}
|
||||
|
||||
{users.next_page_url && (
|
||||
<WhenVisible
|
||||
data="users"
|
||||
params={{ page: users.current_page + 1 }}
|
||||
fallback={<div>Loading more...</div>}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
</code-snippet>
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
- Using traditional `<a>` links instead of Inertia's `<Link>` component (breaks SPA behavior)
|
||||
- Forgetting to add loading states (skeleton screens) when using deferred props
|
||||
- Not handling the `undefined` state of deferred props before data loads
|
||||
- Using `<form>` without preventing default submission (use `<Form>` component or `e.preventDefault()`)
|
||||
- Forgetting to check if `<Form>` component is available in your Inertia version
|
||||
|
|
@ -1,74 +0,0 @@
|
|||
---
|
||||
name: pennant-development
|
||||
description: >-
|
||||
Manages feature flags with Laravel Pennant. Activates when creating, checking, or toggling
|
||||
feature flags; showing or hiding features conditionally; implementing A/B testing; working with
|
||||
@feature directive; or when the user mentions feature flags, feature toggles, Pennant, conditional
|
||||
features, rollouts, or gradually enabling features.
|
||||
---
|
||||
|
||||
# Pennant Features
|
||||
|
||||
## When to Apply
|
||||
|
||||
Activate this skill when:
|
||||
|
||||
- Creating or checking feature flags
|
||||
- Managing feature rollouts
|
||||
- Implementing A/B testing
|
||||
|
||||
## Documentation
|
||||
|
||||
Use `search-docs` for detailed Pennant patterns and documentation.
|
||||
|
||||
## Basic Usage
|
||||
|
||||
### Defining Features
|
||||
|
||||
<code-snippet name="Defining Features" lang="php">
|
||||
use Laravel\Pennant\Feature;
|
||||
|
||||
Feature::define('new-dashboard', function (User $user) {
|
||||
return $user->isAdmin();
|
||||
});
|
||||
</code-snippet>
|
||||
|
||||
### Checking Features
|
||||
|
||||
<code-snippet name="Checking Features" lang="php">
|
||||
if (Feature::active('new-dashboard')) {
|
||||
// Feature is active
|
||||
}
|
||||
|
||||
// With scope
|
||||
if (Feature::for($user)->active('new-dashboard')) {
|
||||
// Feature is active for this user
|
||||
}
|
||||
</code-snippet>
|
||||
|
||||
### Blade Directive
|
||||
|
||||
<code-snippet name="Blade Directive" lang="blade">
|
||||
@feature('new-dashboard')
|
||||
<x-new-dashboard />
|
||||
@else
|
||||
<x-old-dashboard />
|
||||
@endfeature
|
||||
</code-snippet>
|
||||
|
||||
### Activating / Deactivating
|
||||
|
||||
<code-snippet name="Activating Features" lang="php">
|
||||
Feature::activate('new-dashboard');
|
||||
Feature::for($user)->activate('new-dashboard');
|
||||
</code-snippet>
|
||||
|
||||
## Verification
|
||||
|
||||
1. Check feature flag is defined
|
||||
2. Test with different scopes/users
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
- Forgetting to scope features for specific users/entities
|
||||
- Not following existing naming conventions
|
||||
|
|
@ -1,174 +0,0 @@
|
|||
---
|
||||
name: pest-testing
|
||||
description: >-
|
||||
Tests applications using the Pest 4 PHP framework. Activates when writing tests, creating unit or feature
|
||||
tests, adding assertions, testing Livewire components, browser testing, debugging test failures,
|
||||
working with datasets or mocking; or when the user mentions test, spec, TDD, expects, assertion,
|
||||
coverage, or needs to verify functionality works.
|
||||
---
|
||||
|
||||
# Pest Testing 4
|
||||
|
||||
## When to Apply
|
||||
|
||||
Activate this skill when:
|
||||
|
||||
- Creating new tests (unit, feature, or browser)
|
||||
- Modifying existing tests
|
||||
- Debugging test failures
|
||||
- Working with browser testing or smoke testing
|
||||
- Writing architecture tests or visual regression tests
|
||||
|
||||
## Documentation
|
||||
|
||||
Use `search-docs` for detailed Pest 4 patterns and documentation.
|
||||
|
||||
## Basic Usage
|
||||
|
||||
### Creating Tests
|
||||
|
||||
All tests must be written using Pest. Use `php artisan make:test --pest {name}`.
|
||||
|
||||
### Test Organization
|
||||
|
||||
- Unit/Feature tests: `tests/Feature` and `tests/Unit` directories.
|
||||
- Browser tests: `tests/Browser/` directory.
|
||||
- Do NOT remove tests without approval - these are core application code.
|
||||
|
||||
### Basic Test Structure
|
||||
|
||||
<code-snippet name="Basic Pest Test Example" lang="php">
|
||||
|
||||
it('is true', function () {
|
||||
expect(true)->toBeTrue();
|
||||
});
|
||||
|
||||
</code-snippet>
|
||||
|
||||
### Running Tests
|
||||
|
||||
- Run minimal tests with filter before finalizing: `php artisan test --compact --filter=testName`.
|
||||
- Run all tests: `php artisan test --compact`.
|
||||
- Run file: `php artisan test --compact tests/Feature/ExampleTest.php`.
|
||||
|
||||
## Assertions
|
||||
|
||||
Use specific assertions (`assertSuccessful()`, `assertNotFound()`) instead of `assertStatus()`:
|
||||
|
||||
<code-snippet name="Pest Response Assertion" lang="php">
|
||||
|
||||
it('returns all', function () {
|
||||
$this->postJson('/api/docs', [])->assertSuccessful();
|
||||
});
|
||||
|
||||
</code-snippet>
|
||||
|
||||
| Use | Instead of |
|
||||
|-----|------------|
|
||||
| `assertSuccessful()` | `assertStatus(200)` |
|
||||
| `assertNotFound()` | `assertStatus(404)` |
|
||||
| `assertForbidden()` | `assertStatus(403)` |
|
||||
|
||||
## Mocking
|
||||
|
||||
Import mock function before use: `use function Pest\Laravel\mock;`
|
||||
|
||||
## Datasets
|
||||
|
||||
Use datasets for repetitive tests (validation rules, etc.):
|
||||
|
||||
<code-snippet name="Pest Dataset Example" lang="php">
|
||||
|
||||
it('has emails', function (string $email) {
|
||||
expect($email)->not->toBeEmpty();
|
||||
})->with([
|
||||
'james' => 'james@laravel.com',
|
||||
'taylor' => 'taylor@laravel.com',
|
||||
]);
|
||||
|
||||
</code-snippet>
|
||||
|
||||
## Pest 4 Features
|
||||
|
||||
| Feature | Purpose |
|
||||
|---------|---------|
|
||||
| Browser Testing | Full integration tests in real browsers |
|
||||
| Smoke Testing | Validate multiple pages quickly |
|
||||
| Visual Regression | Compare screenshots for visual changes |
|
||||
| Test Sharding | Parallel CI runs |
|
||||
| Architecture Testing | Enforce code conventions |
|
||||
|
||||
### Browser Test Example
|
||||
|
||||
Browser tests run in real browsers for full integration testing:
|
||||
|
||||
- Browser tests live in `tests/Browser/`.
|
||||
- Use Laravel features like `Event::fake()`, `assertAuthenticated()`, and model factories.
|
||||
- Use `RefreshDatabase` for clean state per test.
|
||||
- Interact with page: click, type, scroll, select, submit, drag-and-drop, touch gestures.
|
||||
- Test on multiple browsers (Chrome, Firefox, Safari) if requested.
|
||||
- Test on different devices/viewports (iPhone 14 Pro, tablets) if requested.
|
||||
- Switch color schemes (light/dark mode) when appropriate.
|
||||
- Take screenshots or pause tests for debugging.
|
||||
|
||||
<code-snippet name="Pest Browser Test Example" lang="php">
|
||||
|
||||
it('may reset the password', function () {
|
||||
Notification::fake();
|
||||
|
||||
$this->actingAs(User::factory()->create());
|
||||
|
||||
$page = visit('/sign-in');
|
||||
|
||||
$page->assertSee('Sign In')
|
||||
->assertNoJavascriptErrors()
|
||||
->click('Forgot Password?')
|
||||
->fill('email', 'nuno@laravel.com')
|
||||
->click('Send Reset Link')
|
||||
->assertSee('We have emailed your password reset link!');
|
||||
|
||||
Notification::assertSent(ResetPassword::class);
|
||||
});
|
||||
|
||||
</code-snippet>
|
||||
|
||||
### Smoke Testing
|
||||
|
||||
Quickly validate multiple pages have no JavaScript errors:
|
||||
|
||||
<code-snippet name="Pest Smoke Testing Example" lang="php">
|
||||
|
||||
$pages = visit(['/', '/about', '/contact']);
|
||||
|
||||
$pages->assertNoJavascriptErrors()->assertNoConsoleLogs();
|
||||
|
||||
</code-snippet>
|
||||
|
||||
### Visual Regression Testing
|
||||
|
||||
Capture and compare screenshots to detect visual changes.
|
||||
|
||||
### Test Sharding
|
||||
|
||||
Split tests across parallel processes for faster CI runs.
|
||||
|
||||
### Architecture Testing
|
||||
|
||||
Pest 4 includes architecture testing (from Pest 3):
|
||||
|
||||
<code-snippet name="Architecture Test Example" lang="php">
|
||||
|
||||
arch('controllers')
|
||||
->expect('App\Http\Controllers')
|
||||
->toExtendNothing()
|
||||
->toHaveSuffix('Controller');
|
||||
|
||||
</code-snippet>
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
- Not importing `use function Pest\Laravel\mock;` before using mock
|
||||
- Using `assertStatus(200)` instead of `assertSuccessful()`
|
||||
- Forgetting datasets for repetitive validation tests
|
||||
- Deleting tests without approval
|
||||
- Forgetting `assertNoJavascriptErrors()` in browser tests
|
||||
|
|
@ -1,124 +0,0 @@
|
|||
---
|
||||
name: tailwindcss-development
|
||||
description: >-
|
||||
Styles applications using Tailwind CSS v4 utilities. Activates when adding styles, restyling components,
|
||||
working with gradients, spacing, layout, flex, grid, responsive design, dark mode, colors,
|
||||
typography, or borders; or when the user mentions CSS, styling, classes, Tailwind, restyle,
|
||||
hero section, cards, buttons, or any visual/UI changes.
|
||||
---
|
||||
|
||||
# Tailwind CSS Development
|
||||
|
||||
## When to Apply
|
||||
|
||||
Activate this skill when:
|
||||
|
||||
- Adding styles to components or pages
|
||||
- Working with responsive design
|
||||
- Implementing dark mode
|
||||
- Extracting repeated patterns into components
|
||||
- Debugging spacing or layout issues
|
||||
|
||||
## Documentation
|
||||
|
||||
Use `search-docs` for detailed Tailwind CSS v4 patterns and documentation.
|
||||
|
||||
## Basic Usage
|
||||
|
||||
- Use Tailwind CSS classes to style HTML. Check and follow existing Tailwind conventions in the project before introducing new patterns.
|
||||
- Offer to extract repeated patterns into components that match the project's conventions (e.g., Blade, JSX, Vue).
|
||||
- Consider class placement, order, priority, and defaults. Remove redundant classes, add classes to parent or child elements carefully to reduce repetition, and group elements logically.
|
||||
|
||||
## Tailwind CSS v4 Specifics
|
||||
|
||||
- Always use Tailwind CSS v4 and avoid deprecated utilities.
|
||||
- `corePlugins` is not supported in Tailwind v4.
|
||||
|
||||
### CSS-First Configuration
|
||||
|
||||
In Tailwind v4, configuration is CSS-first using the `@theme` directive — no separate `tailwind.config.js` file is needed:
|
||||
|
||||
<code-snippet name="CSS-First Config" lang="css">
|
||||
@theme {
|
||||
--color-brand: oklch(0.72 0.11 178);
|
||||
}
|
||||
</code-snippet>
|
||||
|
||||
### Import Syntax
|
||||
|
||||
In Tailwind v4, import Tailwind with a regular CSS `@import` statement instead of the `@tailwind` directives used in v3:
|
||||
|
||||
<code-snippet name="v4 Import Syntax" lang="diff">
|
||||
- @tailwind base;
|
||||
- @tailwind components;
|
||||
- @tailwind utilities;
|
||||
+ @import "tailwindcss";
|
||||
</code-snippet>
|
||||
|
||||
### Replaced Utilities
|
||||
|
||||
Tailwind v4 removed deprecated utilities. Use the replacements shown below. Opacity values remain numeric.
|
||||
|
||||
| Deprecated | Replacement |
|
||||
|------------|-------------|
|
||||
| bg-opacity-* | bg-black/* |
|
||||
| text-opacity-* | text-black/* |
|
||||
| border-opacity-* | border-black/* |
|
||||
| divide-opacity-* | divide-black/* |
|
||||
| ring-opacity-* | ring-black/* |
|
||||
| placeholder-opacity-* | placeholder-black/* |
|
||||
| flex-shrink-* | shrink-* |
|
||||
| flex-grow-* | grow-* |
|
||||
| overflow-ellipsis | text-ellipsis |
|
||||
| decoration-slice | box-decoration-slice |
|
||||
| decoration-clone | box-decoration-clone |
|
||||
|
||||
## Spacing
|
||||
|
||||
Use `gap` utilities instead of margins for spacing between siblings:
|
||||
|
||||
<code-snippet name="Gap Utilities" lang="html">
|
||||
<div class="flex gap-8">
|
||||
<div>Item 1</div>
|
||||
<div>Item 2</div>
|
||||
</div>
|
||||
</code-snippet>
|
||||
|
||||
## Dark Mode
|
||||
|
||||
If existing pages and components support dark mode, new pages and components must support it the same way, typically using the `dark:` variant:
|
||||
|
||||
<code-snippet name="Dark Mode" lang="html">
|
||||
<div class="bg-white dark:bg-gray-900 text-gray-900 dark:text-white">
|
||||
Content adapts to color scheme
|
||||
</div>
|
||||
</code-snippet>
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Flexbox Layout
|
||||
|
||||
<code-snippet name="Flexbox Layout" lang="html">
|
||||
<div class="flex items-center justify-between gap-4">
|
||||
<div>Left content</div>
|
||||
<div>Right content</div>
|
||||
</div>
|
||||
</code-snippet>
|
||||
|
||||
### Grid Layout
|
||||
|
||||
<code-snippet name="Grid Layout" lang="html">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
<div>Card 1</div>
|
||||
<div>Card 2</div>
|
||||
<div>Card 3</div>
|
||||
</div>
|
||||
</code-snippet>
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
- Using deprecated v3 utilities (bg-opacity-*, flex-shrink-*, etc.)
|
||||
- Using `@tailwind` directives instead of `@import "tailwindcss"`
|
||||
- Trying to use `tailwind.config.js` instead of CSS `@theme` directive
|
||||
- Using margins for spacing between siblings instead of gap utilities
|
||||
- Forgetting to add dark mode variants when the project uses dark mode
|
||||
|
|
@ -1,89 +0,0 @@
|
|||
---
|
||||
name: wayfinder-development
|
||||
description: >-
|
||||
Activates whenever referencing backend routes in frontend components. Use when
|
||||
importing from @/actions or @/routes, calling Laravel routes from TypeScript,
|
||||
or working with Wayfinder route functions.
|
||||
---
|
||||
|
||||
# Wayfinder Development
|
||||
|
||||
## When to Apply
|
||||
|
||||
Activate whenever referencing backend routes in frontend components:
|
||||
- Importing from `@/actions/` or `@/routes/`
|
||||
- Calling Laravel routes from TypeScript/JavaScript
|
||||
- Creating links or navigation to backend endpoints
|
||||
|
||||
## Documentation
|
||||
|
||||
Use `search-docs` for detailed Wayfinder patterns and documentation.
|
||||
|
||||
## Quick Reference
|
||||
|
||||
### Generate Routes
|
||||
|
||||
Run after route changes if Vite plugin isn't installed:
|
||||
|
||||
php artisan wayfinder:generate --no-interaction
|
||||
|
||||
For form helpers, use `--with-form` flag:
|
||||
|
||||
php artisan wayfinder:generate --with-form --no-interaction
|
||||
|
||||
### Import Patterns
|
||||
|
||||
<code-snippet name="Controller Action Imports" lang="typescript">
|
||||
|
||||
// Named imports for tree-shaking (preferred)...
|
||||
import { show, store, update } from '@/actions/App/Http/Controllers/PostController'
|
||||
|
||||
// Named route imports...
|
||||
import { show as postShow } from '@/routes/post'
|
||||
|
||||
</code-snippet>
|
||||
|
||||
### Common Methods
|
||||
|
||||
<code-snippet name="Wayfinder Methods" lang="typescript">
|
||||
|
||||
// Get route object...
|
||||
show(1) // { url: "/posts/1", method: "get" }
|
||||
|
||||
// Get URL string...
|
||||
show.url(1) // "/posts/1"
|
||||
|
||||
// Specific HTTP methods...
|
||||
show.get(1)
|
||||
store.post()
|
||||
update.patch(1)
|
||||
destroy.delete(1)
|
||||
|
||||
// Form attributes for HTML forms...
|
||||
store.form() // { action: "/posts", method: "post" }
|
||||
|
||||
// Query parameters...
|
||||
show(1, { query: { page: 1 } }) // "/posts/1?page=1"
|
||||
|
||||
</code-snippet>
|
||||
|
||||
## Wayfinder + Inertia
|
||||
|
||||
Use Wayfinder with the `<Form>` component:
|
||||
<code-snippet name="Wayfinder Form (React)" lang="typescript">
|
||||
|
||||
<Form {...store.form()}><input name="title" /></Form>
|
||||
|
||||
</code-snippet>
|
||||
|
||||
## Verification
|
||||
|
||||
1. Run `php artisan wayfinder:generate` to regenerate routes if Vite plugin isn't installed
|
||||
2. Check TypeScript imports resolve correctly
|
||||
3. Verify route URLs match expected paths
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
- Using default imports instead of named imports (breaks tree-shaking)
|
||||
- Forgetting to regenerate after route changes
|
||||
- Not using type-safe parameter objects for route model binding
|
||||
|
|
@ -1 +0,0 @@
|
|||
8.4.17
|
||||
|
|
@ -1 +0,0 @@
|
|||
export { default } from './mcps/main.ts';
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
|
@ -1,11 +0,0 @@
|
|||
{
|
||||
"name": "pi-mcp-bridge",
|
||||
"private": true,
|
||||
"description": "Project-local Pi extension that loads MCP servers from .mcp.json or opencode.json",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/sdk": "^1.29.0",
|
||||
"@sinclair/typebox": "^0.34.49",
|
||||
"zod": "^4.3.6"
|
||||
}
|
||||
}
|
||||
|
|
@ -1,68 +0,0 @@
|
|||
---
|
||||
description: Pick or fix a Sentry issue end-to-end, then PR and watch CI
|
||||
argument-hint: "[issue-id-or-url]"
|
||||
---
|
||||
Run Sentry issue repair workflow for this project only.
|
||||
|
||||
Input: `$ARGUMENTS`
|
||||
|
||||
Goal:
|
||||
1. Use provided Sentry issue id or URL. If none provided, choose the highest-impact unresolved issue using frequency, affected users, recency, and production impact.
|
||||
2. Create/switch to git branch named exactly after the Sentry short issue id.
|
||||
3. Investigate root cause, implement fix, and add/update tests.
|
||||
4. Open PR and watch CI until green.
|
||||
|
||||
Rules:
|
||||
- Protect local work. Start with `git status --short`. If uncommitted changes exist, stop and ask before branching.
|
||||
- Never expose secrets. Do not print tokens, `.env`, Sentry auth, DB URLs, or PII.
|
||||
- Use the Sentry CLI (`sentry`) first. Prefer stored OAuth login over env build tokens; if `SENTRY_AUTH_TOKEN` is invalid or too narrow, run CLI commands as `env -u SENTRY_AUTH_TOKEN sentry ...` unless `SENTRY_FORCE_ENV_TOKEN=1` is intentionally set.
|
||||
- Never print raw Sentry JSON that may contain PII. Redact emails, user IDs when summarizing. Keep secrets out of output.
|
||||
- For Laravel ecosystem changes, use `application-info` and `search-docs` before code changes.
|
||||
- Activate/read relevant project skills when touched: Pest tests, Inertia React, Wayfinder, Tailwind, Fortify, Pennant.
|
||||
- Every code change needs programmatic verification. Add or update a focused Pest/test when feasible. Run minimum affected tests. Run `vendor/bin/pint --dirty --format agent` after PHP edits.
|
||||
- Prefer small surgical fix. No dependency changes without approval.
|
||||
|
||||
Workflow:
|
||||
1. Identify issue:
|
||||
- Confirm auth and org/project access with `sentry auth status`, `sentry org list --json`, and `sentry project list <org> --json` when needed.
|
||||
- If `$ARGUMENTS` is a Sentry URL, extract `/issues/<numeric-id>/` or the visible short issue id, then fetch it with `sentry issue view <issue> --json --fields id,shortId,title,culprit,permalink,level,status,substatus,count,userCount,firstSeen,lastSeen,project,metadata,priority,platform,isUnhandled`.
|
||||
- If `$ARGUMENTS` is a bare issue id or short id, fetch it with `sentry issue view <issue> --json --fields id,shortId,title,culprit,permalink,level,status,substatus,count,userCount,firstSeen,lastSeen,project,metadata,priority,platform,isUnhandled`.
|
||||
- If no args, list unresolved production issues with both frequency and user sorting, then compare top results:
|
||||
- `sentry issue list <org>/<project> --query 'is:unresolved environment:production' --sort freq --limit 25 --json --fields id,shortId,title,culprit,count,userCount,lastSeen,permalink,priority,metadata,project`
|
||||
- `sentry issue list <org>/<project> --query 'is:unresolved environment:production' --sort user --limit 25 --json --fields id,shortId,title,culprit,count,userCount,lastSeen,permalink,priority,metadata,project`
|
||||
- Pick best impact score: high event count, high user count, recent lastSeen, production environment, clear actionable stack.
|
||||
- Record chosen short issue id and Sentry URL in notes.
|
||||
2. Branch:
|
||||
- Derive branch name from Sentry short issue id only, e.g. `WHISPER-MONEY-123`.
|
||||
- Run `git switch -c <issue-id>`; if branch exists, `git switch <issue-id>`.
|
||||
3. Investigate:
|
||||
- Fetch latest issue details and spans with `sentry issue view <issue> --spans all --json`.
|
||||
- Fetch recent events with `sentry issue events <issue> --limit 10 --full --json`.
|
||||
- Extract stack trace, breadcrumbs, environment, release, tags, URLs/routes, affected users count, trace/span data, and suspect queries. Redact PII in notes.
|
||||
- For event details if needed, use `sentry event view <event-id> --json` or `sentry api <endpoint> --json`.
|
||||
- Reproduce locally using tests or focused command. Inspect app logs/browser logs if relevant.
|
||||
- If root cause not obvious after issue/event data, run Seer with `sentry issue explain <issue> --json` and/or `sentry issue plan <issue> --json`.
|
||||
4. Fix:
|
||||
- Read nearby code and conventions first.
|
||||
- Implement minimal fix.
|
||||
- Add regression test covering Sentry failure path.
|
||||
5. Verify:
|
||||
- Run targeted test command, e.g. `php artisan test --compact --filter=<test-or-class>`.
|
||||
- Run lint/format commands required by touched files.
|
||||
- If failures occur, fix and rerun until pass.
|
||||
6. PR:
|
||||
- Commit changes with concise conventional commit.
|
||||
- Push branch.
|
||||
- Create PR with `gh pr create`, including Sentry issue link, root cause, fix summary, and verification commands.
|
||||
- Get PR number from `gh pr view --json number --jq .number` if needed.
|
||||
- Watch CI every 10 seconds with `gh pr checks <number> --watch --fail-fast --interval 10`.
|
||||
- If CI fails, inspect logs, fix, commit/push, and watch again until green.
|
||||
|
||||
Output when done:
|
||||
- Issue id + Sentry URL
|
||||
- Branch name
|
||||
- Root cause
|
||||
- Fix summary
|
||||
- Tests/commands run
|
||||
- PR URL
|
||||
- CI status
|
||||
|
|
@ -1,17 +1,7 @@
|
|||
{
|
||||
"hooks": {
|
||||
"after:bump": "node scripts/enrich-changelog.js && git add CHANGELOG.md"
|
||||
},
|
||||
"git": {
|
||||
"commitMessage": "chore: release v${version}",
|
||||
"tagName": "v${version}",
|
||||
"requireBranch": false,
|
||||
"requireCleanWorkingDir": true,
|
||||
"push": true
|
||||
},
|
||||
"github": {
|
||||
"release": true,
|
||||
"releaseName": "v${version}"
|
||||
"tagName": "v${version}"
|
||||
},
|
||||
"npm": {
|
||||
"publish": false
|
||||
|
|
|
|||
282
AGENTS.md
282
AGENTS.md
|
|
@ -1,282 +0,0 @@
|
|||
Terse like caveman. Technical substance exact. Only fluff die.
|
||||
Drop: articles, filler (just/really/basically), pleasantries, hedging.
|
||||
Fragments OK. Short synonyms. Code unchanged.
|
||||
Pattern: [thing] [action] [reason]. [next step].
|
||||
ACTIVE EVERY RESPONSE. No revert after many turns. No filler drift.
|
||||
Code/commits/PRs: normal. Off: "stop caveman" / "normal mode".
|
||||
|
||||
- After creating a PR use `gh pr checks 123 --watch --fail-fast` to check CI every 10 sec, if something fails, fix it, and update the pr.
|
||||
|
||||
<laravel-boost-guidelines>
|
||||
=== 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 ensure the best experience when 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.4
|
||||
- inertiajs/inertia-laravel (INERTIA_LARAVEL) - v2
|
||||
- laravel/cashier (CASHIER) - v16
|
||||
- laravel/fortify (FORTIFY) - v1
|
||||
- laravel/framework (LARAVEL) - v13
|
||||
- laravel/pennant (PENNANT) - v1
|
||||
- laravel/prompts (PROMPTS) - v0
|
||||
- laravel/wayfinder (WAYFINDER) - v0
|
||||
- larastan/larastan (LARASTAN) - v3
|
||||
- laravel/boost (BOOST) - v2
|
||||
- laravel/mcp (MCP) - v0
|
||||
- laravel/pail (PAIL) - v1
|
||||
- laravel/pint (PINT) - v1
|
||||
- laravel/sail (SAIL) - v1
|
||||
- pestphp/pest (PEST) - v4
|
||||
- phpunit/phpunit (PHPUNIT) - v12
|
||||
- @inertiajs/react (INERTIA_REACT) - v2
|
||||
- react (REACT) - v19
|
||||
- tailwindcss (TAILWINDCSS) - v4
|
||||
- @laravel/vite-plugin-wayfinder (WAYFINDER_VITE) - v0
|
||||
- eslint (ESLINT) - v9
|
||||
- prettier (PRETTIER) - v3
|
||||
|
||||
## Skills Activation
|
||||
|
||||
This project has domain-specific skills available. You MUST activate the relevant skill whenever you work in that domain—don't wait until you're stuck.
|
||||
|
||||
- `cashier-stripe-development` — Handles Laravel Cashier Stripe integration including subscriptions, webhooks, Stripe Checkout, invoices, charges, refunds, trials, coupons, metered billing, and payment failure handling. Triggered when a user mentions Cashier, Billable, IncompletePayment, stripe_id, newSubscription, Stripe subscriptions, or billing. Also applies when setting up webhooks, handling SCA/3DS payment failures, testing with Stripe test cards, or troubleshooting incomplete subscriptions, CSRF webhook errors, or migration publish issues.
|
||||
- `pennant-development` — Use when working with Laravel Pennant the official Laravel feature flag package. Trigger whenever the query mentions Pennant by name or involves feature flags or feature toggles in a Laravel project. Tasks include defining feature flags checking whether features are active creating class based features in `app/Features` using Blade `@feature` directives scoping flags to users or teams building custom Pennant storage drivers protecting routes with feature flags testing feature flags with Pest or PHPUnit and implementing A B testing or gradual rollouts with feature flags. Do not trigger for generic Laravel configuration authorization policies authentication or non Pennant feature management systems.
|
||||
- `wayfinder-development` — Activates whenever referencing backend routes in frontend components. Use when importing from @/actions or @/routes, calling Laravel routes from TypeScript, or working with Wayfinder route functions.
|
||||
- `pest-testing` — Use this skill for Pest PHP testing in Laravel projects only. Trigger whenever any test is being written, edited, fixed, or refactored — including fixing tests that broke after a code change, adding assertions, converting PHPUnit to Pest, adding datasets, and TDD workflows. Always activate when the user asks how to write something in Pest, mentions test files or directories (tests/Feature, tests/Unit, tests/Browser), or needs browser testing, smoke testing multiple pages for JS errors, or architecture tests. Covers: it()/expect() syntax, datasets, mocking, browser testing (visit/click/fill), smoke testing, arch(), Livewire component tests, RefreshDatabase, and all Pest 4 features. Do not use for factories, seeders, migrations, controllers, models, or non-test PHP code.
|
||||
- `inertia-react-development` — Develops Inertia.js v2 React client-side applications. Activates when creating React pages, forms, or navigation; using <Link>, <Form>, useForm, or router; working with deferred props, prefetching, or polling; or when user mentions React with Inertia, React pages, React forms, or React navigation.
|
||||
- `tailwindcss-development` — Always invoke when the user's message includes 'tailwind' in any form. Also invoke for: building responsive grid layouts (multi-column card grids, product grids), flex/grid page structures (dashboards with sidebars, fixed topbars, mobile-toggle navs), styling UI components (cards, tables, navbars, pricing sections, forms, inputs, badges), adding dark mode variants, fixing spacing or typography, and Tailwind v3/v4 work. The core use case: writing or fixing Tailwind utility classes in HTML templates (Blade, JSX, Vue). Skip for backend PHP logic, database queries, API routes, JavaScript with no HTML/CSS component, CSS file audits, build tool configuration, and vanilla CSS.
|
||||
- `fortify-development` — Laravel Fortify headless authentication backend development. Activate when implementing authentication features including login, registration, password reset, email verification, two-factor authentication (2FA/TOTP), profile updates, headless auth, authentication scaffolding, or auth guards in Laravel applications.
|
||||
|
||||
## 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, and 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 they work. 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.
|
||||
|
||||
## Documentation Files
|
||||
|
||||
- You must only create documentation files if explicitly requested by the user.
|
||||
|
||||
## Replies
|
||||
|
||||
- Be concise in your explanations - focus on what's important rather than explaining obvious details.
|
||||
|
||||
=== boost rules ===
|
||||
|
||||
# Laravel Boost
|
||||
|
||||
- Laravel Boost is an MCP server that comes with powerful tools designed specifically for this application. Use them.
|
||||
|
||||
## Artisan Commands
|
||||
|
||||
- Run Artisan commands directly via the command line (e.g., `php artisan route:list`, `php artisan tinker --execute "..."`).
|
||||
- Use `php artisan list` to discover available commands and `php artisan [command] --help` to check 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.
|
||||
|
||||
## Debugging
|
||||
|
||||
- Use the `database-query` tool when you only need to read from the database.
|
||||
- Use the `database-schema` tool to inspect table structure before writing migrations or models.
|
||||
- To execute PHP code for debugging, run `php artisan tinker --execute "your code here"` directly.
|
||||
- To read configuration values, read the config files directly or run `php artisan config:show [key]`.
|
||||
- To inspect routes, run `php artisan route:list` directly.
|
||||
- To check environment variables, read the `.env` file directly.
|
||||
|
||||
## 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 trying other approaches when working with Laravel or Laravel ecosystem packages. This tool automatically passes a list of installed packages and their versions to the remote Boost API, so it returns only version-specific documentation for the user's circumstance. You should pass an array of packages to filter on if you know you need docs for particular packages.
|
||||
- Search the documentation before making code changes to ensure we are taking the correct approach.
|
||||
- Use multiple, broad, simple, topic-based queries at once. For example: `['rate limiting', 'routing rate limiting', 'routing']`. The most relevant results will be returned first.
|
||||
- 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
|
||||
|
||||
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 for single-line bodies.
|
||||
|
||||
## Constructors
|
||||
|
||||
- Use PHP 8 constructor property promotion in `__construct()`.
|
||||
- `public function __construct(public GitHub $github) { }`
|
||||
- Do not allow empty `__construct()` methods with zero parameters unless the constructor is private.
|
||||
|
||||
## Type Declarations
|
||||
|
||||
- Always use explicit return type declarations for methods and functions.
|
||||
- Use appropriate PHP type hints for method parameters.
|
||||
|
||||
<!-- Explicit Return Types and Method Params -->
|
||||
```php
|
||||
protected function isAccessible(User $user, ?string $path = null): bool
|
||||
{
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
## Enums
|
||||
|
||||
- Typically, keys in an Enum should be TitleCase. For example: `FavoritePerson`, `BestLake`, `Monthly`.
|
||||
|
||||
## Comments
|
||||
|
||||
- Prefer PHPDoc blocks over inline comments. Never use comments within the code itself unless the logic is exceptionally complex.
|
||||
|
||||
## PHPDoc Blocks
|
||||
|
||||
- Add useful array shape type definitions when appropriate.
|
||||
|
||||
=== tests rules ===
|
||||
|
||||
# Test Enforcement
|
||||
|
||||
- Every change must be programmatically tested. Write a new test or update an existing test, then run the affected tests to make sure they pass.
|
||||
- Run the minimum number of tests needed to ensure code quality and speed. Use `php artisan test --compact` with a specific filename or filter.
|
||||
|
||||
=== inertia-laravel/core rules ===
|
||||
|
||||
# Inertia
|
||||
|
||||
- Inertia creates fully client-side rendered SPAs without modern SPA complexity, leveraging existing server-side patterns.
|
||||
- Components live in `resources/js/pages` (unless specified in `vite.config.js`). Use `Inertia::render()` for server-side routing instead of Blade views.
|
||||
- ALWAYS use `search-docs` tool for version-specific Inertia documentation and updated code examples.
|
||||
- IMPORTANT: Activate `inertia-react-development` when working with Inertia client-side patterns.
|
||||
|
||||
# Inertia v2
|
||||
|
||||
- Use all Inertia features from v1 and v2. Check the documentation before making changes to ensure the correct approach.
|
||||
- New features: deferred props, infinite scroll, merging props, polling, prefetching, once props, flash data.
|
||||
- When using deferred props, add an empty state with a pulsing or animated skeleton.
|
||||
|
||||
=== 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 `php artisan list` and check their parameters with `php artisan [command] --help`.
|
||||
- If you're creating a generic PHP class, use `php 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 `php artisan make:model --help` to check the available options.
|
||||
|
||||
### 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.
|
||||
|
||||
## 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.
|
||||
|
||||
## Queues
|
||||
|
||||
- Use queued jobs for time-consuming operations with the `ShouldQueue` interface.
|
||||
|
||||
## 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] {name}` 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`.
|
||||
|
||||
=== wayfinder/core rules ===
|
||||
|
||||
# Laravel Wayfinder
|
||||
|
||||
Wayfinder generates TypeScript functions for Laravel routes. Import from `@/actions/` (controllers) or `@/routes/` (named routes).
|
||||
|
||||
- IMPORTANT: Activate `wayfinder-development` skill whenever referencing backend routes in frontend components.
|
||||
- Invokable Controllers: `import StorePost from '@/actions/.../StorePostController'; StorePost()`.
|
||||
- Parameter Binding: Detects route keys (`{post:slug}`) — `show({ slug: "my-post" })`.
|
||||
- Query Merging: `show(1, { mergeQuery: { page: 2, sort: null } })` merges with current URL, `null` removes params.
|
||||
- Inertia: Use `.form()` with `<Form>` component or `form.submit(store())` with useForm.
|
||||
|
||||
=== pint/core rules ===
|
||||
|
||||
# Laravel Pint Code Formatter
|
||||
|
||||
- If you have modified any PHP files, you must run `vendor/bin/pint --dirty --format agent` before finalizing changes to ensure your code matches the project's expected style.
|
||||
- Do not run `vendor/bin/pint --test --format agent`, simply run `vendor/bin/pint --format agent` to fix any formatting issues.
|
||||
|
||||
=== pest/core rules ===
|
||||
|
||||
## Pest
|
||||
|
||||
- This project uses Pest for testing. Create tests: `php artisan make:test --pest {name}`.
|
||||
- Run tests: `php artisan test --compact` or filter: `php artisan test --compact --filter=testName`.
|
||||
- Do NOT delete tests without approval.
|
||||
|
||||
=== inertia-react/core rules ===
|
||||
|
||||
# Inertia + React
|
||||
|
||||
- IMPORTANT: Activate `inertia-react-development` when working with Inertia React client-side patterns.
|
||||
|
||||
=== laravel/fortify rules ===
|
||||
|
||||
# Laravel Fortify
|
||||
|
||||
- Fortify is a headless authentication backend that provides authentication routes and controllers for Laravel applications.
|
||||
- IMPORTANT: Always use the `search-docs` tool for detailed Laravel Fortify patterns and documentation.
|
||||
- IMPORTANT: Activate `developing-with-fortify` skill when working with Fortify authentication features.
|
||||
|
||||
</laravel-boost-guidelines>
|
||||
850
CHANGELOG.md
850
CHANGELOG.md
|
|
@ -1,790 +1,102 @@
|
|||
# Changelog
|
||||
|
||||
## [0.2.6](https://github.com/whisper-money/whisper-money/compare/v0.2.5...v0.2.6) (2026-07-06)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **accounts:** fire drag haptic on first tap, not after dragging ([#576](https://github.com/whisper-money/whisper-money/issues/576)) ([c75e834](https://github.com/whisper-money/whisper-money/commit/c75e834b89b58f19fba0021e423ef5d7fe8ae827))
|
||||
* **accounts:** remove double-skeleton flash on account show ([#634](https://github.com/whisper-money/whisper-money/issues/634)) ([afa80b6](https://github.com/whisper-money/whisper-money/commit/afa80b60fe97ecc504847ef9c4a1e8c9a9c2c9f1)), closes [#632](https://github.com/whisper-money/whisper-money/issues/632)
|
||||
* **accounts:** stop second long-press haptic on drag handle ([#578](https://github.com/whisper-money/whisper-money/issues/578)) ([5db6cdc](https://github.com/whisper-money/whisper-money/commit/5db6cdc6d2fcc03d3d89d16d058834cb89107999)), closes [#576](https://github.com/whisper-money/whisper-money/issues/576)
|
||||
* address remaining security audit findings (round 2) ([#628](https://github.com/whisper-money/whisper-money/issues/628)) ([4fdb7ee](https://github.com/whisper-money/whisper-money/commit/4fdb7eebd916f81bfde7dc4c6adbc8387d6699ca)), closes [#623](https://github.com/whisper-money/whisper-money/issues/623)
|
||||
* **ai:** handle transient AI provider overloads — stop the Sentry noise and retry the dropped work ([#595](https://github.com/whisper-money/whisper-money/issues/595)) ([4038e60](https://github.com/whisper-money/whisper-money/commit/4038e60fbcd9891ceecabe5f66c34dde1abcc6cd))
|
||||
* **ai:** surface learned-rule toast in edit modal and guard weak description keys ([#635](https://github.com/whisper-money/whisper-money/issues/635)) ([c159e87](https://github.com/whisper-money/whisper-money/commit/c159e8782efa5808e1f707eb7309966de3144f9b))
|
||||
* **analysis:** respect category types like the cashflow screen ([#612](https://github.com/whisper-money/whisper-money/issues/612)) ([986f437](https://github.com/whisper-money/whisper-money/commit/986f43705a35fb0e3cd49c2056c2a46e769a91aa))
|
||||
* **appearance:** support MediaQueryList change events on legacy Safari (PHP-LARAVEL-41) ([#646](https://github.com/whisper-money/whisper-money/issues/646)) ([465eb38](https://github.com/whisper-money/whisper-money/commit/465eb38dae1e856f0017f7f0e33bbad31b1d2c71))
|
||||
* **banking:** handle EnableBanking expired sessions as reconnect, not error ([#557](https://github.com/whisper-money/whisper-money/issues/557)) ([c36df98](https://github.com/whisper-money/whisper-money/commit/c36df98d326336c27cf63039eb28518dae3af43e))
|
||||
* **banking:** keep the native green Wise logo, not the aggregator's ([#590](https://github.com/whisper-money/whisper-money/issues/590)) ([578a9b4](https://github.com/whisper-money/whisper-money/commit/578a9b44d8f01d4d22558397af2f31a9e5bf80b8)), closes [#589](https://github.com/whisper-money/whisper-money/issues/589) [#525](https://github.com/whisper-money/whisper-money/issues/525) [#589](https://github.com/whisper-money/whisper-money/issues/589)
|
||||
* **banking:** only log sync failures once the connection gives up ([#603](https://github.com/whisper-money/whisper-money/issues/603)) ([8bbff05](https://github.com/whisper-money/whisper-money/commit/8bbff05b2693cef3edbc8fc4c9350f6ba7ab99d6))
|
||||
* **banking:** skip inaccessible EnableBanking accounts instead of failing the connection ([#559](https://github.com/whisper-money/whisper-money/issues/559)) ([4656870](https://github.com/whisper-money/whisper-money/commit/46568700b2c0610566a7a35efe54810ea51e3925))
|
||||
* **banking:** stop Wise appearing multiple times in the connect list ([#589](https://github.com/whisper-money/whisper-money/issues/589)) ([ed5aac0](https://github.com/whisper-money/whisper-money/commit/ed5aac0c4a0713e5bf2b0f2a82b9258abdcdb986))
|
||||
* **charts:** improve contrast for chart colors 9-10 ([#614](https://github.com/whisper-money/whisper-money/issues/614)) ([a37481f](https://github.com/whisper-money/whisper-money/commit/a37481fb71b935f0f42b282fabe6db3c664be5c6)), closes [hi#index](https://github.com/hi/issues/index)
|
||||
* **discord:** show old → new plan on plan change notification ([#637](https://github.com/whisper-money/whisper-money/issues/637)) ([3972007](https://github.com/whisper-money/whisper-money/commit/39720078447f9b17dacbfd15e9986f978b87b56a))
|
||||
* **discord:** skip zero-amount payment stats messages ([#540](https://github.com/whisper-money/whisper-money/issues/540)) ([7693e48](https://github.com/whisper-money/whisper-money/commit/7693e4813f810c21836b292770590ec511224109))
|
||||
* **i18n:** keep Discord brand name untranslated in French ([#541](https://github.com/whisper-money/whisper-money/issues/541)) ([06effb5](https://github.com/whisper-money/whisper-money/commit/06effb5e6ef2311657c5beaf9ec57918739eb38f))
|
||||
* **integration-requests:** freeze votes on not-doable requests ([#555](https://github.com/whisper-money/whisper-money/issues/555)) ([89c1ab1](https://github.com/whisper-money/whisper-money/commit/89c1ab1ca8edf4afcab183e11d0b6fc7ab095952))
|
||||
* **open-banking:** only block re-adding a bank when a live connection exists ([#569](https://github.com/whisper-money/whisper-money/issues/569)) ([14c4598](https://github.com/whisper-money/whisper-money/commit/14c4598cda6989017af9dd8551791d5a2c456a9d))
|
||||
* **open-banking:** stop storing the XXX no-currency placeholder on accounts ([#602](https://github.com/whisper-money/whisper-money/issues/602)) ([bc57eae](https://github.com/whisper-money/whisper-money/commit/bc57eae5c34cdf37beb7a82b5569fb50d12e3ab7))
|
||||
* **queue:** add supervisor worker for the ai queue ([#546](https://github.com/whisper-money/whisper-money/issues/546)) ([f191d74](https://github.com/whisper-money/whisper-money/commit/f191d740314836ae12b897b6e9f5151ecc39e15f))
|
||||
* **queue:** raise retry_after above the longest job timeout (PHP-LARAVEL-2D) ([#645](https://github.com/whisper-money/whisper-money/issues/645)) ([05d4bae](https://github.com/whisper-money/whisper-money/commit/05d4bae0af1fece9196fabde61a624cf19472da9))
|
||||
* **security:** scope job-status endpoints to owner + feature-area fixes ([#627](https://github.com/whisper-money/whisper-money/issues/627)) ([d55c3f4](https://github.com/whisper-money/whisper-money/commit/d55c3f41df9705fe533d13f3fa9da0cf7790cb4a)), closes [#4](https://github.com/whisper-money/whisper-money/issues/4) [#1](https://github.com/whisper-money/whisper-money/issues/1)
|
||||
* **sentry:** drop browser-extension noise before sending events ([#568](https://github.com/whisper-money/whisper-money/issues/568)) ([52708f9](https://github.com/whisper-money/whisper-money/commit/52708f940df2a86bc1bf47afe72af08abd4e345f))
|
||||
* **settings:** vertically center rows in automation rules and labels tables ([#615](https://github.com/whisper-money/whisper-money/issues/615)) ([e631cbb](https://github.com/whisper-money/whisper-money/commit/e631cbba69d8184f5efbb4c65d198a836a9ab883))
|
||||
* skip demo reset when demo account is disabled ([#626](https://github.com/whisper-money/whisper-money/issues/626)) ([eb31455](https://github.com/whisper-money/whisper-money/commit/eb31455e606f8e0b965b6b3cd83d4e2c9de34df5))
|
||||
* stop double-dispatching transaction listeners (N+1 insert into jobs) ([#620](https://github.com/whisper-money/whisper-money/issues/620)) ([0f8eca5](https://github.com/whisper-money/whisper-money/commit/0f8eca50d036aca21142fac39ca5ed385d2dbada))
|
||||
* **transactions:** let date column size to its content ([#610](https://github.com/whisper-money/whisper-money/issues/610)) ([5ef3e01](https://github.com/whisper-money/whisper-money/commit/5ef3e01c8905795ca29600bab3f62578ffe0673d))
|
||||
* **transactions:** only auto-select account on account pages ([#549](https://github.com/whisper-money/whisper-money/issues/549)) ([9a20335](https://github.com/whisper-money/whisper-money/commit/9a20335c6a4a7fa814f1460afbf8384888ee2e88))
|
||||
* **transactions:** pad Category column when Date column is hidden ([#584](https://github.com/whisper-money/whisper-money/issues/584)) ([d2806b5](https://github.com/whisper-money/whisper-money/commit/d2806b5887753aca2d7ccce8b2360107541890bc)), closes [#583](https://github.com/whisper-money/whisper-money/issues/583) [#582](https://github.com/whisper-money/whisper-money/issues/582)
|
||||
* **transactions:** pad Category column when Date column is hidden ([#585](https://github.com/whisper-money/whisper-money/issues/585)) ([8e38713](https://github.com/whisper-money/whisper-money/commit/8e3871370ae17e2b582effdb72218ba2ed65b40a)), closes [582/#583](https://github.com/whisper-money/whisper-money/issues/583)
|
||||
* **transactions:** restore left padding when category is first column ([#582](https://github.com/whisper-money/whisper-money/issues/582)) ([d11aa2d](https://github.com/whisper-money/whisper-money/commit/d11aa2dfe579e0a9af27909d51ac776f975b3073))
|
||||
* **ui:** make input borders visible in dark mode ([#571](https://github.com/whisper-money/whisper-money/issues/571)) ([83a5e96](https://github.com/whisper-money/whisper-money/commit/83a5e9657e679d0e849c9f5f532a021dce9807ff))
|
||||
* **worktree:** remove double slash in storage/keys copy path ([#629](https://github.com/whisper-money/whisper-money/issues/629)) ([4615d7a](https://github.com/whisper-money/whisper-money/commit/4615d7a88002f2b3c7df847a27f783a710a69cc6))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **accounts:** reorder accounts with drag-and-drop ([#575](https://github.com/whisper-money/whisper-money/issues/575)) ([cd3080e](https://github.com/whisper-money/whisper-money/commit/cd3080ec52fd2586f9920794559c7b500cbef6bf))
|
||||
* add Wise open banking integration with balance sync ([#525](https://github.com/whisper-money/whisper-money/issues/525)) ([1c5a76a](https://github.com/whisper-money/whisper-money/commit/1c5a76a3a4cc9173d7ba2a7f8e3f67fc303e30d4))
|
||||
* **ai:** dismissable AI consent banner that stops after the first decision ([#617](https://github.com/whisper-money/whisper-money/issues/617)) ([7e36bba](https://github.com/whisper-money/whisper-money/commit/7e36bbafef8faa076b25dc70451b7165b000349a))
|
||||
* **ai:** learn from category corrections so the AI stops repeating the same mistake ([#608](https://github.com/whisper-money/whisper-money/issues/608)) ([6727a9c](https://github.com/whisper-money/whisper-money/commit/6727a9c64a7083ac962e4d489a36c61f4c4e590f))
|
||||
* **ai:** manage AI consent outside onboarding with live backfill ([#591](https://github.com/whisper-money/whisper-money/issues/591)) ([9a458b1](https://github.com/whisper-money/whisper-money/commit/9a458b103131a0b848bdc17b5d015c923a30d71e))
|
||||
* **ai:** persist AI categorization suggestions below the label bar ([#547](https://github.com/whisper-money/whisper-money/issues/547)) ([9328cd3](https://github.com/whisper-money/whisper-money/commit/9328cd3e1ba53218b521d6501d8750bb483a2ea6))
|
||||
* **ai:** record the model behind each AI categorization ([#594](https://github.com/whisper-money/whisper-money/issues/594)) ([291cfbe](https://github.com/whisper-money/whisper-money/commit/291cfbe2612a3682f913216a64ebc76c699e55a2))
|
||||
* **banking:** add Interactive Brokers sync via Flex Web Service ([#581](https://github.com/whisper-money/whisper-money/issues/581)) ([f60e6d7](https://github.com/whisper-money/whisper-money/commit/f60e6d70355d77b05ff4cad690cea65c1eb5792d))
|
||||
* **banking:** enable Interactive Brokers for all users ([#593](https://github.com/whisper-money/whisper-money/issues/593)) ([094ff4d](https://github.com/whisper-money/whisper-money/commit/094ff4d5ac1e4ebab787056b8794654ac49f0cd8))
|
||||
* **banking:** let Wise credentials be updated ([#588](https://github.com/whisper-money/whisper-money/issues/588)) ([619ed0f](https://github.com/whisper-money/whisper-money/commit/619ed0f1db44f004b8e4704e2a4e1bbef0e56a74)), closes [#581](https://github.com/whisper-money/whisper-money/issues/581)
|
||||
* **connections:** create a new account from the manage-accounts selector ([#560](https://github.com/whisper-money/whisper-money/issues/560)) ([6cb8d11](https://github.com/whisper-money/whisper-money/commit/6cb8d115631d41d01f0dee025092decc4c31e01c))
|
||||
* **connections:** manage which accounts a bank connection syncs ([#558](https://github.com/whisper-money/whisper-money/issues/558)) ([a9b90a2](https://github.com/whisper-money/whisper-money/commit/a9b90a200efc66d85e20405872a857db829255cf))
|
||||
* **currencies:** add Nigerian Naira (NGN) ([#642](https://github.com/whisper-money/whisper-money/issues/642)) ([6ff7edf](https://github.com/whisper-money/whisper-money/commit/6ff7edf193bfb9baf51b4907a9fe0da49a95f533))
|
||||
* **currency:** add GHS (Ghanaian Cedi) ([#644](https://github.com/whisper-money/whisper-money/issues/644)) ([2aebe45](https://github.com/whisper-money/whisper-money/commit/2aebe45d1f5a481760fd2c36ec98436b29d1c510)), closes [#567](https://github.com/whisper-money/whisper-money/issues/567)
|
||||
* **currency:** add RSD (Serbian Dinar) ([#567](https://github.com/whisper-money/whisper-money/issues/567)) ([934e16c](https://github.com/whisper-money/whisper-money/commit/934e16c0fa2659a7e701d6cfcc23cbaad67d0c13))
|
||||
* **dashboard:** add accounts manager dialog with visibility toggle and reorder ([#604](https://github.com/whisper-money/whisper-money/issues/604)) ([777dfc0](https://github.com/whisper-money/whisper-money/commit/777dfc07b281a5df522fc93154b73e6d7c72d2ef))
|
||||
* **demo:** gate demo account access behind a config flag ([#580](https://github.com/whisper-money/whisper-money/issues/580)) ([a346566](https://github.com/whisper-money/whisper-money/commit/a346566fd0a6398bb7e9b146617f88d0d218a35f))
|
||||
* **drip:** email users stuck on the paywall a day after onboarding ([#562](https://github.com/whisper-money/whisper-money/issues/562)) ([ce6bfc9](https://github.com/whisper-money/whisper-money/commit/ce6bfc9c562195c7dc89028fe3a920a814ff1b7a))
|
||||
* **email:** follow up after post-onboarding AI consent ([#596](https://github.com/whisper-money/whisper-money/issues/596)) ([934d834](https://github.com/whisper-money/whisper-money/commit/934d834ab3dd19d66525fbd883d30de1b3d77982))
|
||||
* **encryption:** commands to warn and remove inactive encrypted-data accounts ([#633](https://github.com/whisper-money/whisper-money/issues/633)) ([477e4d5](https://github.com/whisper-money/whisper-money/commit/477e4d50e26760d387dee1784db7093c05ce593e))
|
||||
* **features:** support percentage rollouts in feature:enable ([#592](https://github.com/whisper-money/whisper-money/issues/592)) ([f72e2a6](https://github.com/whisper-money/whisper-money/commit/f72e2a64ca1101a04ddc3c3384f5f7c75aed8e5d))
|
||||
* **i18n:** add French translation support ([#532](https://github.com/whisper-money/whisper-money/issues/532)) ([a38ed69](https://github.com/whisper-money/whisper-money/commit/a38ed69d2e134651ceddae5082fd2d70493b83e0))
|
||||
* **integration-requests:** add done status and fix review command crash on orphaned author ([#601](https://github.com/whisper-money/whisper-money/issues/601)) ([e4be39b](https://github.com/whisper-money/whisper-money/commit/e4be39be1201b54894097e9580958d7cd23c4740))
|
||||
* **integration-requests:** add not-doable status with a public comment ([#552](https://github.com/whisper-money/whisper-money/issues/552)) ([801f6c7](https://github.com/whisper-money/whisper-money/commit/801f6c7cd4834eeecbe77a078994c89845003a70))
|
||||
* **integration-requests:** community board to request & vote bank integrations ([#550](https://github.com/whisper-money/whisper-money/issues/550)) ([5e8f227](https://github.com/whisper-money/whisper-money/commit/5e8f227fbdabbd15fd752645e7a1405803f032d9))
|
||||
* **integration-requests:** let the admin bypass limits and auto-approve ([#551](https://github.com/whisper-money/whisper-money/issues/551)) ([7e9122e](https://github.com/whisper-money/whisper-money/commit/7e9122eaf442f0ea4cd2f2eecf1dfea1c8e7f7fd))
|
||||
* **integration-requests:** markdown comments and in-progress status ([#553](https://github.com/whisper-money/whisper-money/issues/553)) ([da88adb](https://github.com/whisper-money/whisper-money/commit/da88adbee36c899fa24342d0b62c0cf1063df689))
|
||||
* **integration-requests:** multi-vote, per-plan quota and undo ([#554](https://github.com/whisper-money/whisper-money/issues/554)) ([0ea54fa](https://github.com/whisper-money/whisper-money/commit/0ea54fa0d75dd268c7cb5e59f1da95a9a22976ee))
|
||||
* **landing:** clarify AI framing and add testimonials ([#613](https://github.com/whisper-money/whisper-money/issues/613)) ([d55e15b](https://github.com/whisper-money/whisper-money/commit/d55e15bb4faabf34e4f1f92022db4a17fe085dec))
|
||||
* **onboarding:** auto-enable AI for connected banks, ask the rest ([#618](https://github.com/whisper-money/whisper-money/issues/618)) ([10442c1](https://github.com/whisper-money/whisper-money/commit/10442c1e3293270dcc75a299414b793c5db14610))
|
||||
* **onboarding:** clarify the "categorize at least 5" goal in the categorizer ([#616](https://github.com/whisper-money/whisper-money/issues/616)) ([af64f56](https://github.com/whisper-money/whisper-money/commit/af64f563991897723c69749ac69bf724b162f44c))
|
||||
* **open-banking:** allow re-connecting a bank behind a replace warning ([#570](https://github.com/whisper-money/whisper-money/issues/570)) ([64827fa](https://github.com/whisper-money/whisper-money/commit/64827fabae82cadd98febdd457dcf0106934cc18)), closes [#569](https://github.com/whisper-money/whisper-money/issues/569)
|
||||
* **open-banking:** disable already-connected banks in the connect picker ([#556](https://github.com/whisper-money/whisper-money/issues/556)) ([6e6433c](https://github.com/whisper-money/whisper-money/commit/6e6433c6ad8c6673edc5f263a76096bc4377da96))
|
||||
* **open-banking:** enable manage bank accounts for everyone ([#572](https://github.com/whisper-money/whisper-money/issues/572)) ([0f3cdd4](https://github.com/whisper-money/whisper-money/commit/0f3cdd41aaa72a9544202a3d5add2515cf5ac6ab))
|
||||
* **paywall:** require a plan when the user has accepted AI ([#564](https://github.com/whisper-money/whisper-money/issues/564)) ([29d13ce](https://github.com/whisper-money/whisper-money/commit/29d13ceed103860399b271ffed3cb17810112001))
|
||||
* **stats:** add --no-discord flag to stats:experiment-funnel ([#606](https://github.com/whisper-money/whisper-money/issues/606)) ([1db2871](https://github.com/whisper-money/whisper-money/commit/1db2871398bd86deb0a3c48ba1f1881822e016cc))
|
||||
* **stats:** add --no-discord to the remaining report commands ([#607](https://github.com/whisper-money/whisper-money/issues/607)) ([300756e](https://github.com/whisper-money/whisper-money/commit/300756e55341b84245efbbd37a038ae7edb7217b))
|
||||
* **stats:** add weekly subscription funnel report ([#599](https://github.com/whisper-money/whisper-money/issues/599)) ([756b481](https://github.com/whisper-money/whisper-money/commit/756b4816a6d67922a15cf6b69e9933f8f1bea2c8))
|
||||
* **stats:** weekly paywall stuck-cohort report to Discord ([#563](https://github.com/whisper-money/whisper-money/issues/563)) ([57f8c93](https://github.com/whisper-money/whisper-money/commit/57f8c93e2818ad53abcd8d1ad656aeb1f1edda4d))
|
||||
* **subscriptions:** reframe pay_now paywall copy around try-and-refund ([#605](https://github.com/whisper-money/whisper-money/issues/605)) ([09d6e8e](https://github.com/whisper-money/whisper-money/commit/09d6e8ee6cfe6247439bfb3bfd475ab1a092e722))
|
||||
* **subscriptions:** trial/pricing A/B/C experiment ([#600](https://github.com/whisper-money/whisper-money/issues/600)) ([e5350ff](https://github.com/whisper-money/whisper-money/commit/e5350ff1a6061ee3bdb9596e3b6e925618e39e3e))
|
||||
* **support:** add support link with community-first help modal ([#542](https://github.com/whisper-money/whisper-money/issues/542)) ([4a891a5](https://github.com/whisper-money/whisper-money/commit/4a891a5906d57f17bbf0aeeec957656e7e67f937))
|
||||
* **transactions:** default balance toggle on and apply it server-side ([#566](https://github.com/whisper-money/whisper-money/issues/566)) ([b76a0de](https://github.com/whisper-money/whisper-money/commit/b76a0de0746e334835c84e7aef96140e8ef7d00f))
|
||||
* **transactions:** highlight new transactions since last visit ([#609](https://github.com/whisper-money/whisper-money/issues/609)) ([884038c](https://github.com/whisper-money/whisper-money/commit/884038c13bd093ca5ec1f6a656af40453f085efc))
|
||||
* **transactions:** make new-transaction marker cross-device ([#611](https://github.com/whisper-money/whisper-money/issues/611)) ([ee69c51](https://github.com/whisper-money/whisper-money/commit/ee69c51a846a944632e6c23e098014f768229310)), closes [#609](https://github.com/whisper-money/whisper-money/issues/609)
|
||||
* **transactions:** refine new transaction form layout and balance toggle ([#597](https://github.com/whisper-money/whisper-money/issues/597)) ([d6ec983](https://github.com/whisper-money/whisper-money/commit/d6ec9830dff95b1fa869b57604ccc892e97113bb))
|
||||
* **transactions:** release transaction analysis to all users ([#579](https://github.com/whisper-money/whisper-money/issues/579)) ([ae6f869](https://github.com/whisper-money/whisper-money/commit/ae6f8696118cc9d4808a8ee9270de2b25850dd70))
|
||||
* **transactions:** reorder filters and switch accounts to a logo dropdown ([#598](https://github.com/whisper-money/whisper-money/issues/598)) ([d7bc4e6](https://github.com/whisper-money/whisper-money/commit/d7bc4e6707a802044b5f931c54cefca23dcbeebc))
|
||||
* **transactions:** serve import dedup and account ledger from the backend ([#631](https://github.com/whisper-money/whisper-money/issues/631)) ([021cb66](https://github.com/whisper-money/whisper-money/commit/021cb6664311ec475e87b628ca7a88baf8de4907))
|
||||
* **welcome:** add Francisco Montes testimonial ([#636](https://github.com/whisper-money/whisper-money/issues/636)) ([02087ab](https://github.com/whisper-money/whisper-money/commit/02087abcc7c7e9f7c210ae922c7813206db6093e))
|
||||
* **welcome:** add Haru testimonial with Discord avatar ([#577](https://github.com/whisper-money/whisper-money/issues/577)) ([b0e74fa](https://github.com/whisper-money/whisper-money/commit/b0e74fac2c629078d4d88b9a2365d343571cc0e6))
|
||||
|
||||
|
||||
### Performance Improvements
|
||||
|
||||
* **accounts:** defer the account ledger prop on show ([#632](https://github.com/whisper-money/whisper-money/issues/632)) ([9326d8f](https://github.com/whisper-money/whisper-money/commit/9326d8fd2fd3c9739cffa7d0f2f384fa49a623c1)), closes [#631](https://github.com/whisper-money/whisper-money/issues/631) [#631](https://github.com/whisper-money/whisper-money/issues/631) [#631](https://github.com/whisper-money/whisper-money/issues/631)
|
||||
* **ai:** reduce N+1 in bulk category updates (PHP-LARAVEL-40, partial) ([#624](https://github.com/whisper-money/whisper-money/issues/624)) ([3d3f6da](https://github.com/whisper-money/whisper-money/commit/3d3f6daa77c130e5a91547d9426189a1e820cad5)), closes [#2](https://github.com/whisper-money/whisper-money/issues/2)
|
||||
* **banking:** kill per-transaction dedup N+1 in bank sync (PHP-LARAVEL-3Y) ([#621](https://github.com/whisper-money/whisper-money/issues/621)) ([84bad76](https://github.com/whisper-money/whisper-money/commit/84bad76316425616899f03157fa8246144635288))
|
||||
* **db:** index transactions for the daily synced-email slow query (PHP-LARAVEL-3X) ([#622](https://github.com/whisper-money/whisper-money/issues/622)) ([ad46e46](https://github.com/whisper-money/whisper-money/commit/ad46e465be3cb2d3a29726a7c4cc2f822ecf67a3))
|
||||
|
||||
## [0.2.5](https://github.com/whisper-money/whisper-money/compare/v0.2.4...v0.2.5) (2026-06-15)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **account:** block deletion while subscription or trial is active ([#531](https://github.com/whisper-money/whisper-money/issues/531)) ([2bfb569](https://github.com/whisper-money/whisper-money/commit/2bfb569a2226df44b71363e731889ab07a2a41c4)) by [@victor-falcon](https://github.com/victor-falcon)
|
||||
* **analysis:** align summary card amounts to a common baseline ([#515](https://github.com/whisper-money/whisper-money/issues/515)) ([21f8f3b](https://github.com/whisper-money/whisper-money/commit/21f8f3b27719f3ff47b4769e5fdda00c323fe22d)) by [@victor-falcon](https://github.com/victor-falcon)
|
||||
* **analysis:** truncate long breakdown names so amounts stay in widget ([#518](https://github.com/whisper-money/whisper-money/issues/518)) ([49acc8a](https://github.com/whisper-money/whisper-money/commit/49acc8a884617fc8a97e7b82e7dcedb5931a20b4)) by [@victor-falcon](https://github.com/victor-falcon)
|
||||
* **auth:** prevent FormData crash on successful login ([#503](https://github.com/whisper-money/whisper-money/issues/503)) ([f4bbbfd](https://github.com/whisper-money/whisper-money/commit/f4bbbfd767388642b856b53814187b9c85e4ac22)) by [@victor-falcon](https://github.com/victor-falcon)
|
||||
* **automation-rules:** hint amount sign for expenses vs income ([#524](https://github.com/whisper-money/whisper-money/issues/524)) ([e526f86](https://github.com/whisper-money/whisper-money/commit/e526f861b2a7f46a073273482f10111f06e44f84)) by [@victor-falcon](https://github.com/victor-falcon)
|
||||
* **budgets:** make period generation idempotent ([#533](https://github.com/whisper-money/whisper-money/issues/533)) ([cd323bb](https://github.com/whisper-money/whisper-money/commit/cd323bbe529678e68bca23e84a9cdb0d78c33373)) by [@victor-falcon](https://github.com/victor-falcon)
|
||||
* **cashflow:** bound trend window to prevent request timeout ([#534](https://github.com/whisper-money/whisper-money/issues/534)) ([1d4bcd5](https://github.com/whisper-money/whisper-money/commit/1d4bcd5082413071ddcc2764ab866e23ea73ab5a)) by [@victor-falcon](https://github.com/victor-falcon)
|
||||
* **currency:** make rate fetching resilient to slow CDN ([#502](https://github.com/whisper-money/whisper-money/issues/502)) ([4b90bcf](https://github.com/whisper-money/whisper-money/commit/4b90bcfc96b4dd6340981d53f2e665d02872288f)) by [@victor-falcon](https://github.com/victor-falcon)
|
||||
* **header:** keep mobile logo on one line, compact auth buttons ([#512](https://github.com/whisper-money/whisper-money/issues/512)) ([1530544](https://github.com/whisper-money/whisper-money/commit/1530544b8b3322f90829991df3d41d06e43e54a8)) by [@victor-falcon](https://github.com/victor-falcon)
|
||||
* **import:** honor selected date format for CSV imports ([#494](https://github.com/whisper-money/whisper-money/issues/494)) ([744d874](https://github.com/whisper-money/whisper-money/commit/744d874464b803b4f9a187347cb833c9440a62d1)) by [@victor-falcon](https://github.com/victor-falcon)
|
||||
* **layout:** keep bottom padding while floating nav is visible ([#537](https://github.com/whisper-money/whisper-money/issues/537)) ([6671d89](https://github.com/whisper-money/whisper-money/commit/6671d89ea10638983a5ba10a718ea16dde982697)) by [@victor-falcon](https://github.com/victor-falcon)
|
||||
* **perf:** batch feature flag resolution in shared Inertia data ([#500](https://github.com/whisper-money/whisper-money/issues/500)) ([cb728ce](https://github.com/whisper-money/whisper-money/commit/cb728ce176fbac50984c183abae8af19acf1c8e7)) by [@victor-falcon](https://github.com/victor-falcon), closes [#496](https://github.com/whisper-money/whisper-money/issues/496)
|
||||
* **sentry:** only report errors in production ([#467](https://github.com/whisper-money/whisper-money/issues/467)) ([d68fee6](https://github.com/whisper-money/whisper-money/commit/d68fee6c2dd54f923cb0a57c1dd483d5fa4e1ed5)) by [@victor-falcon](https://github.com/victor-falcon)
|
||||
* **transactions:** combine category and label filters with OR ([#495](https://github.com/whisper-money/whisper-money/issues/495)) ([af87ac7](https://github.com/whisper-money/whisper-money/commit/af87ac75600060939b7c27e99548bace462a0d73)) by [@victor-falcon](https://github.com/victor-falcon)
|
||||
* **transactions:** improve mobile analysis button affordance ([#520](https://github.com/whisper-money/whisper-money/issues/520)) ([3223824](https://github.com/whisper-money/whisper-money/commit/3223824edb03d3f5657af55dc8b01a8e79259f06)) by [@victor-falcon](https://github.com/victor-falcon)
|
||||
* **transactions:** prevent crash when sorting by nullable column ([#501](https://github.com/whisper-money/whisper-money/issues/501)) ([a69c602](https://github.com/whisper-money/whisper-money/commit/a69c602366c5a2c8e18ad106827fc30693ba8471)) by [@victor-falcon](https://github.com/victor-falcon)
|
||||
* **transactions:** tidy filter bar into two balanced rows ([#510](https://github.com/whisper-money/whisper-money/issues/510)) ([6486908](https://github.com/whisper-money/whisper-money/commit/6486908716e563b3b8970b9ec7b73989d7ee72be)) by [@victor-falcon](https://github.com/victor-falcon)
|
||||
* verify email via signed link without requiring login ([#490](https://github.com/whisper-money/whisper-money/issues/490)) ([14b7955](https://github.com/whisper-money/whisper-money/commit/14b79557d79dd1c5c5876b6c6b1a480baedd536e)) by [@victor-falcon](https://github.com/victor-falcon)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add catch-all budgets ([#527](https://github.com/whisper-money/whisper-money/issues/527)) ([dbec1c4](https://github.com/whisper-money/whisper-money/commit/dbec1c4c134a257b95c3c1402590a6727026a4d4)) by [@tonigruni](https://github.com/tonigruni)
|
||||
* **ai:** add weekly AI-suggestions cohort report ([#530](https://github.com/whisper-money/whisper-money/issues/530)) ([906e3cc](https://github.com/whisper-money/whisper-money/commit/906e3cc2b466428e7efa4c4c66cb56a068475451)) by [@victor-falcon](https://github.com/victor-falcon)
|
||||
* **ai:** auto-categorize transactions with AI (behind flag) ([#535](https://github.com/whisper-money/whisper-money/issues/535)) ([8013a0b](https://github.com/whisper-money/whisper-money/commit/8013a0b6f2d53c6df64bcf6019592dd1f8e477d1)) by [@victor-falcon](https://github.com/victor-falcon)
|
||||
* **ai:** defer per-transaction categorization until onboarding completes ([#536](https://github.com/whisper-money/whisper-money/issues/536)) ([e065d4a](https://github.com/whisper-money/whisper-money/commit/e065d4ab65f603f5396cafcf4634e544c2eead2f)) by [@victor-falcon](https://github.com/victor-falcon)
|
||||
* **ai:** share AI sparkle icon and mark AI-generated rules ([#538](https://github.com/whisper-money/whisper-money/issues/538)) ([7dde67c](https://github.com/whisper-money/whisper-money/commit/7dde67c606fa0f8603f7603c0a6769df6755028c)) by [@victor-falcon](https://github.com/victor-falcon)
|
||||
* **ai:** suggest automation rules during onboarding ([#523](https://github.com/whisper-money/whisper-money/issues/523)) ([8056ede](https://github.com/whisper-money/whisper-money/commit/8056ede63644a4fac2afa88fcc9d89d7fb3bcea1)) by [@victor-falcon](https://github.com/victor-falcon)
|
||||
* **analysis:** break down spending by sub-category ([#508](https://github.com/whisper-money/whisper-money/issues/508)) ([c944a2c](https://github.com/whisper-money/whisper-money/commit/c944a2c37ecfab3c483a6c0ffa3596bdd8b73f01)) by [@victor-falcon](https://github.com/victor-falcon)
|
||||
* **analysis:** per-category 12-month spending drawer ([#519](https://github.com/whisper-money/whisper-money/issues/519)) ([9c3c4d5](https://github.com/whisper-money/whisper-money/commit/9c3c4d573ee8a04155d56da39bc213b052a138a4)) by [@victor-falcon](https://github.com/victor-falcon)
|
||||
* **analysis:** project-aware transaction analysis ([#513](https://github.com/whisper-money/whisper-money/issues/513)) ([7cc49a5](https://github.com/whisper-money/whisper-money/commit/7cc49a53689d95855ca254a4df8ebd6fbad5720c)) by [@victor-falcon](https://github.com/victor-falcon)
|
||||
* **analysis:** shared bar-list breakdowns in transaction drawer ([#517](https://github.com/whisper-money/whisper-money/issues/517)) ([bcd025f](https://github.com/whisper-money/whisper-money/commit/bcd025f1b16490baf871718b24db71997895e07e)) by [@victor-falcon](https://github.com/victor-falcon)
|
||||
* **auth:** show/hide toggle on password fields ([#499](https://github.com/whisper-money/whisper-money/issues/499)) ([58254fc](https://github.com/whisper-money/whisper-money/commit/58254fcedeb76ff99b88f5b5fafe00f807510f91)) by [@victor-falcon](https://github.com/victor-falcon)
|
||||
* **banking:** add command to disconnect connections by id ([#497](https://github.com/whisper-money/whisper-money/issues/497)) ([9192947](https://github.com/whisper-money/whisper-money/commit/91929477ab94cd721bc8b6c3a7c9a28dcdfb31cf)) by [@victor-falcon](https://github.com/victor-falcon)
|
||||
* **cashflow:** enlarge and color-code net cashflow and saved cards ([#505](https://github.com/whisper-money/whisper-money/issues/505)) ([346e21a](https://github.com/whisper-money/whisper-money/commit/346e21ad4eec3b9dd7369abd6503b66fd7262c92)) by [@victor-falcon](https://github.com/victor-falcon)
|
||||
* **console:** add agent:db command for querying local and prod DB ([#522](https://github.com/whisper-money/whisper-money/issues/522)) ([d2a4412](https://github.com/whisper-money/whisper-money/commit/d2a44121189624ab5e5c8e1ac1baac4ffd17ec21)) by [@victor-falcon](https://github.com/victor-falcon)
|
||||
* **currencies:** add Colombian and Dominican peso ([#471](https://github.com/whisper-money/whisper-money/issues/471)) ([e5b4933](https://github.com/whisper-money/whisper-money/commit/e5b493329a6b5b8e6fdf04f873e591546c42441d)) by [@victor-falcon](https://github.com/victor-falcon)
|
||||
* **currency:** add NZD (New Zealand Dollar) ([#504](https://github.com/whisper-money/whisper-money/issues/504)) ([899ea6a](https://github.com/whisper-money/whisper-money/commit/899ea6a939787e567a41566aeb6fbeee6885600d)) by [@victor-falcon](https://github.com/victor-falcon)
|
||||
* enable category tree for all users, remove flag ([#488](https://github.com/whisper-money/whisper-money/issues/488)) ([f8cc881](https://github.com/whisper-money/whisper-money/commit/f8cc8816a898514a558667a2938f6255363b2ed7)) by [@victor-falcon](https://github.com/victor-falcon)
|
||||
* expand parent categories inline in breakdowns ([#486](https://github.com/whisper-money/whisper-money/issues/486)) ([53d0518](https://github.com/whisper-money/whisper-money/commit/53d051800bb1e676563bbc1038e607efc186bebd)) by [@victor-falcon](https://github.com/victor-falcon)
|
||||
* expand Sankey subcategories inline ([#485](https://github.com/whisper-money/whisper-money/issues/485)) ([679c8d7](https://github.com/whisper-money/whisper-money/commit/679c8d7bff3b69a5b70c55ae90fd1d8236af99ee)) by [@victor-falcon](https://github.com/victor-falcon)
|
||||
* **importer:** support YYYYMMDD date format ([#470](https://github.com/whisper-money/whisper-money/issues/470)) ([f9d1303](https://github.com/whisper-money/whisper-money/commit/f9d1303d986aa2cee9ee6dbf92d6a3fb708f8f05)) by [@victor-falcon](https://github.com/victor-falcon)
|
||||
* **landing:** add go now button to redirect modal ([#506](https://github.com/whisper-money/whisper-money/issues/506)) ([c53c40c](https://github.com/whisper-money/whisper-money/commit/c53c40cf0b54a94c163f28ebd504dd720ff03339)) by [@victor-falcon](https://github.com/victor-falcon)
|
||||
* **landing:** real testimonials with gravatar/facehash avatars ([#493](https://github.com/whisper-money/whisper-money/issues/493)) ([300188f](https://github.com/whisper-money/whisper-money/commit/300188f135a598a4ae2a5bb6f119af1d13cae7eb)) by [@victor-falcon](https://github.com/victor-falcon)
|
||||
* **open-banking:** finalize bank connection without a session via state token ([#498](https://github.com/whisper-money/whisper-money/issues/498)) ([a7dde5f](https://github.com/whisper-money/whisper-money/commit/a7dde5fbc5e1a09d55d64ff101e5ea4061335fd0)) by [@victor-falcon](https://github.com/victor-falcon)
|
||||
* optionally update manual account balance on transaction delete ([#491](https://github.com/whisper-money/whisper-money/issues/491)) ([45e25e0](https://github.com/whisper-money/whisper-money/commit/45e25e018de377622197ae0c3c39c9cab6053220)) by [@victor-falcon](https://github.com/victor-falcon)
|
||||
* parent/child category tree ([#474](https://github.com/whisper-money/whisper-money/issues/474)) ([1cc1056](https://github.com/whisper-money/whisper-money/commit/1cc10566a36562fa37ac7ecb9592da03284f5f51)) by [@victor-falcon](https://github.com/victor-falcon)
|
||||
* **settings:** let users disable bank transactions email ([#472](https://github.com/whisper-money/whisper-money/issues/472)) ([e178f1b](https://github.com/whisper-money/whisper-money/commit/e178f1b1bdb57a778ae2124e651078d1904f71c4)) by [@victor-falcon](https://github.com/victor-falcon)
|
||||
* single-open Sankey expand, fit overflowing children ([#487](https://github.com/whisper-money/whisper-money/issues/487)) ([721cbef](https://github.com/whisper-money/whisper-money/commit/721cbef02464acb40ab1428c769b65a042b4d96a)) by [@victor-falcon](https://github.com/victor-falcon)
|
||||
* **transactions:** filtered analysis dashboard ([#507](https://github.com/whisper-money/whisper-money/issues/507)) ([8375fd4](https://github.com/whisper-money/whisper-money/commit/8375fd490ecc473bbcb7450830ba5b55e4b6bb26)) by [@victor-falcon](https://github.com/victor-falcon)
|
||||
* **transactions:** save and reuse transaction filters ([#496](https://github.com/whisper-money/whisper-money/issues/496)) ([8df44c2](https://github.com/whisper-money/whisper-money/commit/8df44c2ef492abe4a9f5d385c9eb5ca0835c7675)) by [@victor-falcon](https://github.com/victor-falcon)
|
||||
* **users:** track last login and last active timestamps ([#516](https://github.com/whisper-money/whisper-money/issues/516)) ([fcf2d3d](https://github.com/whisper-money/whisper-money/commit/fcf2d3d1ad5f4f85542bd92f7bdaf23ec26c8c50)) by [@victor-falcon](https://github.com/victor-falcon)
|
||||
* **welcome:** add Albert G. testimonial ([#529](https://github.com/whisper-money/whisper-money/issues/529)) ([0f48ffb](https://github.com/whisper-money/whisper-money/commit/0f48ffbbeffe12321d434c9e4714d09eb2ce9b02)) by [@victor-falcon](https://github.com/victor-falcon)
|
||||
|
||||
## [0.2.4](https://github.com/whisper-money/whisper-money/compare/v0.2.3...v0.2.4) (2026-06-01)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **accounts:** sync currency from first account ([#430](https://github.com/whisper-money/whisper-money/issues/430)) ([0d4c683](https://github.com/whisper-money/whisper-money/commit/0d4c68361afc114615323418e2cfcf380e5ac13f))
|
||||
* **accounts:** translate update button in edit account modal ([#455](https://github.com/whisper-money/whisper-money/issues/455)) ([65175e1](https://github.com/whisper-money/whisper-money/commit/65175e184a900404b12a5acccabfa0d700ba57ea))
|
||||
* **automation:** avoid rule preview n+1 ([#431](https://github.com/whisper-money/whisper-money/issues/431)) ([fd67cf7](https://github.com/whisper-money/whisper-money/commit/fd67cf7c72148ce6e3afa97a2cb35893f3457e4b))
|
||||
* **automation:** avoid skipping rule matches ([#433](https://github.com/whisper-money/whisper-money/issues/433)) ([9772cfc](https://github.com/whisper-money/whisper-money/commit/9772cfc37c19d521ab8b1cd64a5de467ae4ae5ce))
|
||||
* **banking:** handle balance-fetch timeouts and silence handled retries ([#450](https://github.com/whisper-money/whisper-money/issues/450)) ([64b78e3](https://github.com/whisper-money/whisper-money/commit/64b78e36801a34321ee2fc12ef41a2938d13c572))
|
||||
* batch automation rule application ([#435](https://github.com/whisper-money/whisper-money/issues/435)) ([606093d](https://github.com/whisper-money/whisper-money/commit/606093d311f307abe76e2c5ac10c7afc6f927578))
|
||||
* **budgets:** explain locked edit fields ([#437](https://github.com/whisper-money/whisper-money/issues/437)) ([235911b](https://github.com/whisper-money/whisper-money/commit/235911b960c80ba1f18dc059ced6af7b43b225da))
|
||||
* **cashflow:** clarify period comparisons ([#436](https://github.com/whisper-money/whisper-money/issues/436)) ([0250fdc](https://github.com/whisper-money/whisper-money/commit/0250fdc268ab27e4058a4a5bf6efa4b699133944))
|
||||
* **cashflow:** defer period label translation ([#427](https://github.com/whisper-money/whisper-money/issues/427)) ([1278a2b](https://github.com/whisper-money/whisper-money/commit/1278a2b972bf85649d0f2390277765b8a3f0bc8b))
|
||||
* **cashflow:** stack mobile header controls ([#426](https://github.com/whisper-money/whisper-money/issues/426)) ([949ca11](https://github.com/whisper-money/whisper-money/commit/949ca110fa2d250e316af45d5ca42a932b919343))
|
||||
* **categories:** allow recreate after delete ([#444](https://github.com/whisper-money/whisper-money/issues/444)) ([2fa822e](https://github.com/whisper-money/whisper-money/commit/2fa822e6d960f73b4d168e012a1a003a86415f85))
|
||||
* **categories:** expose cashflow setting on create ([#448](https://github.com/whisper-money/whisper-money/issues/448)) ([5119528](https://github.com/whisper-money/whisper-money/commit/5119528149a1ea1686e70d081418a10ec61edd68))
|
||||
* **chart:** mask stacked bar edges ([#439](https://github.com/whisper-money/whisper-money/issues/439)) ([d5d262e](https://github.com/whisper-money/whisper-money/commit/d5d262e9fd5720b21bbe1c44500ef1034290cf9a))
|
||||
* **currency:** degrade gracefully when rates return 404 ([#449](https://github.com/whisper-money/whisper-money/issues/449)) ([bef657c](https://github.com/whisper-money/whisper-money/commit/bef657c2ed8a7ed7be1b7c7d232dc037d76ee39a))
|
||||
* filter Safari cashback extension errors ([#447](https://github.com/whisper-money/whisper-money/issues/447)) ([0b94067](https://github.com/whisper-money/whisper-money/commit/0b9406714e158244b64cb449e6b19320305ee6b9))
|
||||
* **import:** correct balance for same-day, zero and negative values ([#456](https://github.com/whisper-money/whisper-money/issues/456)) ([144d919](https://github.com/whisper-money/whisper-money/commit/144d919c0b23d8231b1095e148d4ca4a58f0e955))
|
||||
* **logging:** keep laravel.log writable across container UIDs ([#451](https://github.com/whisper-money/whisper-money/issues/451)) ([741dc49](https://github.com/whisper-money/whisper-money/commit/741dc49d5371b3aa867d45c78974fe0049d6b346))
|
||||
* move community link to user menu ([#442](https://github.com/whisper-money/whisper-money/issues/442)) ([4f46ae3](https://github.com/whisper-money/whisper-money/commit/4f46ae3e2d9e365a1bb45355fe2f2c310cab2bb8))
|
||||
* net category refunds in cashflow ([#441](https://github.com/whisper-money/whisper-money/issues/441)) ([6caadad](https://github.com/whisper-money/whisper-money/commit/6caadad1dbc267ea5d2bb58f2ce92200146349bf))
|
||||
* **register:** don't block signup on unrecognized browser timezone ([#462](https://github.com/whisper-money/whisper-money/issues/462)) ([96ee311](https://github.com/whisper-money/whisper-money/commit/96ee311299b0fb6d8234fd17cab44caa3c886488))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **accounts:** add transaction action ([#438](https://github.com/whisper-money/whisper-money/issues/438)) ([534a147](https://github.com/whisper-money/whisper-money/commit/534a14790e777f3bd3d993fe6083fef40a9727ab))
|
||||
* **accounts:** show 50 transactions per page and link to full list ([#459](https://github.com/whisper-money/whisper-money/issues/459)) ([85ea3cc](https://github.com/whisper-money/whisper-money/commit/85ea3cc71416d446fdc8858fbd8a562a02334182))
|
||||
* add BRL currency support ([#453](https://github.com/whisper-money/whisper-money/issues/453)) ([4dec0ab](https://github.com/whisper-money/whisper-money/commit/4dec0ab7ca14e3100ee2b584a22b39f8cdf8e110))
|
||||
* add Discord admin feed for daily stats and Stripe events ([#458](https://github.com/whisper-money/whisper-money/issues/458)) ([0b528b7](https://github.com/whisper-money/whisper-money/commit/0b528b79025314294db53a61a07231199b2b864c)), closes [#457](https://github.com/whisper-money/whisper-money/issues/457)
|
||||
* add PKR currency support ([#443](https://github.com/whisper-money/whisper-money/issues/443)) ([cfa61fd](https://github.com/whisper-money/whisper-money/commit/cfa61fd23cc9b7888755684d24ede3f6a35863cb))
|
||||
* add Stripe subscription stats command ([#457](https://github.com/whisper-money/whisper-money/issues/457)) ([670a0a6](https://github.com/whisper-money/whisper-money/commit/670a0a65c73d19da091f95ea9a381d7dcab1b240))
|
||||
* **budgets:** track multiple categories and labels per budget ([#466](https://github.com/whisper-money/whisper-money/issues/466)) ([71dd6e2](https://github.com/whisper-money/whisper-money/commit/71dd6e2b7fcf44d889641842a13e19091a47d186))
|
||||
* **cashflow:** add savings and period views ([#424](https://github.com/whisper-money/whisper-money/issues/424)) ([ed737db](https://github.com/whisper-money/whisper-money/commit/ed737db7b28442ea3674290eb7eb3f15744dd5b2))
|
||||
* **cashflow:** rework summary cards into net + saved/invested ([#465](https://github.com/whisper-money/whisper-money/issues/465)) ([5ce439f](https://github.com/whisper-money/whisper-money/commit/5ce439f463bfdac7ebb3aa5c0396d098999fdad6))
|
||||
* **categorize:** show debtor and creditor names ([#454](https://github.com/whisper-money/whisper-money/issues/454)) ([05ee8dc](https://github.com/whisper-money/whisper-money/commit/05ee8dc44258e7c9e6dcb6e77afedeb92f3329a2))
|
||||
* **currency:** add Saudi Riyal (SAR) ([#461](https://github.com/whisper-money/whisper-money/issues/461)) ([a71626a](https://github.com/whisper-money/whisper-money/commit/a71626a350a568fc0591847aba6b0faac0d484fb))
|
||||
* **discord:** enrich Stripe event messages and dedupe deliveries ([#460](https://github.com/whisper-money/whisper-money/issues/460)) ([f9bf0ea](https://github.com/whisper-money/whisper-money/commit/f9bf0ea5fff32f26af8d05dc5645a302524b3a95))
|
||||
* **landing:** redirect signed-in users ([#429](https://github.com/whisper-money/whisper-money/issues/429)) ([4f42de7](https://github.com/whisper-money/whisper-money/commit/4f42de74a1beda4b7032a6e2fe6d9d1eec4edd4c))
|
||||
* **leads:** add user lead re-invite campaign ([#432](https://github.com/whisper-money/whisper-money/issues/432)) ([7b03d7c](https://github.com/whisper-money/whisper-money/commit/7b03d7cf230ef0cea3fe4ce6ea2215e3348910f9))
|
||||
* **leads:** schedule invitation emails ([#434](https://github.com/whisper-money/whisper-money/issues/434)) ([d5d22b9](https://github.com/whisper-money/whisper-money/commit/d5d22b9d5724cfb7363ead047946ff587772d9ea))
|
||||
* **posthog:** route analytics through reverse proxy ([#463](https://github.com/whisper-money/whisper-money/issues/463)) ([448bb2e](https://github.com/whisper-money/whisper-money/commit/448bb2e64af8ebb8b2a28700d793b97fba75bf6d))
|
||||
* **transactions:** add counterparty fields ([#440](https://github.com/whisper-money/whisper-money/issues/440)) ([10da06e](https://github.com/whisper-money/whisper-money/commit/10da06ed849c019fcd041d5889d5864a6c26844f))
|
||||
|
||||
## [0.2.3](https://github.com/whisper-money/whisper-money/compare/v0.2.2...v0.2.3) (2026-05-25)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* allow managing canceled connections ([#417](https://github.com/whisper-money/whisper-money/issues/417)) ([eaba315](https://github.com/whisper-money/whisper-money/commit/eaba3151967a942044cffbd33c0dcee8d00cb457))
|
||||
* build arm64 images on native runners ([#419](https://github.com/whisper-money/whisper-money/issues/419)) ([364c72d](https://github.com/whisper-money/whisper-money/commit/364c72db72812e337a08813019bfd8d6c84ce1a3))
|
||||
* preview categorizer rule application ([#421](https://github.com/whisper-money/whisper-money/issues/421)) ([faf046b](https://github.com/whisper-money/whisper-money/commit/faf046b3c4213417190504cd0b7491012e7fcffd))
|
||||
* publish arm64 docker images asynchronously ([#418](https://github.com/whisper-money/whisper-money/issues/418)) ([4411601](https://github.com/whisper-money/whisper-money/commit/44116010dee35eb93b6826e4c955d0c662aa5927)), closes [#412](https://github.com/whisper-money/whisper-money/issues/412)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* keep past due subscriptions active ([#416](https://github.com/whisper-money/whisper-money/issues/416)) ([88faa5b](https://github.com/whisper-money/whisper-money/commit/88faa5beb60c8b9948ff11284609b21fbfebee30))
|
||||
* **mail:** use AWS SES for email delivery ([#422](https://github.com/whisper-money/whisper-money/issues/422)) ([d8bb78e](https://github.com/whisper-money/whisper-money/commit/d8bb78e5e096938292d0a971637d077d76cf61a8))
|
||||
|
||||
## [0.2.2](https://github.com/whisper-money/whisper-money/compare/v0.2.0...v0.2.2) (2026-05-22)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **banking:** dedup EnableBanking transactions by deterministic fingerprint ([#390](https://github.com/whisper-money/whisper-money/issues/390)) ([d9204bb](https://github.com/whisper-money/whisper-money/commit/d9204bb3d611c382493e45691d9ec963a73bf454))
|
||||
* **banking:** treat Indexa Capital performance 404 as empty data ([#386](https://github.com/whisper-money/whisper-money/issues/386)) ([06e7eed](https://github.com/whisper-money/whisper-money/commit/06e7eed4e20796accf0555659baeffb481e925bd))
|
||||
* **budget:** show today marker ([#411](https://github.com/whisper-money/whisper-money/issues/411)) ([933dfde](https://github.com/whisper-money/whisper-money/commit/933dfdeb1b32ffba04850b973d9c3d4681262053))
|
||||
* **connections:** show expired reconnect ([#407](https://github.com/whisper-money/whisper-money/issues/407)) ([d2e00f1](https://github.com/whisper-money/whisper-money/commit/d2e00f14e5302ccd40f620733479fc7a7b410699))
|
||||
* keep lead invite command aliases ([#406](https://github.com/whisper-money/whisper-money/issues/406)) ([11f989d](https://github.com/whisper-money/whisper-money/commit/11f989d03af290f9ee32bc6e46fad327dd2c1e03))
|
||||
* **notifications:** skip mail dispatch when recipient email is invalid ([#387](https://github.com/whisper-money/whisper-money/issues/387)) ([d140b4f](https://github.com/whisper-money/whisper-money/commit/d140b4fd4cd5402b464c203d619c231db9672ef7))
|
||||
* **sentry:** ignore postMessage clone noise ([#373](https://github.com/whisper-money/whisper-money/issues/373)) ([6335287](https://github.com/whisper-money/whisper-money/commit/63352877654bc880e09e36d0a3efada527961d82))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Coinbase banking integration ([#388](https://github.com/whisper-money/whisper-money/issues/388)) ([e71a743](https://github.com/whisper-money/whisper-money/commit/e71a743a0a6e3a1a6bde5c1bc29df94555f74bd1))
|
||||
* **import:** calculate balances from transactions ([#403](https://github.com/whisper-money/whisper-money/issues/403)) ([66ff427](https://github.com/whisper-money/whisper-money/commit/66ff427481dfe0e00cf632e3f0f1caef33238636))
|
||||
|
||||
## [0.2.1](https://github.com/whisper-money/whisper-money/compare/v0.2.0...v0.2.1) (2026-05-12)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Add yearly budget period ([#384](https://github.com/whisper-money/whisper-money/issues/384)) ([f8f3b06](https://github.com/whisper-money/whisper-money/commit/f8f3b06))
|
||||
* Add labels to automation rules ([#379](https://github.com/whisper-money/whisper-money/issues/379)) ([5b8e7e8](https://github.com/whisper-money/whisper-money/commit/5b8e7e8))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Fix exchange rate cache race (PHP-LARAVEL-1V) ([#383](https://github.com/whisper-money/whisper-money/issues/383)) ([c3dcbb4](https://github.com/whisper-money/whisper-money/commit/c3dcbb4))
|
||||
* Fix cashflow null category rows ([#382](https://github.com/whisper-money/whisper-money/issues/382)) ([30cc4da](https://github.com/whisper-money/whisper-money/commit/30cc4da))
|
||||
* Fix browser translation crash (PHP-LARAVEL-1S) ([#381](https://github.com/whisper-money/whisper-money/issues/381)) ([e635fda](https://github.com/whisper-money/whisper-money/commit/e635fda))
|
||||
* Fix cashflow multi-currency totals ([#380](https://github.com/whisper-money/whisper-money/issues/380)) ([4e03996](https://github.com/whisper-money/whisper-money/commit/4e03996))
|
||||
* Fix service worker registration rejection ([#376](https://github.com/whisper-money/whisper-money/issues/376)) ([3526e5f](https://github.com/whisper-money/whisper-money/commit/3526e5f))
|
||||
* Recover from stale Vite chunks ([#374](https://github.com/whisper-money/whisper-money/issues/374)) ([69610c5](https://github.com/whisper-money/whisper-money/commit/69610c5))
|
||||
* **sentry:** ignore postMessage clone noise ([#373](https://github.com/whisper-money/whisper-money/issues/373)) ([6335287](https://github.com/whisper-money/whisper-money/commit/6335287))
|
||||
* Fix Sentry transaction and dashboard crashes ([#372](https://github.com/whisper-money/whisper-money/issues/372)) ([718cfa9](https://github.com/whisper-money/whisper-money/commit/718cfa9))
|
||||
* Fix Sentry release commit detection in image build ([#371](https://github.com/whisper-money/whisper-money/issues/371)) ([f4ab4a1](https://github.com/whisper-money/whisper-money/commit/f4ab4a1))
|
||||
* Prevent cached cashflow analytics responses ([#368](https://github.com/whisper-money/whisper-money/issues/368)) ([97df059](https://github.com/whisper-money/whisper-money/commit/97df059))
|
||||
* Fix duplicate category name validation ([#364](https://github.com/whisper-money/whisper-money/issues/364)) ([e3c2d2f](https://github.com/whisper-money/whisper-money/commit/e3c2d2f))
|
||||
|
||||
|
||||
### Chores
|
||||
|
||||
* Add sentry issue slash command ([#375](https://github.com/whisper-money/whisper-money/issues/375)) ([c929c1f](https://github.com/whisper-money/whisper-money/commit/c929c1f))
|
||||
* Update worktree script ([#366](https://github.com/whisper-money/whisper-money/issues/366)) ([360a38a](https://github.com/whisper-money/whisper-money/commit/360a38a))
|
||||
* Speed up PR CI browser path ([#365](https://github.com/whisper-money/whisper-money/issues/365)) ([e36d6f3](https://github.com/whisper-money/whisper-money/commit/e36d6f3))
|
||||
|
||||
# [0.2.0](https://github.com/whisper-money/whisper-money/compare/v0.1.20...v0.2.0) (2026-05-07)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **banking:** clamp linkedDateFrom to today on EnableBanking sync ([#343](https://github.com/whisper-money/whisper-money/issues/343)) ([f6c2057](https://github.com/whisper-money/whisper-money/commit/f6c20576b5dd6a98cb69c860825459fe010e2164))
|
||||
* **budgets:** remove Custom period type to fix duplicate-key crash ([#355](https://github.com/whisper-money/whisper-money/issues/355)) ([22043ce](https://github.com/whisper-money/whisper-money/commit/22043ced29e80486bcc3bb025952fda0f0b1f537))
|
||||
* **dashboard:** avoid month overflow in real estate projection ([#340](https://github.com/whisper-money/whisper-money/issues/340)) ([8f42496](https://github.com/whisper-money/whisper-money/commit/8f42496a5f6cd655828df7c49f358ad61d7e8002))
|
||||
* include production Dockerfile in deploy filter ([#350](https://github.com/whisper-money/whisper-money/issues/350)) ([21b5692](https://github.com/whisper-money/whisper-money/commit/21b5692174f2cf23d44a93e26f7b39d21edfe383))
|
||||
* **onboarding:** guard window access in SSR ([#351](https://github.com/whisper-money/whisper-money/issues/351)) ([b1709b7](https://github.com/whisper-money/whisper-money/commit/b1709b714e5e5d591351db51f7d2b31fb201fe74))
|
||||
* **real-estate:** compound annual revaluation monthly ([#337](https://github.com/whisper-money/whisper-money/issues/337)) ([13f741a](https://github.com/whisper-money/whisper-money/commit/13f741aaed38681571c5950da844f44309306858))
|
||||
* unblock onboarding after sync failure ([#346](https://github.com/whisper-money/whisper-money/issues/346)) ([70f3897](https://github.com/whisper-money/whisper-money/commit/70f3897b5534940c4be1dfdce3b4ce8978a882b9))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **accounts:** show projection on real estate chart ([#338](https://github.com/whisper-money/whisper-money/issues/338)) ([0f2300b](https://github.com/whisper-money/whisper-money/commit/0f2300bf3e420576893758117ed5583b39f656d7))
|
||||
* **banking:** back off scheduler when EnableBanking returns 429 ([#352](https://github.com/whisper-money/whisper-money/issues/352)) ([f800847](https://github.com/whisper-money/whisper-money/commit/f80084759133a5e00fc997602266575d3806dfaa))
|
||||
* **leads:** cohort-based launch invitations with per-user Stripe coupons ([#333](https://github.com/whisper-money/whisper-money/issues/333)) ([ab3d6e9](https://github.com/whisper-money/whisper-money/commit/ab3d6e9fcaeccf3b57027c26904460e788c8df3e))
|
||||
|
||||
|
||||
### Performance Improvements
|
||||
|
||||
* **resend:** default sync-leads to last 24h window ([#354](https://github.com/whisper-money/whisper-money/issues/354)) ([e387c03](https://github.com/whisper-money/whisper-money/commit/e387c038ca6e5e0ea3f757e28c52125ea20ba198))
|
||||
|
||||
## [0.1.20](https://github.com/whisper-money/whisper-money/compare/v0.1.19...v0.1.20) (2026-04-24)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **accounts:** use chart color scheme for real estate sparkline and balance charts ([#247](https://github.com/whisper-money/whisper-money/issues/247)) ([8b71115](https://github.com/whisper-money/whisper-money/commit/8b71115afc0f46ec1867e7030bffc87cad481a10))
|
||||
* add missing port to frontend Bugsink DSN ([#260](https://github.com/whisper-money/whisper-money/issues/260)) ([6ce5b12](https://github.com/whisper-money/whisper-money/commit/6ce5b123ce9b58ae7ec660d8cbcd005fb1748e35))
|
||||
* align onboarding account types with current asset support ([#273](https://github.com/whisper-money/whisper-money/issues/273)) ([80274e0](https://github.com/whisper-money/whisper-money/commit/80274e03a8e697509ddbd0ec3e7a4e9d5d752d10))
|
||||
* **auth:** allow forced registration ([#307](https://github.com/whisper-money/whisper-money/issues/307)) ([75736f3](https://github.com/whisper-money/whisper-money/commit/75736f3e59966e6821f436d4aac7f45e4111e5da))
|
||||
* avoid iOS PWA status bar overlap ([#281](https://github.com/whisper-money/whisper-money/issues/281)) ([80b6668](https://github.com/whisper-money/whisper-money/commit/80b666836c9ad106c526eb45c82046af953c0342))
|
||||
* **banking:** retry failed sync connections and log every sync attempt ([#251](https://github.com/whisper-money/whisper-money/issues/251)) ([f3b5929](https://github.com/whisper-money/whisper-money/commit/f3b5929ecc2ca4d093e645ff996fc47b63440e17))
|
||||
* batch Pennant feature flag queries to avoid N+1 selects ([#244](https://github.com/whisper-money/whisper-money/issues/244)) ([8ac6ed4](https://github.com/whisper-money/whisper-money/commit/8ac6ed4d83e14eaab9fe8215247e091fab8258c3)), closes [#241](https://github.com/whisper-money/whisper-money/issues/241)
|
||||
* **budgets:** make budget assignment idempotent ([#303](https://github.com/whisper-money/whisper-money/issues/303)) ([b1ceda6](https://github.com/whisper-money/whisper-money/commit/b1ceda61f93d1bb385060b7ffee35fb56fd41962))
|
||||
* **budgets:** retry assignment deadlocks ([#304](https://github.com/whisper-money/whisper-money/issues/304)) ([45e311e](https://github.com/whisper-money/whisper-money/commit/45e311e17baaa510a4309724937c5b18ded42631))
|
||||
* **cashflow:** exclude transfer categories from sankey ([#235](https://github.com/whisper-money/whisper-money/issues/235)) ([debb47f](https://github.com/whisper-money/whisper-money/commit/debb47f6af2808669a319a696d9a81036ca7b961))
|
||||
* **cashflow:** net transfer categories in sankey ([#257](https://github.com/whisper-money/whisper-money/issues/257)) ([83f7e83](https://github.com/whisper-money/whisper-money/commit/83f7e83a134db2fe98f4b3ba75f173b7e0f44e44))
|
||||
* **cashflow:** read period from server props instead of window ([#302](https://github.com/whisper-money/whisper-money/issues/302)) ([22952c4](https://github.com/whisper-money/whisper-money/commit/22952c4e75cfbe933b42c91da826ff0e33e472e3))
|
||||
* **chart:** hide tooltip on scroll with opacity fade ([#320](https://github.com/whisper-money/whisper-money/issues/320)) ([38e1976](https://github.com/whisper-money/whisper-money/commit/38e1976270b3afafac93d02a5586c508762e25af))
|
||||
* **chart:** tooltip escapes overflow, truncates long labels ([#317](https://github.com/whisper-money/whisper-money/issues/317)) ([e4d2ade](https://github.com/whisper-money/whisper-money/commit/e4d2ade92f4c532fa040a9b98e2fcee2ba5cc3b9))
|
||||
* **ci:** order sentry deploy after build ([#309](https://github.com/whisper-money/whisper-money/issues/309)) ([bfe1af3](https://github.com/whisper-money/whisper-money/commit/bfe1af3c839e3370d5b6132efdaaad5a6b9983a3))
|
||||
* **ci:** skip outdated production deploys ([b36197e](https://github.com/whisper-money/whisper-money/commit/b36197e76bca7b73cc50f4f53775974326cae264))
|
||||
* clarify account creation modal copy ([#274](https://github.com/whisper-money/whisper-money/issues/274)) ([dafc58f](https://github.com/whisper-money/whisper-money/commit/dafc58f49f0a832a45bbd3f02fd39340e575a4d7))
|
||||
* clarify mobile settings navigation ([#272](https://github.com/whisper-money/whisper-money/issues/272)) ([62ab1b3](https://github.com/whisper-money/whisper-money/commit/62ab1b38db8fc03e4e3172cc31676442b850deaf))
|
||||
* **dashboard:** dismiss account card tooltip when tapping outside ([#318](https://github.com/whisper-money/whisper-money/issues/318)) ([753002f](https://github.com/whisper-money/whisper-money/commit/753002f930f4abe8c8025bac7f28609d1694152c))
|
||||
* **dashboard:** treat loans as debt in net worth ([#238](https://github.com/whisper-money/whisper-money/issues/238)) ([f140b5d](https://github.com/whisper-money/whisper-money/commit/f140b5df7f2188dde8d278eca47a4e8eaa431f86))
|
||||
* default account charts to user currency ([#271](https://github.com/whisper-money/whisper-money/issues/271)) ([38cf672](https://github.com/whisper-money/whisper-money/commit/38cf672c8e9ba24e8f8f956e2b19a2c05c98064a))
|
||||
* default to standard onboarding option ([#276](https://github.com/whisper-money/whisper-money/issues/276)) ([d91d9d3](https://github.com/whisper-money/whisper-money/commit/d91d9d3b3eb2ac7c6c9deed2ef2454835daf5d5a))
|
||||
* **demo-reset:** use renamed 'ING Direct' bank ([#301](https://github.com/whisper-money/whisper-money/issues/301)) ([cfa54a2](https://github.com/whisper-money/whisper-money/commit/cfa54a2d9dc9b8031d18528b51bde933ed501729))
|
||||
* **docker:** ensure www-data owns storage after artisan commands ([#329](https://github.com/whisper-money/whisper-money/issues/329)) ([0eca002](https://github.com/whisper-money/whisper-money/commit/0eca00285699ca67dccd6c7ab8ec5af853a951fc))
|
||||
* expose pi mcp extension as mcps.ts ([#315](https://github.com/whisper-money/whisper-money/issues/315)) ([c7cfa10](https://github.com/whisper-money/whisper-money/commit/c7cfa1011764be700687da5e499de1fde3445e65))
|
||||
* **i18n:** add missing Spanish translations for mortgage UI strings ([0a535fb](https://github.com/whisper-money/whisper-money/commit/0a535fbf4729afc4bf0c791faddbfc71397c01ef))
|
||||
* **i18n:** translate Unknown Income/Expense and other missing ES strings ([#331](https://github.com/whisper-money/whisper-money/issues/331)) ([79075db](https://github.com/whisper-money/whisper-money/commit/79075dbcdf2003373483afd396d2b4cb4b415f6a))
|
||||
* keep iOS content below the notch ([#280](https://github.com/whisper-money/whisper-money/issues/280)) ([b505d68](https://github.com/whisper-money/whisper-money/commit/b505d68ef0ac4d52ee94a85b4e6b113c9d8d35c9))
|
||||
* keep iOS popovers below the notch ([#282](https://github.com/whisper-money/whisper-money/issues/282)) ([ea9956f](https://github.com/whisper-money/whisper-money/commit/ea9956f21da3f7498bf947f539c6b31fa844fe96))
|
||||
* limit bank sync emails to one per day ([#290](https://github.com/whisper-money/whisper-money/issues/290)) ([552aa59](https://github.com/whisper-money/whisper-money/commit/552aa59aaf5e476c81d81483ca9118f872730d2e))
|
||||
* **loans:** project monthly balances from actual entries instead of original params ([#259](https://github.com/whisper-money/whisper-money/issues/259)) ([7e95828](https://github.com/whisper-money/whisper-money/commit/7e958284e3944a9bf3dfae08524f81afbca4a7da))
|
||||
* make transaction sync email use default sender ([#265](https://github.com/whisper-money/whisper-money/issues/265)) ([7be0fe0](https://github.com/whisper-money/whisper-money/commit/7be0fe012041283df651c1fbce7b3f69102a500f))
|
||||
* **open-banking:** respect local email hours ([#306](https://github.com/whisper-money/whisper-money/issues/306)) ([fbffdd3](https://github.com/whisper-money/whisper-money/commit/fbffdd3f3c16ae075bb2d779e22d1b4e82a792e9))
|
||||
* **open-banking:** skip silent sync emails ([#295](https://github.com/whisper-money/whisper-money/issues/295)) ([473ac03](https://github.com/whisper-money/whisper-money/commit/473ac03088b3ad6e09c32344e0b4ca5f1db489ea))
|
||||
* **open-banking:** sort bank sync email data ([#292](https://github.com/whisper-money/whisper-money/issues/292)) ([c90e816](https://github.com/whisper-money/whisper-money/commit/c90e8166bfc94f1af7aab2197edd87d68eb9e1b9))
|
||||
* **open-banking:** suppress first sync email ([#310](https://github.com/whisper-money/whisper-money/issues/310)) ([16675f6](https://github.com/whisper-money/whisper-money/commit/16675f6518ec2e652b711c483d28d4b22792abd6))
|
||||
* preserve cents in chart amounts ([#270](https://github.com/whisper-money/whisper-money/issues/270)) ([0735ee6](https://github.com/whisper-money/whisper-money/commit/0735ee6d697bd8d46044a223bc1061b8742f035e))
|
||||
* **pricing:** update final release prices ([#288](https://github.com/whisper-money/whisper-money/issues/288)) ([319ca75](https://github.com/whisper-money/whisper-money/commit/319ca758e1e9869445512e9311b3d26a4197291f))
|
||||
* prioritize exact bank search matches ([#267](https://github.com/whisper-money/whisper-money/issues/267)) ([1e20361](https://github.com/whisper-money/whisper-money/commit/1e2036110fe05d564069bcc57ffadec4fb8a8147))
|
||||
* reorder signed names in mail templates ([#266](https://github.com/whisper-money/whisper-money/issues/266)) ([fec9373](https://github.com/whisper-money/whisper-money/commit/fec93734c0dd2d618c00e99247506d314b9b10e7))
|
||||
* route new PWA guests to signup ([#313](https://github.com/whisper-money/whisper-money/issues/313)) ([905edeb](https://github.com/whisper-money/whisper-money/commit/905edeb4a249cf71a9fccd7815f14fbadc20c884))
|
||||
* **schedule:** remove stale horizon snapshot ([#293](https://github.com/whisper-money/whisper-money/issues/293)) ([b438a1c](https://github.com/whisper-money/whisper-money/commit/b438a1c73bfb388c784764dbe08b2274c40126ed))
|
||||
* split drip and default email senders ([#263](https://github.com/whisper-money/whisper-money/issues/263)) ([ce5692c](https://github.com/whisper-money/whisper-money/commit/ce5692cb3036ec47c4f82ae57aaadfd58e6c14a4))
|
||||
* **user:** persist detected timezones ([#296](https://github.com/whisper-money/whisper-money/issues/296)) ([fde5405](https://github.com/whisper-money/whisper-money/commit/fde5405777250f71cdcc1b45fae73fdb64cd7496))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **accounts:** add loan amortization projections for loan accounts ([#246](https://github.com/whisper-money/whisper-money/issues/246)) ([bb65bdc](https://github.com/whisper-money/whisper-money/commit/bb65bdc16e2f0952ec7508dbce418d0155715077))
|
||||
* **accounts:** add market value and annual revaluation to real estate accounts ([#245](https://github.com/whisper-money/whisper-money/issues/245)) ([fa11dc7](https://github.com/whisper-money/whisper-money/commit/fa11dc78e0c60e310a13708edfd35926f1435a0b))
|
||||
* **accounts:** add real estate asset tracking ([#241](https://github.com/whisper-money/whisper-money/issues/241)) ([395c4ad](https://github.com/whisper-money/whisper-money/commit/395c4ad2c34b43b341a675ce5526edf2a3d03cd0))
|
||||
* **accounts:** add today marker on projected balance chart ([#321](https://github.com/whisper-money/whisper-money/issues/321)) ([4b145e2](https://github.com/whisper-money/whisper-money/commit/4b145e230b5a19a25b585260b05f2cc2c19fe066))
|
||||
* **accounts:** allow setting initial balance when creating balance-tracking accounts ([#239](https://github.com/whisper-money/whisper-money/issues/239)) ([7a05621](https://github.com/whisper-money/whisper-money/commit/7a056213cf6a29eab0b2416f69fd7dfa9ab1061d))
|
||||
* **accounts:** merge real estate accounts with linked mortgages in UI ([#248](https://github.com/whisper-money/whisper-money/issues/248)) ([6e97635](https://github.com/whisper-money/whisper-money/commit/6e976354ba2e673d5b183bacc3e9a896937ee54f))
|
||||
* **accounts:** show mortgage data and equity on real estate account page ([#243](https://github.com/whisper-money/whisper-money/issues/243)) ([9732432](https://github.com/whisper-money/whisper-money/commit/973243277a512b40f46d48cae557d240924fe2cb))
|
||||
* add appearance shortcut to user menu ([#269](https://github.com/whisper-money/whisper-money/issues/269)) ([3acb277](https://github.com/whisper-money/whisper-money/commit/3acb277fb5838c8538b989aa0ba7a8e209ac917f))
|
||||
* **billing:** apply Stripe tax rates to subscriptions ([#325](https://github.com/whisper-money/whisper-money/issues/325)) ([74cbdd4](https://github.com/whisper-money/whisper-money/commit/74cbdd42efea0e8884639b049a5de7138489fad2))
|
||||
* **cashflow:** show tracked transfers in Sankey diagram ([#237](https://github.com/whisper-money/whisper-money/issues/237)) ([6dda5f5](https://github.com/whisper-money/whisper-money/commit/6dda5f56ade8d669b9c0843d4980c2d76c9dc614)), closes [hi#level](https://github.com/hi/issues/level)
|
||||
* **cashflow:** track transfer categories in trends ([#236](https://github.com/whisper-money/whisper-money/issues/236)) ([272dac1](https://github.com/whisper-money/whisper-money/commit/272dac14b82b6863af6eddf88dc54e0fb408c9f1))
|
||||
* **dashboard:** merge real estate accounts with linked mortgages on dashboard ([752176e](https://github.com/whisper-money/whisper-money/commit/752176e80d67241ab4566d3ced0a7abe8a987b69))
|
||||
* **landing:** add signed auth links ([#312](https://github.com/whisper-money/whisper-money/issues/312)) ([240fcf1](https://github.com/whisper-money/whisper-money/commit/240fcf17030c605ed5daaa3fffa77018e20968c5))
|
||||
* link loans to existing properties ([#275](https://github.com/whisper-money/whisper-money/issues/275)) ([a7c1bd3](https://github.com/whisper-money/whisper-money/commit/a7c1bd35ef058f6ef468cdd96a3b9e3a9be89de1))
|
||||
* **loans:** backfill historical balances on loan creation ([#322](https://github.com/whisper-money/whisper-money/issues/322)) ([5b1d059](https://github.com/whisper-money/whisper-money/commit/5b1d059e020f7aa12c3502b51b174d0615a820e1))
|
||||
* **open-banking:** remove feature flag gating ([#297](https://github.com/whisper-money/whisper-money/issues/297)) ([244344e](https://github.com/whisper-money/whisper-money/commit/244344e953033b12a948c5f9d85d7db4639bba1d))
|
||||
* **real-estate:** auto-calculate revaluation % and generate historical balances ([#253](https://github.com/whisper-money/whisper-money/issues/253)) ([094fb1b](https://github.com/whisper-money/whisper-money/commit/094fb1b7446ca57e32e00018b78ddd645eeea3a3))
|
||||
* resend verification emails to unverified leads ([#287](https://github.com/whisper-money/whisper-money/issues/287)) ([5b78509](https://github.com/whisper-money/whisper-money/commit/5b7850958882988d80080eaa456e599007b974c8))
|
||||
* selective retry of failed lead email jobs ([#286](https://github.com/whisper-money/whisper-money/issues/286)) ([f408dbe](https://github.com/whisper-money/whisper-money/commit/f408dbe4c8a8ccbd7368ac675525a85b70c9abdf))
|
||||
* **settings:** centralize currency options and split profile/account support ([#256](https://github.com/whisper-money/whisper-money/issues/256)) ([3d58237](https://github.com/whisper-money/whisper-money/commit/3d5823728a18a146e9c420e7f924014bd66bd3c8))
|
||||
* store invested_amount in user currency instead of account currency ([#262](https://github.com/whisper-money/whisper-money/issues/262)) ([c3ff4c6](https://github.com/whisper-money/whisper-money/commit/c3ff4c684a50eb1b506d59954442f2ba7a41b04d))
|
||||
* **stripe:** add promo code generator ([#311](https://github.com/whisper-money/whisper-money/issues/311)) ([69665c3](https://github.com/whisper-money/whisper-money/commit/69665c3c588ad0b4d27594d2b55fdb185553483a))
|
||||
* **subscriptions:** add configurable trial period to paid plans ([#324](https://github.com/whisper-money/whisper-money/issues/324)) ([b399aaa](https://github.com/whisper-money/whisper-money/commit/b399aaaa0dcafc27f9f9665209f9aceecf0b70e7))
|
||||
* sync user leads to resend ([#283](https://github.com/whisper-money/whisper-money/issues/283)) ([dc0695c](https://github.com/whisper-money/whisper-money/commit/dc0695c2ca55d3447b814b44bd8f13848922f92a))
|
||||
* verify waitlist leads ([#285](https://github.com/whisper-money/whisper-money/issues/285)) ([d0aab3d](https://github.com/whisper-money/whisper-money/commit/d0aab3d11bad80fefb35fa01055347ce1413d18b))
|
||||
|
||||
## [0.1.19](https://github.com/whisper-money/whisper-money/compare/v0.1.18...v0.1.19) (2026-03-17)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **banking:** treat 429 rate limit as transient, skip error status on sync ([#224](https://github.com/whisper-money/whisper-money/issues/224)) ([5b9ae2a](https://github.com/whisper-money/whisper-money/commit/5b9ae2a5259ecf1e55e4074295c52dcc0429ef71))
|
||||
* **cashflow:** only count sign-matching transactions in Sankey category breakdown ([#232](https://github.com/whisper-money/whisper-money/issues/232)) ([9e2a9ca](https://github.com/whisper-money/whisper-money/commit/9e2a9cadfe0210e0f2a45da8dbcaab1552dc0844))
|
||||
* **ci:** allow deploy retry loop to survive curl timeout ([#233](https://github.com/whisper-money/whisper-money/issues/233)) ([cd40bc7](https://github.com/whisper-money/whisper-money/commit/cd40bc75d9b60acede4fc519f3f8f66ad8f560c3))
|
||||
* **haptics:** use a local WebHaptics wrapper ([#225](https://github.com/whisper-money/whisper-money/issues/225)) ([f600524](https://github.com/whisper-money/whisper-money/commit/f600524c2b834b9322fda1ca7a6881b43c5d5194))
|
||||
* prevent account label combobox crash ([#230](https://github.com/whisper-money/whisper-money/issues/230)) ([a60fd6f](https://github.com/whisper-money/whisper-money/commit/a60fd6f452b58d8ba9e4033dffc27a4f0c0fff15))
|
||||
* **settings:** restore budgets settings redirect ([#228](https://github.com/whisper-money/whisper-money/issues/228)) ([e5fcaee](https://github.com/whisper-money/whisper-money/commit/e5fcaee8f8a0c9badf0450fb209ff7cd7e4c0d2e))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **cashflow:** make income/expense category rows clickable to transactions ([#234](https://github.com/whisper-money/whisper-money/issues/234)) ([ec24565](https://github.com/whisper-money/whisper-money/commit/ec245655b8f5541a6bafec92edede97bf75573aa))
|
||||
|
||||
## [0.1.18](https://github.com/whisper-money/whisper-money/compare/v0.1.17...v0.1.18) (2026-03-12)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **banking:** correct backfill-ibans endpoint and handle expired sessions gracefully ([#222](https://github.com/whisper-money/whisper-money/issues/222)) ([08dfb07](https://github.com/whisper-money/whisper-money/commit/08dfb07a90ac4e29b10d5412853d6d11579f3d52))
|
||||
* **banking:** correct backfill-ibans endpoint, handle expired sessions, and update labels ([#223](https://github.com/whisper-money/whisper-money/issues/223)) ([b92c4ed](https://github.com/whisper-money/whisper-money/commit/b92c4ed149974e1cb1b48af215dbbd6d10f419e4))
|
||||
* **banking:** update external_account_id on reconnect and store IBAN ([#220](https://github.com/whisper-money/whisper-money/issues/220)) ([4408f71](https://github.com/whisper-money/whisper-money/commit/4408f719b49cb16ea306ab945ce79e507d948ec0))
|
||||
* **banks:set-logo:** add JPEG support test coverage and prompt for missing arguments ([#214](https://github.com/whisper-money/whisper-money/issues/214)) ([cbe28ff](https://github.com/whisper-money/whisper-money/commit/cbe28ff708a2f94df4f590d913f3f370514be9e9))
|
||||
* **cashflow:** hide amounts on sankey chart when privacy mode is enabled ([8eb7a0c](https://github.com/whisper-money/whisper-money/commit/8eb7a0cfd79f7b4ed931b696dde5d9ba42039a2e))
|
||||
* **transactions:** cap description column width to prevent horizontal overflow ([#216](https://github.com/whisper-money/whisper-money/issues/216)) ([28c8df3](https://github.com/whisper-money/whisper-money/commit/28c8df34d5fc8242cc91df3c119caa5832f9a394))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **banking:** add banking:backfill-ibans command to populate missing IBANs ([#221](https://github.com/whisper-money/whisper-money/issues/221)) ([07ab9d5](https://github.com/whisper-money/whisper-money/commit/07ab9d5b963de4f7083d86f470923a144b5652ac))
|
||||
* **connections:** add EnableBanking reconnect flow ([#218](https://github.com/whisper-money/whisper-money/issues/218)) ([1f5e6ac](https://github.com/whisper-money/whisper-money/commit/1f5e6ac450f0240020db92c369c30d291e01c512))
|
||||
* **connections:** filter already-connected institutions from connect bank dialog ([#217](https://github.com/whisper-money/whisper-money/issues/217)) ([1058904](https://github.com/whisper-money/whisper-money/commit/1058904b14ac82df7dd1a1e8848b08b1ca64a143))
|
||||
* **dashboard:** sort net worth chart accounts by average balance ([#219](https://github.com/whisper-money/whisper-money/issues/219)) ([b1cf133](https://github.com/whisper-money/whisper-money/commit/b1cf133b5ae059a4aa830195d412a77796f66530))
|
||||
* **emails:** co-founder language, welcome rewrite, and Spanish translations ([#208](https://github.com/whisper-money/whisper-money/issues/208)) ([8ca4c8d](https://github.com/whisper-money/whisper-money/commit/8ca4c8d6c685fe214941dea4374f8af9dc30e7ac))
|
||||
* **landing:** billing period toggle with yearly discount on pricing section ([#215](https://github.com/whisper-money/whisper-money/issues/215)) ([e9572e4](https://github.com/whisper-money/whisper-money/commit/e9572e4031416a5daa982a4f87e9615157ccd29d))
|
||||
* **landing:** open-banking feature section with conditional grid layout ([#209](https://github.com/whisper-money/whisper-money/issues/209)) ([93369d8](https://github.com/whisper-money/whisper-money/commit/93369d8b6fb378cd16b086a1c65fd31dbd519350))
|
||||
* **pricing:** update landing page pricing table ([#207](https://github.com/whisper-money/whisper-money/issues/207)) ([21b03c7](https://github.com/whisper-money/whisper-money/commit/21b03c7c36a9017cc899a67cfcf3b01a54be5920))
|
||||
|
||||
## [0.1.17](https://github.com/whisper-money/whisper-money/compare/v0.1.16...v0.1.17) (2026-03-05)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **amount-display:** eliminate float round-trip causing missing thousands separator ([#191](https://github.com/whisper-money/whisper-money/issues/191)) ([956b661](https://github.com/whisper-money/whisper-money/commit/956b6614486b48c43f4171ec0e4336409490ff34))
|
||||
* **billing:** create Stripe customer before redirecting to billing portal ([#206](https://github.com/whisper-money/whisper-money/issues/206)) ([e8bc5fd](https://github.com/whisper-money/whisper-money/commit/e8bc5fd7866afab83dc0b807fdda8f6b3a0b1cc8))
|
||||
* **browser-test:** reload transactions in syncing step and fix Skip button selector ([#203](https://github.com/whisper-money/whisper-money/issues/203)) ([3f6c676](https://github.com/whisper-money/whisper-money/commit/3f6c67631be95310cfee77bb2bed52d26ba74896)), closes [#201](https://github.com/whisper-money/whisper-money/issues/201)
|
||||
* **haptics:** restore haptic feedback to MobileBackButton ([#198](https://github.com/whisper-money/whisper-money/issues/198)) ([fdc9d14](https://github.com/whisper-money/whisper-money/commit/fdc9d14c47c5e4d2eda9264592b1d7387dee6330))
|
||||
* **i18n:** fix unlocalised string in create budget form ([#187](https://github.com/whisper-money/whisper-money/issues/187)) ([40a7942](https://github.com/whisper-money/whisper-money/commit/40a7942b85b0c145e21a1856ce40f86e89dc427d))
|
||||
* **i18n:** force thousands separator for 4-digit amounts in es-ES locale ([#193](https://github.com/whisper-money/whisper-money/issues/193)) ([be2e205](https://github.com/whisper-money/whisper-money/commit/be2e205965eb2afbee4c7457c2f8a84d2356177f))
|
||||
* **migration:** make add_waitlist_columns migration idempotent ([#200](https://github.com/whisper-money/whisper-money/issues/200)) ([cf9071c](https://github.com/whisper-money/whisper-money/commit/cf9071c11b237579b4f44de69dd688a3fcdd94b6))
|
||||
* **onboarding:** gate connect bank option behind open-banking feature flag ([#197](https://github.com/whisper-money/whisper-money/issues/197)) ([09d81ac](https://github.com/whisper-money/whisper-money/commit/09d81ac7e7f2ebee953a85894d44a6848284d400))
|
||||
* **static-analysis:** clear phpstan-baseline by fixing all suppressed errors ([#183](https://github.com/whisper-money/whisper-money/issues/183)) ([3e087bd](https://github.com/whisper-money/whisper-money/commit/3e087bdcd77ec638ee9d9dbb0d616b0ef78ff554))
|
||||
* **testcontainers:** stop and remove MySQL container on signal and shutdown ([#202](https://github.com/whisper-money/whisper-money/issues/202)) ([011ba13](https://github.com/whisper-money/whisper-money/commit/011ba131142fb4e587ae5609d7ecab15c2b88796))
|
||||
* **transactions:** move clear button inline with filters row on all screen sizes ([#192](https://github.com/whisper-money/whisper-money/issues/192)) ([b455ad7](https://github.com/whisper-money/whisper-money/commit/b455ad71ddc9b100107fcf67b7b78f907f698de5))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* (Onboarding) add categorization intro screen with benefit cards ([#201](https://github.com/whisper-money/whisper-money/issues/201)) ([a8dfac1](https://github.com/whisper-money/whisper-money/commit/a8dfac14226e90eac2a396236cd433d5d38501fb))
|
||||
* **budgets:** make budget title clickable with muted hover effect ([#186](https://github.com/whisper-money/whisper-money/issues/186)) ([970e858](https://github.com/whisper-money/whisper-money/commit/970e85814e108b995711926e4c80e5580fa2736d))
|
||||
* **dashboard:** make top spending categories clickable with transaction filter link ([#189](https://github.com/whisper-money/whisper-money/issues/189)) ([832fc61](https://github.com/whisper-money/whisper-money/commit/832fc6177e7f8ff337b79170e76e4cd53ea99e95))
|
||||
* **haptics:** add haptic feedback to nav items and back buttons ([#196](https://github.com/whisper-money/whisper-money/issues/196)) ([3d74267](https://github.com/whisper-money/whisper-money/commit/3d742677b59f36fc6266adbe0904b7230387e6eb))
|
||||
* **mobile:** add scroll-aware back button on detail pages ([#194](https://github.com/whisper-money/whisper-money/issues/194)) ([7fec851](https://github.com/whisper-money/whisper-money/commit/7fec8514e47d38f7d0ec253164d241703c0281d0))
|
||||
* **onboarding:** inline connected account flow with auto-account creation and step deep-linking ([#184](https://github.com/whisper-money/whisper-money/issues/184)) ([993c91a](https://github.com/whisper-money/whisper-money/commit/993c91a6b6f1f65ee200c96764ecb8c0ad2fbdc6))
|
||||
* **pricing:** dynamic Stripe pricing with locale-aware formatting ([#204](https://github.com/whisper-money/whisper-money/issues/204)) ([ac1476e](https://github.com/whisper-money/whisper-money/commit/ac1476eeffee91a67bd91443c5a10b4c46576275))
|
||||
* **privacy:** enable privacy mode for all users and extend amount masking ([#182](https://github.com/whisper-money/whisper-money/issues/182)) ([152b186](https://github.com/whisper-money/whisper-money/commit/152b186c103458e8d7833034027750a663555906))
|
||||
* **subscription:** allow free plan for open banking users without connected banks ([#188](https://github.com/whisper-money/whisper-money/issues/188)) ([d8f6a68](https://github.com/whisper-money/whisper-money/commit/d8f6a680ceb3a11ed215e4bdb969e0a18fa74833))
|
||||
* **waitlist:** waiting list with referral system ([#199](https://github.com/whisper-money/whisper-money/issues/199)) ([4d0d203](https://github.com/whisper-money/whisper-money/commit/4d0d203fd373df5608d6a15dd3da0980c5c49502)), closes [#500](https://github.com/whisper-money/whisper-money/issues/500)
|
||||
|
||||
## [0.1.16](https://github.com/whisper-money/whisper-money/compare/v0.1.14...v0.1.16) (2026-03-01)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **i18n:** localise missing strings in budget dialogs to Spanish ([#177](https://github.com/whisper-money/whisper-money/issues/177)) ([7260525](https://github.com/whisper-money/whisper-money/commit/7260525890a9ca94bbecdf7e38c6ce81e5f900ee))
|
||||
* **i18n:** localize billing settings page into Spanish ([#176](https://github.com/whisper-money/whisper-money/issues/176)) ([7a8eda9](https://github.com/whisper-money/whisper-money/commit/7a8eda9d905ec3dc771671a474f566a0205aa87d))
|
||||
* **i18n:** localize mobile bottom navigation labels into Spanish ([#173](https://github.com/whisper-money/whisper-money/issues/173)) ([717bf34](https://github.com/whisper-money/whisper-money/commit/717bf34103855cdb7c39fb6fab2559f8f797782e))
|
||||
* Missing space between page sections and create button ([6c5961d](https://github.com/whisper-money/whisper-money/commit/6c5961da050b3548134f685e5b591f8dc314481e))
|
||||
* **tooling:** fix stringWidth error in release-it interactive prompt ([#179](https://github.com/whisper-money/whisper-money/issues/179)) ([866f908](https://github.com/whisper-money/whisper-money/commit/866f90838e4e9be8c3bccb1034ae339868c60a4c))
|
||||
* **transactions:** fix toolbar overflow on mobile and shorten button label ([#175](https://github.com/whisper-money/whisper-money/issues/175)) ([0388705](https://github.com/whisper-money/whisper-money/commit/0388705c1236e0398f1c8246ce6426e76f27c6ee))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **Budgets:** add period navigation and unify period selector UI ([#171](https://github.com/whisper-money/whisper-money/issues/171)) ([0493b87](https://github.com/whisper-money/whisper-money/commit/0493b87562ac0d66aa933e4b77863265b7c72e24))
|
||||
* **i18n:** add localization test and fix missing Spanish translations ([#174](https://github.com/whisper-money/whisper-money/issues/174)) ([9317238](https://github.com/whisper-money/whisper-money/commit/9317238c49269f99e6689e580f1bea0f0f28288a))
|
||||
* **nav:** add icon+label mobile nav with active pill and full-width buttons ([#178](https://github.com/whisper-money/whisper-money/issues/178)) ([efd86bc](https://github.com/whisper-money/whisper-money/commit/efd86bc8d7e3aca3b433bc3d880f8c279d790f8c))
|
||||
* **rules:** move automation rule evaluation to the backend ([#168](https://github.com/whisper-money/whisper-money/issues/168)) ([eda72d4](https://github.com/whisper-money/whisper-money/commit/eda72d4304948fb73094195fb71509d0b08c8f67))
|
||||
* **transactions:** re-add select all matching filters to bulk actions bar ([#169](https://github.com/whisper-money/whisper-money/issues/169)) ([0d9fc5a](https://github.com/whisper-money/whisper-money/commit/0d9fc5a2b9243c0d449f497c12b2978038fdf42a))
|
||||
* **ui:** add create buttons to accounts and budgets pages ([#172](https://github.com/whisper-money/whisper-money/issues/172)) ([9f5e62f](https://github.com/whisper-money/whisper-money/commit/9f5e62f736803a43467673c635758143caac7f48))
|
||||
* **ui:** add glowing effect to all card components ([#170](https://github.com/whisper-money/whisper-money/issues/170)) ([4d14e4d](https://github.com/whisper-money/whisper-money/commit/4d14e4d2f0c006245bcb473ac8a0b11930dee460))
|
||||
|
||||
## [0.1.14](https://github.com/whisper-money/whisper-money/compare/v0.1.13...v0.1.14) (2026-03-01)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **accounts:** widen bank column and truncate text on mobile ([#163](https://github.com/whisper-money/whisper-money/issues/163)) ([e01d62f](https://github.com/whisper-money/whisper-money/commit/e01d62ffd46861270581102da969c7cda12397b9))
|
||||
* **categorizer:** fetch uncategorized transactions from backend instead of IndexedDB ([#165](https://github.com/whisper-money/whisper-money/issues/165)) ([9bb835e](https://github.com/whisper-money/whisper-money/commit/9bb835e79b02182679b111a439171eb71e427010))
|
||||
* **i18n:** fix missing space after Tracking label and add account/accounts Spanish translations ([#167](https://github.com/whisper-money/whisper-money/issues/167)) ([cd0da10](https://github.com/whisper-money/whisper-money/commit/cd0da10014373af5d3f6ff3298c0fd8247adccb2))
|
||||
* prevent gain/loss sign from wrapping off the amount ([#158](https://github.com/whisper-money/whisper-money/issues/158)) ([a4d2100](https://github.com/whisper-money/whisper-money/commit/a4d2100459fde6a2f9a2becdd0807a0eae3dfd65)), closes [whisper-money/whisper-money#157](https://github.com/whisper-money/whisper-money/issues/157)
|
||||
* **ui:** app icon visible on light wallpapers + country select overflow on mobile ([#162](https://github.com/whisper-money/whisper-money/issues/162)) ([1b7b147](https://github.com/whisper-money/whisper-money/commit/1b7b147832f90894b6bb4806e0295853a458f296))
|
||||
* **ux:** improve status badge, hide balance update for connected accounts, localize delete confirm ([#159](https://github.com/whisper-money/whisper-money/issues/159)) ([79dd24b](https://github.com/whisper-money/whisper-money/commit/79dd24b23ef8ebd9594df9af8da1007e7f3f0f6e))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **automation-rules:** simplify smart rules UI, fix re-evaluation, and localize amounts ([#161](https://github.com/whisper-money/whisper-money/issues/161)) ([b1f01e4](https://github.com/whisper-money/whisper-money/commit/b1f01e4a8f3eedc9e5848cdda103bdc06c3ce571))
|
||||
* **cashflow:** promote trend chart above money flow and increase height ([#166](https://github.com/whisper-money/whisper-money/issues/166)) ([39a47ec](https://github.com/whisper-money/whisper-money/commit/39a47ec23ff52e609825cbd8ebdfa9576b0df22e)), closes [hi#value](https://github.com/hi/issues/value)
|
||||
* **categories:** add Self-Employment Income income category ([#164](https://github.com/whisper-money/whisper-money/issues/164)) ([77b225d](https://github.com/whisper-money/whisper-money/commit/77b225d74795bbb1c18e526bd38dfa3859ecac44))
|
||||
* **i18n:** localize Spanish translations and currency formatting ([#160](https://github.com/whisper-money/whisper-money/issues/160)) ([2b9fd23](https://github.com/whisper-money/whisper-money/commit/2b9fd2384a3a06f0132498b8fda3ae50624f25d9))
|
||||
|
||||
## [0.1.13](https://github.com/whisper-money/whisper-money/compare/v0.1.12...v0.1.13) (2026-02-25)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **budgets:** handle refunds correctly in budget spending calculations ([#152](https://github.com/whisper-money/whisper-money/issues/152)) ([f2a7f95](https://github.com/whisper-money/whisper-money/commit/f2a7f955e67465bb415685d9c17ecab213f7decf))
|
||||
* improve connection error message contrast in dark mode ([#155](https://github.com/whisper-money/whisper-money/issues/155)) ([e718f5d](https://github.com/whisper-money/whisper-money/commit/e718f5df5c5d996ff867081f53a64d8cc9259e78))
|
||||
* **open-banking:** use net_amounts for Indexa Capital invested amount calculation ([#156](https://github.com/whisper-money/whisper-money/issues/156)) ([ae2a8c0](https://github.com/whisper-money/whisper-money/commit/ae2a8c011831f48daa0433a415db3337ce445e86))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **open-banking:** add update credentials flow for API-key connections ([#154](https://github.com/whisper-money/whisper-money/issues/154)) ([690be20](https://github.com/whisper-money/whisper-money/commit/690be20f216c7e000032ffb8dc0d68e4046d5632))
|
||||
* Update facehash and enable blink ([2550339](https://github.com/whisper-money/whisper-money/commit/255033999d1bef5d4ae28e5ddb0ebf4f59478639))
|
||||
* use testcontainers for isolated MySQL in test runs ([#153](https://github.com/whisper-money/whisper-money/issues/153)) ([e4243c2](https://github.com/whisper-money/whisper-money/commit/e4243c2eaac5dd1fc59bb132cb51ab71712062ad))
|
||||
|
||||
## [0.1.12](https://github.com/whisper-money/whisper-money/compare/v0.1.10...v0.1.12) (2026-02-24)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Pricing table on dark scheme ([faddd59](https://github.com/whisper-money/whisper-money/commit/faddd59537903572109033cb2112eb0b6504d86a))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* enable invested amount tracking for savings accounts ([#142](https://github.com/whisper-money/whisper-money/issues/142)) ([0a9ca5b](https://github.com/whisper-money/whisper-money/commit/0a9ca5b606809e1772884887534317d7e86cfd8e))
|
||||
* investment benefits — show gains/losses on investment accounts ([#140](https://github.com/whisper-money/whisper-money/issues/140)) ([299b8a5](https://github.com/whisper-money/whisper-money/commit/299b8a56d87f8217a9d5ce5a0916361a751d5a94))
|
||||
|
||||
|
||||
### Performance Improvements
|
||||
|
||||
* **accounts:** replace client-side API calls with Inertia deferred prop ([#144](https://github.com/whisper-money/whisper-money/issues/144)) ([ce9574a](https://github.com/whisper-money/whisper-money/commit/ce9574aa147067447a870bf4d4f1347b7d81c08b))
|
||||
* **dashboard:** optimize query performance and eliminate redundant requests ([#146](https://github.com/whisper-money/whisper-money/issues/146)) ([ae81e20](https://github.com/whisper-money/whisper-money/commit/ae81e20a66285ccda6e2a7d22b7ea0f683f0ffb4))
|
||||
* make banking syncs incremental on subsequent runs ([#141](https://github.com/whisper-money/whisper-money/issues/141)) ([d48fea1](https://github.com/whisper-money/whisper-money/commit/d48fea15b2c48e4f4647d7762569774d42e3a87d))
|
||||
|
||||
## [0.1.10](https://github.com/whisper-money/whisper-money/compare/v0.1.9...v0.1.10) (2026-02-20)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Accounts name on settings/account ([202835f](https://github.com/whisper-money/whisper-money/commit/202835f76e6741ba0bf70c25b14fa1f63ec7ac94))
|
||||
* Add gap between filter/create button on mobile settings pages ([#115](https://github.com/whisper-money/whisper-money/issues/115)) ([726bce6](https://github.com/whisper-money/whisper-money/commit/726bce61ef8e1d923b49a576d3fcccad31e1adc1))
|
||||
* Automerge PR's where not triggering CI on main branch ([ab160ae](https://github.com/whisper-money/whisper-money/commit/ab160ae4890371a9100b6cd89cbce2d0a09180d2))
|
||||
* Budget period not found on last day of period ([#91](https://github.com/whisper-money/whisper-money/issues/91)) ([00b2ca7](https://github.com/whisper-money/whisper-money/commit/00b2ca7c55d947d95c9582ce2039a91376f83db9))
|
||||
* **cashflow:** prevent breakdown cards overflow on mobile ([#139](https://github.com/whisper-money/whisper-money/issues/139)) ([c03f576](https://github.com/whisper-money/whisper-money/commit/c03f5767585ce084945da00bbf3af902dce5d123))
|
||||
* **charts:** use settings popover for chart controls on mobile ([#137](https://github.com/whisper-money/whisper-money/issues/137)) ([880b276](https://github.com/whisper-money/whisper-money/commit/880b27675cd1428fd6194fa7d0a058f398817079))
|
||||
* console error ([a76826b](https://github.com/whisper-money/whisper-money/commit/a76826bd62e213537c3308bb0678abe9c40a54b3))
|
||||
* Console log errors with Charts ([48b4b7b](https://github.com/whisper-money/whisper-money/commit/48b4b7bd01d4c9f80b3de98048746c537719d2af))
|
||||
* Delete pending connection and show toast on cancelled bank authorization ([#111](https://github.com/whisper-money/whisper-money/issues/111)) ([c7f3f1a](https://github.com/whisper-money/whisper-money/commit/c7f3f1a9788d33f324028aabcad19238f1c00ec3))
|
||||
* Disable email verification on dev/local ([1b0f3ba](https://github.com/whisper-money/whisper-money/commit/1b0f3ba24dc2c18fcd6c43abcb7c42c6184ec6ea))
|
||||
* Discord link ([d7f0084](https://github.com/whisper-money/whisper-money/commit/d7f00843380042ac121e50eb94deb0ea86470f55))
|
||||
* Header on iOS ([1d669b4](https://github.com/whisper-money/whisper-money/commit/1d669b44ca9e54247f66615cf11e5647aa2b2327))
|
||||
* Hide transaction checkboxes on mobile ([#109](https://github.com/whisper-money/whisper-money/issues/109)) ([abd7a2f](https://github.com/whisper-money/whisper-money/commit/abd7a2f9aa681aa19e091eeb6b3a161a71f1ae69))
|
||||
* Install script improvements ([da328ef](https://github.com/whisper-money/whisper-money/commit/da328efe7925c8fbb092579415ee66dfa2903891))
|
||||
* Missing import ([b3103d4](https://github.com/whisper-money/whisper-money/commit/b3103d4a61e20a33003c4dc436ab34bf9180fa0f))
|
||||
* Onboarding, account not shown on the import drawer ([#121](https://github.com/whisper-money/whisper-money/issues/121)) ([eeca437](https://github.com/whisper-money/whisper-money/commit/eeca437586b8f9f564782ea92bf85412eea28bb6))
|
||||
* Prevent account card content overflow on long names ([#133](https://github.com/whisper-money/whisper-money/issues/133)) ([a2b1e91](https://github.com/whisper-money/whisper-money/commit/a2b1e91b49695c299ea8c437048e6c3d429e653f))
|
||||
* Prevent automerge when CI checks have failed ([#95](https://github.com/whisper-money/whisper-money/issues/95)) ([6101cfd](https://github.com/whisper-money/whisper-money/commit/6101cfdfa022f3ecb78cc924e014667169f51d08)), closes [#94](https://github.com/whisper-money/whisper-money/issues/94) [#94](https://github.com/whisper-money/whisper-money/issues/94)
|
||||
* Prevent re-syncing deleted bank transactions ([#114](https://github.com/whisper-money/whisper-money/issues/114)) ([d1ba189](https://github.com/whisper-money/whisper-money/commit/d1ba18932e80e858c8e928530a5d012747288b96))
|
||||
* Small dashboard UI fix ([1500e5c](https://github.com/whisper-money/whisper-money/commit/1500e5cd9126bfd5be4e5e0c4e5767cb59d97c9a))
|
||||
* Top spending categories bug ([e8e4f47](https://github.com/whisper-money/whisper-money/commit/e8e4f4780497780daf88e2c1a6abd69bf290a9a2))
|
||||
* Top spending categories on mobile ([74ac346](https://github.com/whisper-money/whisper-money/commit/74ac346ca0c3b38a4b4b59a308a087fba78bc0e3))
|
||||
* Top spending category must be 100% with always ([f31a44b](https://github.com/whisper-money/whisper-money/commit/f31a44bba2756a4111f3e5e2c3d1b7ae2c643124))
|
||||
* Trigger sync on transactions drawer ([f88444f](https://github.com/whisper-money/whisper-money/commit/f88444fece957899245d0717414ac1efa345edb9))
|
||||
* Use workflow_run trigger for automerge ([#89](https://github.com/whisper-money/whisper-money/issues/89)) ([dfd8bf8](https://github.com/whisper-money/whisper-money/commit/dfd8bf8092a666fe4e955a37a7c63de33d732ced))
|
||||
* Welcome page mobile display for iOS ([#94](https://github.com/whisper-money/whisper-money/issues/94)) ([28f9432](https://github.com/whisper-money/whisper-money/commit/28f9432af4912344825007f463ae91480e336932))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Add --user and --connection filters to banking:sync command ([#122](https://github.com/whisper-money/whisper-money/issues/122)) ([b9abf49](https://github.com/whisper-money/whisper-money/commit/b9abf49617c7a8ef2803fc9231e35a2695ef5004))
|
||||
* Add 'Today' marker on budget spending chart ([#134](https://github.com/whisper-money/whisper-money/issues/134)) ([a0d19ae](https://github.com/whisper-money/whisper-money/commit/a0d19aef812024c9f218665b4d144d3ba7ba28f2))
|
||||
* Add automerge workflow for labeled PRs ([#88](https://github.com/whisper-money/whisper-money/issues/88)) ([10bd7da](https://github.com/whisper-money/whisper-money/commit/10bd7da5dbc1c0dada7bc0b72375ad9cd3fd9be7))
|
||||
* Add Binance integration ([#131](https://github.com/whisper-money/whisper-money/issues/131)) ([df9fc38](https://github.com/whisper-money/whisper-money/commit/df9fc385623a1ace173d4fbbc6e9a79ed93dc5ed))
|
||||
* Add Bitpanda exchange integration ([#132](https://github.com/whisper-money/whisper-money/issues/132)) ([fe76c2e](https://github.com/whisper-money/whisper-money/commit/fe76c2e43d2aa32210292456bc9d9c50355f3c2b))
|
||||
* Add daily balance chart with area visualization for account pages ([#135](https://github.com/whisper-money/whisper-money/issues/135)) ([126f7f7](https://github.com/whisper-money/whisper-money/commit/126f7f7e72e90ef0ae2862fd463136b70857e6ff))
|
||||
* Add daily granularity toggle with area visualization to net worth chart ([#136](https://github.com/whisper-money/whisper-money/issues/136)) ([900cf41](https://github.com/whisper-money/whisper-money/commit/900cf41e317433eedd76e28c7e6b9846cb330d69))
|
||||
* Add Indexa Capital integration ([#130](https://github.com/whisper-money/whisper-money/issues/130)) ([3f541ca](https://github.com/whisper-money/whisper-money/commit/3f541ca4d6376bc8a04d54851eccc7265060fe78))
|
||||
* Add multi-currency conversion for net worth charts ([#138](https://github.com/whisper-money/whisper-money/issues/138)) ([b743cad](https://github.com/whisper-money/whisper-money/commit/b743cad8039167d30c2278156b707b170922ac5a))
|
||||
* Add per-bank description formatter for bank-synced transactions ([#120](https://github.com/whisper-money/whisper-money/issues/120)) ([9242b3f](https://github.com/whisper-money/whisper-money/commit/9242b3fe5f14a03321a7bf4d1c9478f109f05ab6))
|
||||
* Add previous period comparison to budget chart ([#93](https://github.com/whisper-money/whisper-money/issues/93)) ([9bbd91a](https://github.com/whisper-money/whisper-money/commit/9bbd91ac12d98c8919de9b15c56d841333ebbb18))
|
||||
* Apply automation rules to bank-synced transactions ([#112](https://github.com/whisper-money/whisper-money/issues/112)) ([8ce0adf](https://github.com/whisper-money/whisper-money/commit/8ce0adf8aec1e297ba87ab2ce4b39c851241fae0))
|
||||
* Bulk delete with type-to-confirm modal ([#110](https://github.com/whisper-money/whisper-money/issues/110)) ([03fec11](https://github.com/whisper-money/whisper-money/commit/03fec11705acc7521d4b179d41ae59af60f34023))
|
||||
* Decrypt encrypted transactions on key unlock ([#123](https://github.com/whisper-money/whisper-money/issues/123)) ([6abec95](https://github.com/whisper-money/whisper-money/commit/6abec95d0eee266e1a4570400bd82d2e5228695e))
|
||||
* Docker dev env with Caddy, PHP on host ([#103](https://github.com/whisper-money/whisper-money/issues/103)) ([caccac6](https://github.com/whisper-money/whisper-money/commit/caccac6166c2e4fc9d485c710f06a65c1dd7360e))
|
||||
* Enable email verification on sign up ([#97](https://github.com/whisper-money/whisper-money/issues/97)) ([370d388](https://github.com/whisper-money/whisper-money/commit/370d388d99e01ab07fcfdec0991701e5204a30c3))
|
||||
* Improve PWA standalone experience and redirect to dashboard ([#90](https://github.com/whisper-money/whisper-money/issues/90)) ([b4897ef](https://github.com/whisper-money/whisper-money/commit/b4897ef4250fc467d01f57e7dc08ecf21faeb183)), closes [#71](https://github.com/whisper-money/whisper-money/issues/71)
|
||||
* Integrate EnableBanking as open banking provider ([#106](https://github.com/whisper-money/whisper-money/issues/106)) ([db7b6e4](https://github.com/whisper-money/whisper-money/commit/db7b6e4da7d6513c9fb088f4460256550cce246f))
|
||||
* Plaintext transactions behind feature flag ([#105](https://github.com/whisper-money/whisper-money/issues/105)) ([e35f712](https://github.com/whisper-money/whisper-money/commit/e35f7125b31e7682f36081cfb5f5750cf0433631))
|
||||
* Redirect to dashboard when running as installed PWA ([#92](https://github.com/whisper-money/whisper-money/issues/92)) ([1d1c0c3](https://github.com/whisper-money/whisper-money/commit/1d1c0c36fe3041dc8bc179dc2b96aaba4fd87214))
|
||||
* Remove dev command from whispermoney ([1930cf2](https://github.com/whisper-money/whisper-money/commit/1930cf229eb96508d158e145f47a7c7c82821f49))
|
||||
* Replace settings sidebar with dropdown on mobile ([#117](https://github.com/whisper-money/whisper-money/issues/117)) ([b69138d](https://github.com/whisper-money/whisper-money/commit/b69138df60ecc7358cbbdb698c0f3d4767b1c643))
|
||||
* Replace user avatar with Facehash faces ([#86](https://github.com/whisper-money/whisper-money/issues/86)) ([6aa9da3](https://github.com/whisper-money/whisper-money/commit/6aa9da3df39e768a87037d5d4bb9d6f981728714))
|
||||
* Show loading spinner on landing page when in PWA mode ([#96](https://github.com/whisper-money/whisper-money/issues/96)) ([21d36bb](https://github.com/whisper-money/whisper-money/commit/21d36bb53b849e17a9c3e46b065d64607e48188f))
|
||||
* Show PWA install button on mobile landing page ([#99](https://github.com/whisper-money/whisper-money/issues/99)) ([abc71da](https://github.com/whisper-money/whisper-money/commit/abc71daa7e65b386eb30ba9672c8288016665a56))
|
||||
* Spanish localization ([#74](https://github.com/whisper-money/whisper-money/issues/74)) ([70b603e](https://github.com/whisper-money/whisper-money/commit/70b603e901c058c40f19b7c7ce1d31c7ecb0f640))
|
||||
|
||||
## [0.1.9](https://github.com/whisper-money/whisper-money/compare/v0.1.8...v0.1.9) (2026-01-28)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Apply automation rule labels on transaction creation and import ([#79](https://github.com/whisper-money/whisper-money/issues/79)) ([a6a2a0d](https://github.com/whisper-money/whisper-money/commit/a6a2a0d58cd1f2dee3dd524567e7ccf6a074c02a)), closes [#61](https://github.com/whisper-money/whisper-money/issues/61)
|
||||
* Delete transactions on local browser DB after deleting it on the backend ([d1f69a2](https://github.com/whisper-money/whisper-money/commit/d1f69a284a28386b124bbbea295ce8064ab2a362))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Print sponsor message on whispermoney script ([f03fcf5](https://github.com/whisper-money/whisper-money/commit/f03fcf5ac61517b20b82bffd3277a6ab66098d89))
|
||||
* Release budgets feature to all users ([#84](https://github.com/whisper-money/whisper-money/issues/84)) ([a9b889b](https://github.com/whisper-money/whisper-money/commit/a9b889b1459ba9dd5f857ce30c837a183f77dc79))
|
||||
* Reload transactions table on import proccess complete ([bbc3027](https://github.com/whisper-money/whisper-money/commit/bbc302754541f4c61080284f8ca729fe5aea4ecf))
|
||||
* Sync new users to Resend contacts ([#85](https://github.com/whisper-money/whisper-money/issues/85)) ([952a5d4](https://github.com/whisper-money/whisper-money/commit/952a5d4be784634ba1b2095621fabed3fd86d56d))
|
||||
|
||||
## [0.1.8](https://github.com/whisper-money/whisper-money/compare/v0.1.7...v0.1.8) (2026-01-25)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Fire transaction updated event after a label change ([#73](https://github.com/whisper-money/whisper-money/issues/73)) ([134a292](https://github.com/whisper-money/whisper-money/commit/134a292ddb5d58b7428c4a50becee8dd957e4c09))
|
||||
- Progress bar color on dark scheme ([d216d0c](https://github.com/whisper-money/whisper-money/commit/d216d0c071e8ff380627122f8f70e947bb7f667b))
|
||||
- Typo in composer dev command ([f30e600](https://github.com/whisper-money/whisper-money/commit/f30e600b75fb71f28e63ee88ec1bc414038adba5))
|
||||
- Update transactions ([91dd23e](https://github.com/whisper-money/whisper-money/commit/91dd23edc05e3efa72723347ac5b010ebea5c479))
|
||||
|
||||
### Features
|
||||
|
||||
- Add label support to single transaction update endpoint ([#75](https://github.com/whisper-money/whisper-money/issues/75)) ([e5eca1e](https://github.com/whisper-money/whisper-money/commit/e5eca1eacb86aec87f6aee8a9a685400778d2583))
|
||||
- Load transactions history on budget created ([#72](https://github.com/whisper-money/whisper-money/issues/72)) ([fee7ad3](https://github.com/whisper-money/whisper-money/commit/fee7ad36abd899d63b220a9d7ad0b670d9feec7f))
|
||||
|
||||
## [0.1.7](https://github.com/whisper-money/whisper-money/compare/v0.1.6...v0.1.7) (2026-01-21)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Error showing randomg transactions from local browser DB ([a7c8544](https://github.com/whisper-money/whisper-money/commit/a7c8544249a887bb96f256d9d336a9b8e13090f1))
|
||||
- unused vars ([f1a2d78](https://github.com/whisper-money/whisper-money/commit/f1a2d787e5be6c096703f27a5463696f6095f72c))
|
||||
|
||||
### Features
|
||||
|
||||
- Add PostHog ([#70](https://github.com/whisper-money/whisper-money/issues/70)) ([f5d09eb](https://github.com/whisper-money/whisper-money/commit/f5d09eb2475dc3c1c76dd6a7c23fadb771576fdb))
|
||||
|
||||
## [0.1.6](https://github.com/whisper-money/whisper-money/compare/v0.1.5...v0.1.6) (2026-01-19)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- MYSQL_EXTRA_OPTIONS env var ([49ed94c](https://github.com/whisper-money/whisper-money/commit/49ed94cbc7f0d55bf3fef38ce1a620449fe51e1e))
|
||||
|
||||
### Features
|
||||
|
||||
- Better, easier, and faster account balance update modal ([#65](https://github.com/whisper-money/whisper-money/issues/65)) ([f4ab918](https://github.com/whisper-money/whisper-money/commit/f4ab9181e1235885f0f8158d67de2cd719bfb0d3))
|
||||
- Don't check upgrades if not in main branch or in DEV_MODE ([16a331a](https://github.com/whisper-money/whisper-money/commit/16a331ab5f654e332bfd0625f3621b99c13f61dd))
|
||||
|
||||
## [0.1.5](https://github.com/whisper-money/whisper-money/compare/v0.1.3...v0.1.5) (2026-01-17)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- broken dashboard while loading ([253fe44](https://github.com/whisper-money/whisper-money/commit/253fe447bdd999db78f7fa96e2ffa34e8194e5ce))
|
||||
- Check IDOR vulnerabilities ([#60](https://github.com/whisper-money/whisper-money/issues/60)) ([80117c3](https://github.com/whisper-money/whisper-money/commit/80117c3edeaf5c5a5166f3815fc555a15b5ce686))
|
||||
- delay emails to avoid reaching daily resend limit ([8ac2520](https://github.com/whisper-money/whisper-money/commit/8ac25200dc9ed5a5b4e24e36e32668e52ea95477))
|
||||
- Remove scheduled horizon command (unused anymore) ([63bde93](https://github.com/whisper-money/whisper-money/commit/63bde938b51d5f13a6f817a0beb5d91f48f3d6f3))
|
||||
- Use user currency in top spending categories card ([#57](https://github.com/whisper-money/whisper-money/issues/57)) ([21a4d87](https://github.com/whisper-money/whisper-money/commit/21a4d87f8562a0e95a62abe261cff7accb8fb2b2)), closes [#56](https://github.com/whisper-money/whisper-money/issues/56)
|
||||
|
||||
### Features
|
||||
|
||||
- Add wispermoney local command ([#59](https://github.com/whisper-money/whisper-money/issues/59)) ([ffd9694](https://github.com/whisper-money/whisper-money/commit/ffd96949e5e682aa42904d241772ba87ac72a067))
|
||||
- Auto-open encryption key modal after login ([#54](https://github.com/whisper-money/whisper-money/issues/54)) ([d16282d](https://github.com/whisper-money/whisper-money/commit/d16282dbad7c7b58843c28dedf0a04265355a8a6))
|
||||
- Automated setup script for local deployment ([#58](https://github.com/whisper-money/whisper-money/issues/58)) ([819bea1](https://github.com/whisper-money/whisper-money/commit/819bea19223bdf2a33ff4a66c2e4803f26fbaf5e))
|
||||
- Group small expending categories on the Sankey chart ([5618893](https://github.com/whisper-money/whisper-money/commit/5618893be8a0e0255e1abd7b3e2ff7c65e3eb046))
|
||||
- Persist transactions filter on the URL ([c9877a5](https://github.com/whisper-money/whisper-money/commit/c9877a503dea45505dc46a1b9b23142e0aefc290))
|
||||
- Persist cashflow period on the URL ([1343e1c](https://github.com/whisper-money/whisper-money/commit/1343e1c75fc645ae7253f2d02b50178243cb70d9))
|
||||
|
||||
## [0.1.4](https://github.com/whisper-money/whisper-money/compare/v0.1.3...v0.1.4) (2026-01-11)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- delay emails to avoid reaching daily resend limit ([8ac2520](https://github.com/whisper-money/whisper-money/commit/8ac25200dc9ed5a5b4e24e36e32668e52ea95477))
|
||||
- Remove scheduled horizon command (unused anymore) ([63bde93](https://github.com/whisper-money/whisper-money/commit/63bde938b51d5f13a6f817a0beb5d91f48f3d6f3))
|
||||
|
||||
### Features
|
||||
|
||||
- Group small expending categories on the Sankey chart ([5618893](https://github.com/whisper-money/whisper-money/commit/5618893be8a0e0255e1abd7b3e2ff7c65e3eb046))
|
||||
- Persist transactions filter on the URL ([c9877a5](https://github.com/whisper-money/whisper-money/commit/c9877a503dea45505dc46a1b9b23142e0aefc290))
|
||||
- Persist cashflow period on the URL ([1343e1c](https://github.com/whisper-money/whisper-money/commit/1343e1c75fc645ae7253f2d02b50178243cb70d9))
|
||||
|
||||
## [0.1.3](https://github.com/whisper-money/whisper-money/compare/v0.1.1...v0.1.3) (2026-01-09)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- issue on filters when no label created ([cb1d6a2](https://github.com/whisper-money/whisper-money/commit/cb1d6a230f0c1c0734e5267a5a4d2753f4b91cff))
|
||||
- scroll category combobox to top while searching ([c1ddc14](https://github.com/whisper-money/whisper-money/commit/c1ddc1477d89c9bfc9aa36bc81f8d48fda05208a))
|
||||
|
||||
### Features
|
||||
|
||||
- new roadmap and feedback links ([0646b38](https://github.com/whisper-money/whisper-money/commit/0646b380cecc6c3a2859206429a84c0e3ac1c798))
|
||||
- Send custom emails to users ([#52](https://github.com/whisper-money/whisper-money/issues/52)) ([683b3f3](https://github.com/whisper-money/whisper-money/commit/683b3f32a7a1467a9fd2e269903570c33164ff83))
|
||||
|
||||
## [0.1.2](https://github.com/whisper-money/whisper-money/compare/v0.1.1...v0.1.2) (2026-01-07)
|
||||
|
||||
### New Features
|
||||
|
||||
- Cashflow view ([475c650](https://github.com/whisper-money/whisper-money/pull/49))
|
||||
- Demo account ([8addcad](https://github.com/whisper-money/whisper-money/pull/51))
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- issue on filters when no label created ([cb1d6a2](https://github.com/whisper-money/whisper-money/commit/cb1d6a230f0c1c0734e5267a5a4d2753f4b91cff))
|
||||
* issue on filters when no label created ([cb1d6a2](https://github.com/whisper-money/whisper-money/commit/cb1d6a230f0c1c0734e5267a5a4d2753f4b91cff))
|
||||
|
||||
## 0.1.1 (2026-01-05)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- add SSR guards to localStorage/sessionStorage access ([3b56e24](https://github.com/whisper-money/whisper-money/commit/3b56e2444713f922bccc2790f676dab167758500))
|
||||
- add SyncProvider to SSR entry point ([3177fa3](https://github.com/whisper-money/whisper-money/commit/3177fa3519e2728c813f7532bfc7c65b603398b7))
|
||||
- app logo icon auto of the dashboard ([e813849](https://github.com/whisper-money/whisper-money/commit/e813849e7ba352f1ad0100fd77a41d770bed1968))
|
||||
- apply border radius to visible bar segments in stacked chart ([413f83f](https://github.com/whisper-money/whisper-money/commit/413f83f96163b1ae6ce5e62d810fbdaccae480d6))
|
||||
- asd key element to accounts index page ([8eab41a](https://github.com/whisper-money/whisper-money/commit/8eab41ac89747437f3afcd27e90012ddc8d1e3dd))
|
||||
- auto-regenerate APP_KEY if invalid format (missing base64: prefix) ([797cb06](https://github.com/whisper-money/whisper-money/commit/797cb06f86037a1f89b0875aa5ed38307c70ed57))
|
||||
- automated rules broken and now they work in batches ([890593d](https://github.com/whisper-money/whisper-money/commit/890593d9674d0aacf9f4491a49e36ca6884afa9b))
|
||||
- Automated rules with labels ([#32](https://github.com/whisper-money/whisper-money/issues/32)) ([bf0c9ae](https://github.com/whisper-money/whisper-money/commit/bf0c9ae989f2543b7093630bfa0723c669689b3b))
|
||||
- bulk action bar style ([045c7a5](https://github.com/whisper-money/whisper-money/commit/045c7a5752081eb0b1ba9cbe5744eab13ad2d7c5))
|
||||
- **category-combobox:** Improve UI responsiveness and truncate category names ([2cecd01](https://github.com/whisper-money/whisper-money/commit/2cecd014e0cff0aefe70ace625baacbf58255f6d))
|
||||
- **charts:** mobile ui, and desktop tooltips ([818a49e](https://github.com/whisper-money/whisper-money/commit/818a49e79956f16d71e01736593cec762bb67a46))
|
||||
- deploy ci ([d4410a6](https://github.com/whisper-money/whisper-money/commit/d4410a67fe81e0e409138ab7913b7e3787604e66))
|
||||
- increase nginx buffer sizes ([a87b36d](https://github.com/whisper-money/whisper-money/commit/a87b36de3f4416abacb232976ca3d113592d32fa))
|
||||
- make encryption key storage SSR-safe to prevent 502 errors ([0fcc66e](https://github.com/whisper-money/whisper-money/commit/0fcc66e25d2eba710111e0da2bed64bbe5ee9110))
|
||||
- make useIsMobile hook and utility functions SSR-safe ([40762bc](https://github.com/whisper-money/whisper-money/commit/40762bc528447f42b39ca5121a9047f376ffbe6b))
|
||||
- migration history ([b52e2de](https://github.com/whisper-money/whisper-money/commit/b52e2de9870294e0aa5a8da5f047a51930d52167))
|
||||
- **mobile:** account chart ([14a9343](https://github.com/whisper-money/whisper-money/commit/14a9343c1d5142beea7bc9dbfa130510fd5addbc))
|
||||
- normalize transaction_date to YYYY-MM-DD for duplicate detection ([#4](https://github.com/whisper-money/whisper-money/issues/4)) ([7492b2e](https://github.com/whisper-money/whisper-money/commit/7492b2e7360f6b8e53be891ce55a74e0b4fa6c66))
|
||||
- re-enable ssr for all routes after issue is fixed ([1d96f5d](https://github.com/whisper-money/whisper-money/commit/1d96f5dc63b6a8abf6107f683ceb9c73fc8763b1))
|
||||
- rong schedule import ([c684695](https://github.com/whisper-money/whisper-money/commit/c684695008cbf180cc4a621b9fc325ee8669e5da))
|
||||
- **sync:** make transaction creation idempotent ([#38](https://github.com/whisper-money/whisper-money/issues/38)) ([3cbe0a7](https://github.com/whisper-money/whisper-money/commit/3cbe0a7879df68affe62944901dfc2054855fbf1))
|
||||
- toast on mobile ([716e21b](https://github.com/whisper-money/whisper-money/commit/716e21b219a31a07b8e6cf859567b45e15d1a485))
|
||||
- transaction list on account page ([ce09f32](https://github.com/whisper-money/whisper-money/commit/ce09f32a9290561363169ec7a7d3b85999aaf35e))
|
||||
- **TransactionFilters:** Update badge styling for uncategorized selection ([a2d7af2](https://github.com/whisper-money/whisper-money/commit/a2d7af27898040dcfbb7287ba8803edbf28db14d))
|
||||
- **transactions:** Decrypt account names for automation rule evaluation ([323b738](https://github.com/whisper-money/whisper-money/commit/323b7386c1e5e1cfbf32258d7430b2e3686e4b4c))
|
||||
- **transactions:** We were creating transactions with numberic ID instead of UUID v7 ([52e1a7b](https://github.com/whisper-money/whisper-money/commit/52e1a7bd955d0018ba5a2cfa761e6c58aaa81d3f))
|
||||
- use direct PDO connection test for MySQL readiness check ([a7ee776](https://github.com/whisper-money/whisper-money/commit/a7ee776af791a92f42fada35965476b9d903b50a))
|
||||
- use markdown to send user lead invitation mail ([1e9566a](https://github.com/whisper-money/whisper-money/commit/1e9566a289125133d23bba7a7ed2102e126b5a08))
|
||||
- wrap SSR app with EncryptionKeyProvider ([770f091](https://github.com/whisper-money/whisper-money/commit/770f091b9b4509e0b5ca51ded1080b228594500e))
|
||||
- wrong user menu text ([b2d1bcf](https://github.com/whisper-money/whisper-money/commit/b2d1bcf54c7061ab6cc2adb8182795eedd20233d))
|
||||
* add SSR guards to localStorage/sessionStorage access ([3b56e24](https://github.com/whisper-money/whisper-money/commit/3b56e2444713f922bccc2790f676dab167758500))
|
||||
* add SyncProvider to SSR entry point ([3177fa3](https://github.com/whisper-money/whisper-money/commit/3177fa3519e2728c813f7532bfc7c65b603398b7))
|
||||
* app logo icon auto of the dashboard ([e813849](https://github.com/whisper-money/whisper-money/commit/e813849e7ba352f1ad0100fd77a41d770bed1968))
|
||||
* apply border radius to visible bar segments in stacked chart ([413f83f](https://github.com/whisper-money/whisper-money/commit/413f83f96163b1ae6ce5e62d810fbdaccae480d6))
|
||||
* asd key element to accounts index page ([8eab41a](https://github.com/whisper-money/whisper-money/commit/8eab41ac89747437f3afcd27e90012ddc8d1e3dd))
|
||||
* auto-regenerate APP_KEY if invalid format (missing base64: prefix) ([797cb06](https://github.com/whisper-money/whisper-money/commit/797cb06f86037a1f89b0875aa5ed38307c70ed57))
|
||||
* automated rules broken and now they work in batches ([890593d](https://github.com/whisper-money/whisper-money/commit/890593d9674d0aacf9f4491a49e36ca6884afa9b))
|
||||
* Automated rules with labels ([#32](https://github.com/whisper-money/whisper-money/issues/32)) ([bf0c9ae](https://github.com/whisper-money/whisper-money/commit/bf0c9ae989f2543b7093630bfa0723c669689b3b))
|
||||
* bulk action bar style ([045c7a5](https://github.com/whisper-money/whisper-money/commit/045c7a5752081eb0b1ba9cbe5744eab13ad2d7c5))
|
||||
* **category-combobox:** Improve UI responsiveness and truncate category names ([2cecd01](https://github.com/whisper-money/whisper-money/commit/2cecd014e0cff0aefe70ace625baacbf58255f6d))
|
||||
* **charts:** mobile ui, and desktop tooltips ([818a49e](https://github.com/whisper-money/whisper-money/commit/818a49e79956f16d71e01736593cec762bb67a46))
|
||||
* deploy ci ([d4410a6](https://github.com/whisper-money/whisper-money/commit/d4410a67fe81e0e409138ab7913b7e3787604e66))
|
||||
* increase nginx buffer sizes ([a87b36d](https://github.com/whisper-money/whisper-money/commit/a87b36de3f4416abacb232976ca3d113592d32fa))
|
||||
* make encryption key storage SSR-safe to prevent 502 errors ([0fcc66e](https://github.com/whisper-money/whisper-money/commit/0fcc66e25d2eba710111e0da2bed64bbe5ee9110))
|
||||
* make useIsMobile hook and utility functions SSR-safe ([40762bc](https://github.com/whisper-money/whisper-money/commit/40762bc528447f42b39ca5121a9047f376ffbe6b))
|
||||
* migration history ([b52e2de](https://github.com/whisper-money/whisper-money/commit/b52e2de9870294e0aa5a8da5f047a51930d52167))
|
||||
* **mobile:** account chart ([14a9343](https://github.com/whisper-money/whisper-money/commit/14a9343c1d5142beea7bc9dbfa130510fd5addbc))
|
||||
* normalize transaction_date to YYYY-MM-DD for duplicate detection ([#4](https://github.com/whisper-money/whisper-money/issues/4)) ([7492b2e](https://github.com/whisper-money/whisper-money/commit/7492b2e7360f6b8e53be891ce55a74e0b4fa6c66))
|
||||
* re-enable ssr for all routes after issue is fixed ([1d96f5d](https://github.com/whisper-money/whisper-money/commit/1d96f5dc63b6a8abf6107f683ceb9c73fc8763b1))
|
||||
* rong schedule import ([c684695](https://github.com/whisper-money/whisper-money/commit/c684695008cbf180cc4a621b9fc325ee8669e5da))
|
||||
* **sync:** make transaction creation idempotent ([#38](https://github.com/whisper-money/whisper-money/issues/38)) ([3cbe0a7](https://github.com/whisper-money/whisper-money/commit/3cbe0a7879df68affe62944901dfc2054855fbf1))
|
||||
* toast on mobile ([716e21b](https://github.com/whisper-money/whisper-money/commit/716e21b219a31a07b8e6cf859567b45e15d1a485))
|
||||
* transaction list on account page ([ce09f32](https://github.com/whisper-money/whisper-money/commit/ce09f32a9290561363169ec7a7d3b85999aaf35e))
|
||||
* **TransactionFilters:** Update badge styling for uncategorized selection ([a2d7af2](https://github.com/whisper-money/whisper-money/commit/a2d7af27898040dcfbb7287ba8803edbf28db14d))
|
||||
* **transactions:** Decrypt account names for automation rule evaluation ([323b738](https://github.com/whisper-money/whisper-money/commit/323b7386c1e5e1cfbf32258d7430b2e3686e4b4c))
|
||||
* **transactions:** We were creating transactions with numberic ID instead of UUID v7 ([52e1a7b](https://github.com/whisper-money/whisper-money/commit/52e1a7bd955d0018ba5a2cfa761e6c58aaa81d3f))
|
||||
* use direct PDO connection test for MySQL readiness check ([a7ee776](https://github.com/whisper-money/whisper-money/commit/a7ee776af791a92f42fada35965476b9d903b50a))
|
||||
* use markdown to send user lead invitation mail ([1e9566a](https://github.com/whisper-money/whisper-money/commit/1e9566a289125133d23bba7a7ed2102e126b5a08))
|
||||
* wrap SSR app with EncryptionKeyProvider ([770f091](https://github.com/whisper-money/whisper-money/commit/770f091b9b4509e0b5ca51ded1080b228594500e))
|
||||
* wrong user menu text ([b2d1bcf](https://github.com/whisper-money/whisper-money/commit/b2d1bcf54c7061ab6cc2adb8182795eedd20233d))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
- **.cursor:** Add whisper-money rule configuration ([e80647d](https://github.com/whisper-money/whisper-money/commit/e80647dc130f1c4b5f51857b27649229cf887701))
|
||||
- **AccountBalanceSync:** Update existing balances and add new ones efficiently ([c2c6894](https://github.com/whisper-money/whisper-money/commit/c2c6894cb860e768fdb2c5ece746bf97129784db))
|
||||
- Add account balance chart improvements and icons ([#5](https://github.com/whisper-money/whisper-money/issues/5)) ([5f149b4](https://github.com/whisper-money/whisper-money/commit/5f149b4bae7065f2c2aaa191941bdc3fa9dfe41e))
|
||||
- Add bank selection to edit transaction dialog ([0473371](https://github.com/whisper-money/whisper-money/commit/0473371fce68f95cbce5aa3bf590253e56c7129d))
|
||||
- Add Discord invite link to welcome page ([f3c0fa1](https://github.com/whisper-money/whisper-money/commit/f3c0fa1355921a2dceab1e1dd5df5e0cd5527c7f))
|
||||
- Add financial models and seeders ([635cde0](https://github.com/whisper-money/whisper-money/commit/635cde021b59c9078e72882327c17d500503d22a))
|
||||
- Add import transactions button to transactions page ([e5a77a9](https://github.com/whisper-money/whisper-money/commit/e5a77a9aca92cc8b12e09d24402ef3d84a223b0e))
|
||||
- add multiple chart view modes for net worth evolution ([#37](https://github.com/whisper-money/whisper-money/issues/37)) ([c5df59c](https://github.com/whisper-money/whisper-money/commit/c5df59c285b253ac5f4bbef36a4523fe885491af))
|
||||
- Add new category icons and colors ([c339105](https://github.com/whisper-money/whisper-money/commit/c33910587585ea8da4dfde4b79aa14498fc58692))
|
||||
- Add privacy mode to hide monetary amounts ([#28](https://github.com/whisper-money/whisper-money/issues/28)) ([8811afb](https://github.com/whisper-money/whisper-money/commit/8811afbad8f5ef2dae0ebb8562a66d8ae9aa3938))
|
||||
- add transaction labels feature ([#24](https://github.com/whisper-money/whisper-money/issues/24)) ([4b5d65b](https://github.com/whisper-money/whisper-money/commit/4b5d65ba03371c7b85bab0b64ec4dc8d19b015b3))
|
||||
- add version tracking with git tags and changelog ([db81c9b](https://github.com/whisper-money/whisper-money/commit/db81c9b88861dd60eef97eba035cf03ca1a7d6a1))
|
||||
- **auth:** Add key clearing on login ([3795e46](https://github.com/whisper-money/whisper-money/commit/3795e46d4fb11e228524f2e8557cd931a315db8e))
|
||||
- **automation:** Add re-evaluate all transactions functionality ([e937a86](https://github.com/whisper-money/whisper-money/commit/e937a8647dbe69fbd93ea2b5ddad44bbe7ba4a18))
|
||||
- **automation:** Add sync functionality to automation rule dialogs ([e009abb](https://github.com/whisper-money/whisper-money/commit/e009abbee19252bab2dbcc18170c54870df9f5b9))
|
||||
- **category:** Update default categories list and sorting logic ([73d847f](https://github.com/whisper-money/whisper-money/commit/73d847f38b35e3c25a3f890574e42b5210d12d67))
|
||||
- centralize pricing config with multiple plans support ([#20](https://github.com/whisper-money/whisper-money/issues/20)) ([58b9343](https://github.com/whisper-money/whisper-money/commit/58b934333f55a43372fefd634cde05a3b0109859))
|
||||
- Configure Resend email integration ([#34](https://github.com/whisper-money/whisper-money/issues/34)) ([3c22453](https://github.com/whisper-money/whisper-money/commit/3c22453fc611a109d69ed3c6bff2e6fb12163aba))
|
||||
- **Docker:** Add Bun installation and update build process ([4379239](https://github.com/whisper-money/whisper-money/commit/43792392b4e9b3213b39348eeaa002e13348df9a))
|
||||
- **Docker:** Add Wayfinder route generation and update asset build process ([a13e7fd](https://github.com/whisper-money/whisper-money/commit/a13e7fd538628b0ebc1c1b0a9893a5b36b2b32d2))
|
||||
- **Docker:** Optimize build process by removing unnecessary steps and adjusting environment variables ([732775e](https://github.com/whisper-money/whisper-money/commit/732775e47ef92f01f0449b2cad1e337627bd5a4b))
|
||||
- **Docker:** Replace pnpm with Bun for Node.js package management ([5b45006](https://github.com/whisper-money/whisper-money/commit/5b450067eb51e003e0074a44276587d7afe8514c))
|
||||
- **Docker:** Replace pnpm with bun for package management and build process ([b4b891f](https://github.com/whisper-money/whisper-money/commit/b4b891f204a7bf8fe1f1b9c036cfee6052a18bd4))
|
||||
- **encrypted-text:** Add animation and random character generation ([7d8474f](https://github.com/whisper-money/whisper-money/commit/7d8474f6b81f032ac4585fceb293c9d5e6e5594d))
|
||||
- **encrypted-text:** Improve encryption UI with dynamic masking and loading state ([ff186a4](https://github.com/whisper-money/whisper-money/commit/ff186a4887c715b10205508d41f453df90201b26))
|
||||
- Implement drip email campaign system ([#35](https://github.com/whisper-money/whisper-money/issues/35)) ([46c5b13](https://github.com/whisper-money/whisper-money/commit/46c5b137392a333c98ebcb6d3435556b52a18994))
|
||||
- **import-transactions-drawer:** Add json-logic-js dependency and improve import logic ([1df3bad](https://github.com/whisper-money/whisper-money/commit/1df3bad3c3d27e4fe224277c4aedb8872fb6ba25))
|
||||
- **lucide-react:** Add custom icons to Toaster component ([573b2fd](https://github.com/whisper-money/whisper-money/commit/573b2fdb0a13cd2c2064996c8660a98ed97a60c2))
|
||||
- **queue:** Implement queueable email jobs with rate limiting ([3d0d6c8](https://github.com/whisper-money/whisper-money/commit/3d0d6c8bef11e06e3a39b7a8e9dbc4fb166657e7))
|
||||
- **react:** add authentication check in SyncProvider ([48bce81](https://github.com/whisper-money/whisper-money/commit/48bce81d9a23f894008bdfaa9c6876431f0c293e))
|
||||
- Remove console.log and add padding to components ([c1f99fe](https://github.com/whisper-money/whisper-money/commit/c1f99fedd6255621e3c9a301d79bbe3968908aea))
|
||||
- Replace Input with Textarea for editable descriptions ([2b6acf4](https://github.com/whisper-money/whisper-money/commit/2b6acf49d8770c74538e0f8664d9e88b4ae0b63e))
|
||||
- **settings:** Update account management UI and add sync functionality ([ab63edd](https://github.com/whisper-money/whisper-money/commit/ab63edde2b23f1a9055fcce7b456a4825251cebb))
|
||||
- **shared:** Add CategoryCombobox component ([57879bb](https://github.com/whisper-money/whisper-money/commit/57879bb7118850ae03ed2059dc5b775c29f5885d))
|
||||
- **sync:** Add sync functionality for accounts, banks, categories, and status button ([9256148](https://github.com/whisper-money/whisper-money/commit/9256148961201ba52fe93d29517fb6c0dbf24147))
|
||||
- **traefik:** Add secure headers middleware to WhisperMoney service ([242be5f](https://github.com/whisper-money/whisper-money/commit/242be5f415be11696fafdf4db68f4dafae964c66))
|
||||
- **TransactionController:** Add store method for creating transactions ([c1fbd4d](https://github.com/whisper-money/whisper-money/commit/c1fbd4d09fe67a092ad45e49b97ce7a172cf9913))
|
||||
- **TransactionSyncController:** Sort transactions by transaction_date and updated_at ([41f5c64](https://github.com/whisper-money/whisper-money/commit/41f5c6485c11934e69c6efab2868ea541e2856d4))
|
||||
- **ui:** Implement virtual scrolling for DataTable component ([07ca633](https://github.com/whisper-money/whisper-money/commit/07ca63347e9bae5bc59b8f0f8073e64da1df68f4))
|
||||
- **ui:** Improve chart tooltip content rendering and calculation ([d04b6a0](https://github.com/whisper-money/whisper-money/commit/d04b6a0174910f5e8eb4dce491805e60d7e67c04))
|
||||
- update date formatting logic in transaction components ([d13ecc2](https://github.com/whisper-money/whisper-money/commit/d13ecc2722509501d018b27a3b4dd83e7ab4351b))
|
||||
- Update encryption key button icon based on state ([08baf3b](https://github.com/whisper-money/whisper-money/commit/08baf3b19a8d4a631d2942a31e47071be68a128c))
|
||||
- Update ProfileController to include two-factor authentication settings ([e21c9cc](https://github.com/whisper-money/whisper-money/commit/e21c9cc3a89fdb8ac84bea49e4a1f6963ab7542e))
|
||||
- Update welcome page title to focus on understanding finances ([3ac7102](https://github.com/whisper-money/whisper-money/commit/3ac71025013ed1c8da713c753b9ef2bd3e050eee))
|
||||
- **use-dashboard-data:** Add conditional formatting for current year dates ([525e770](https://github.com/whisper-money/whisper-money/commit/525e7709cc8c92f90ece1bfce572e8434de60b15))
|
||||
- **welcome:** Add GitHub link and refactor auth buttons ([2ab362d](https://github.com/whisper-money/whisper-money/commit/2ab362dc5db7fa14104232cce283e53f5b658761))
|
||||
* **.cursor:** Add whisper-money rule configuration ([e80647d](https://github.com/whisper-money/whisper-money/commit/e80647dc130f1c4b5f51857b27649229cf887701))
|
||||
* **AccountBalanceSync:** Update existing balances and add new ones efficiently ([c2c6894](https://github.com/whisper-money/whisper-money/commit/c2c6894cb860e768fdb2c5ece746bf97129784db))
|
||||
* Add account balance chart improvements and icons ([#5](https://github.com/whisper-money/whisper-money/issues/5)) ([5f149b4](https://github.com/whisper-money/whisper-money/commit/5f149b4bae7065f2c2aaa191941bdc3fa9dfe41e))
|
||||
* Add bank selection to edit transaction dialog ([0473371](https://github.com/whisper-money/whisper-money/commit/0473371fce68f95cbce5aa3bf590253e56c7129d))
|
||||
* Add Discord invite link to welcome page ([f3c0fa1](https://github.com/whisper-money/whisper-money/commit/f3c0fa1355921a2dceab1e1dd5df5e0cd5527c7f))
|
||||
* Add financial models and seeders ([635cde0](https://github.com/whisper-money/whisper-money/commit/635cde021b59c9078e72882327c17d500503d22a))
|
||||
* Add import transactions button to transactions page ([e5a77a9](https://github.com/whisper-money/whisper-money/commit/e5a77a9aca92cc8b12e09d24402ef3d84a223b0e))
|
||||
* add multiple chart view modes for net worth evolution ([#37](https://github.com/whisper-money/whisper-money/issues/37)) ([c5df59c](https://github.com/whisper-money/whisper-money/commit/c5df59c285b253ac5f4bbef36a4523fe885491af))
|
||||
* Add new category icons and colors ([c339105](https://github.com/whisper-money/whisper-money/commit/c33910587585ea8da4dfde4b79aa14498fc58692))
|
||||
* Add privacy mode to hide monetary amounts ([#28](https://github.com/whisper-money/whisper-money/issues/28)) ([8811afb](https://github.com/whisper-money/whisper-money/commit/8811afbad8f5ef2dae0ebb8562a66d8ae9aa3938))
|
||||
* add transaction labels feature ([#24](https://github.com/whisper-money/whisper-money/issues/24)) ([4b5d65b](https://github.com/whisper-money/whisper-money/commit/4b5d65ba03371c7b85bab0b64ec4dc8d19b015b3))
|
||||
* add version tracking with git tags and changelog ([db81c9b](https://github.com/whisper-money/whisper-money/commit/db81c9b88861dd60eef97eba035cf03ca1a7d6a1))
|
||||
* **auth:** Add key clearing on login ([3795e46](https://github.com/whisper-money/whisper-money/commit/3795e46d4fb11e228524f2e8557cd931a315db8e))
|
||||
* **automation:** Add re-evaluate all transactions functionality ([e937a86](https://github.com/whisper-money/whisper-money/commit/e937a8647dbe69fbd93ea2b5ddad44bbe7ba4a18))
|
||||
* **automation:** Add sync functionality to automation rule dialogs ([e009abb](https://github.com/whisper-money/whisper-money/commit/e009abbee19252bab2dbcc18170c54870df9f5b9))
|
||||
* **category:** Update default categories list and sorting logic ([73d847f](https://github.com/whisper-money/whisper-money/commit/73d847f38b35e3c25a3f890574e42b5210d12d67))
|
||||
* centralize pricing config with multiple plans support ([#20](https://github.com/whisper-money/whisper-money/issues/20)) ([58b9343](https://github.com/whisper-money/whisper-money/commit/58b934333f55a43372fefd634cde05a3b0109859))
|
||||
* Configure Resend email integration ([#34](https://github.com/whisper-money/whisper-money/issues/34)) ([3c22453](https://github.com/whisper-money/whisper-money/commit/3c22453fc611a109d69ed3c6bff2e6fb12163aba))
|
||||
* **Docker:** Add Bun installation and update build process ([4379239](https://github.com/whisper-money/whisper-money/commit/43792392b4e9b3213b39348eeaa002e13348df9a))
|
||||
* **Docker:** Add Wayfinder route generation and update asset build process ([a13e7fd](https://github.com/whisper-money/whisper-money/commit/a13e7fd538628b0ebc1c1b0a9893a5b36b2b32d2))
|
||||
* **Docker:** Optimize build process by removing unnecessary steps and adjusting environment variables ([732775e](https://github.com/whisper-money/whisper-money/commit/732775e47ef92f01f0449b2cad1e337627bd5a4b))
|
||||
* **Docker:** Replace pnpm with Bun for Node.js package management ([5b45006](https://github.com/whisper-money/whisper-money/commit/5b450067eb51e003e0074a44276587d7afe8514c))
|
||||
* **Docker:** Replace pnpm with bun for package management and build process ([b4b891f](https://github.com/whisper-money/whisper-money/commit/b4b891f204a7bf8fe1f1b9c036cfee6052a18bd4))
|
||||
* **encrypted-text:** Add animation and random character generation ([7d8474f](https://github.com/whisper-money/whisper-money/commit/7d8474f6b81f032ac4585fceb293c9d5e6e5594d))
|
||||
* **encrypted-text:** Improve encryption UI with dynamic masking and loading state ([ff186a4](https://github.com/whisper-money/whisper-money/commit/ff186a4887c715b10205508d41f453df90201b26))
|
||||
* Implement drip email campaign system ([#35](https://github.com/whisper-money/whisper-money/issues/35)) ([46c5b13](https://github.com/whisper-money/whisper-money/commit/46c5b137392a333c98ebcb6d3435556b52a18994))
|
||||
* **import-transactions-drawer:** Add json-logic-js dependency and improve import logic ([1df3bad](https://github.com/whisper-money/whisper-money/commit/1df3bad3c3d27e4fe224277c4aedb8872fb6ba25))
|
||||
* **lucide-react:** Add custom icons to Toaster component ([573b2fd](https://github.com/whisper-money/whisper-money/commit/573b2fdb0a13cd2c2064996c8660a98ed97a60c2))
|
||||
* **queue:** Implement queueable email jobs with rate limiting ([3d0d6c8](https://github.com/whisper-money/whisper-money/commit/3d0d6c8bef11e06e3a39b7a8e9dbc4fb166657e7))
|
||||
* **react:** add authentication check in SyncProvider ([48bce81](https://github.com/whisper-money/whisper-money/commit/48bce81d9a23f894008bdfaa9c6876431f0c293e))
|
||||
* Remove console.log and add padding to components ([c1f99fe](https://github.com/whisper-money/whisper-money/commit/c1f99fedd6255621e3c9a301d79bbe3968908aea))
|
||||
* Replace Input with Textarea for editable descriptions ([2b6acf4](https://github.com/whisper-money/whisper-money/commit/2b6acf49d8770c74538e0f8664d9e88b4ae0b63e))
|
||||
* **settings:** Update account management UI and add sync functionality ([ab63edd](https://github.com/whisper-money/whisper-money/commit/ab63edde2b23f1a9055fcce7b456a4825251cebb))
|
||||
* **shared:** Add CategoryCombobox component ([57879bb](https://github.com/whisper-money/whisper-money/commit/57879bb7118850ae03ed2059dc5b775c29f5885d))
|
||||
* **sync:** Add sync functionality for accounts, banks, categories, and status button ([9256148](https://github.com/whisper-money/whisper-money/commit/9256148961201ba52fe93d29517fb6c0dbf24147))
|
||||
* **traefik:** Add secure headers middleware to WhisperMoney service ([242be5f](https://github.com/whisper-money/whisper-money/commit/242be5f415be11696fafdf4db68f4dafae964c66))
|
||||
* **TransactionController:** Add store method for creating transactions ([c1fbd4d](https://github.com/whisper-money/whisper-money/commit/c1fbd4d09fe67a092ad45e49b97ce7a172cf9913))
|
||||
* **TransactionSyncController:** Sort transactions by transaction_date and updated_at ([41f5c64](https://github.com/whisper-money/whisper-money/commit/41f5c6485c11934e69c6efab2868ea541e2856d4))
|
||||
* **ui:** Implement virtual scrolling for DataTable component ([07ca633](https://github.com/whisper-money/whisper-money/commit/07ca63347e9bae5bc59b8f0f8073e64da1df68f4))
|
||||
* **ui:** Improve chart tooltip content rendering and calculation ([d04b6a0](https://github.com/whisper-money/whisper-money/commit/d04b6a0174910f5e8eb4dce491805e60d7e67c04))
|
||||
* update date formatting logic in transaction components ([d13ecc2](https://github.com/whisper-money/whisper-money/commit/d13ecc2722509501d018b27a3b4dd83e7ab4351b))
|
||||
* Update encryption key button icon based on state ([08baf3b](https://github.com/whisper-money/whisper-money/commit/08baf3b19a8d4a631d2942a31e47071be68a128c))
|
||||
* Update ProfileController to include two-factor authentication settings ([e21c9cc](https://github.com/whisper-money/whisper-money/commit/e21c9cc3a89fdb8ac84bea49e4a1f6963ab7542e))
|
||||
* Update welcome page title to focus on understanding finances ([3ac7102](https://github.com/whisper-money/whisper-money/commit/3ac71025013ed1c8da713c753b9ef2bd3e050eee))
|
||||
* **use-dashboard-data:** Add conditional formatting for current year dates ([525e770](https://github.com/whisper-money/whisper-money/commit/525e7709cc8c92f90ece1bfce572e8434de60b15))
|
||||
* **welcome:** Add GitHub link and refactor auth buttons ([2ab362d](https://github.com/whisper-money/whisper-money/commit/2ab362dc5db7fa14104232cce283e53f5b658761))
|
||||
|
||||
|
||||
### Reverts
|
||||
|
||||
- Revert "swap horizon -> queue:work on mysql" ([03880ca](https://github.com/whisper-money/whisper-money/commit/03880ca4920eba081d33147ceedd982f81c1a65b))
|
||||
* Revert "swap horizon -> queue:work on mysql" ([03880ca](https://github.com/whisper-money/whisper-money/commit/03880ca4920eba081d33147ceedd982f81c1a65b))
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
|
|
|
|||
573
CLAUDE.md
573
CLAUDE.md
|
|
@ -4,27 +4,17 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
|
|||
|
||||
## Project Overview
|
||||
|
||||
Whisper Money is a privacy-first personal finance app. Your data is never shared with third parties—you are the owner. It uses Laravel 12 (PHP 8.4) backend with React 19 frontend via Inertia.js v2.
|
||||
Whisper Money is a privacy-first personal finance app with end-to-end encryption. It uses Laravel 12 (PHP 8.4) backend with React 19 frontend via Inertia.js v2.
|
||||
|
||||
## Commands
|
||||
|
||||
### Development
|
||||
|
||||
```bash
|
||||
composer run dev # Start full dev environment (PHP server, queue, Vite, logs)
|
||||
bun run dev # Vite dev server only
|
||||
```
|
||||
|
||||
### Running the server for QA
|
||||
|
||||
When a command or skill needs the user to open the app in Chrome to review a change:
|
||||
|
||||
1. **First run in a worktree?** Prepare it: `./worktree.sh /path/to/main/repo` (copies `.env` + `storage/keys`, installs `bun` and `composer` deps). Skip if already set up.
|
||||
2. **Start the server** if it isn't running: `composer run dev`.
|
||||
3. **Get the URL** — it's dynamic per worktree, so don't guess it. On startup `composer run dev` prints and copies it to the clipboard (`✓ <URL> copied to clipboard`); read it from the `server` pane output. Ask the user to open that URL in Chrome to QA.
|
||||
|
||||
### Build & Quality
|
||||
|
||||
```bash
|
||||
bun run build # Production build (don't run automatically - ask user)
|
||||
bun run format # Format code with Prettier
|
||||
|
|
@ -33,15 +23,13 @@ vendor/bin/pint --dirty # PHP code formatting
|
|||
```
|
||||
|
||||
### Testing
|
||||
|
||||
```bash
|
||||
php artisan test --exclue-testsuite=Browser # Run all tests
|
||||
php artisan test # Run all tests
|
||||
php artisan test tests/Feature/ExampleTest.php # Run specific file
|
||||
php artisan test --filter=testName # Filter by test name
|
||||
php artisan test --filter=testName # Filter by test name
|
||||
```
|
||||
|
||||
### CI Requirements (must pass before finalizing changes)
|
||||
|
||||
```bash
|
||||
bun install --frozen-lockfile
|
||||
composer install --no-interaction --prefer-dist --optimize-autoloader
|
||||
|
|
@ -55,21 +43,18 @@ bun run lint
|
|||
## Architecture
|
||||
|
||||
### Backend (Laravel 12)
|
||||
|
||||
- **Streamlined structure**: No middleware files in `app/Http/Middleware/`, configuration in `bootstrap/app.php`
|
||||
- **Commands auto-register**: Files in `app/Console/Commands/` are automatically available
|
||||
- **Form Requests**: Always use for validation instead of inline controller validation
|
||||
- **Eloquent**: Prefer `Model::query()` over `DB::`, use eager loading to prevent N+1
|
||||
|
||||
### Frontend (React 19 + Inertia v2)
|
||||
|
||||
- **Pages**: `resources/js/pages/` - Inertia page components
|
||||
- **Components**: `resources/js/components/` - Reusable UI components
|
||||
- **Services**: `resources/js/services/` - Sync services with IndexedDB (Dexie) for offline support
|
||||
- **Wayfinder**: Type-safe routes generated in `resources/js/routes/` and `resources/js/actions/`
|
||||
|
||||
### Wayfinder Usage
|
||||
|
||||
```typescript
|
||||
// Import controller methods (tree-shakable)
|
||||
import { show, store } from '@/actions/App/Http/Controllers/PostController'
|
||||
|
|
@ -82,7 +67,6 @@ show.url(1) // "/posts/1"
|
|||
```
|
||||
|
||||
### Navigation
|
||||
|
||||
- Use `router.visit()` from `@inertiajs/react` or `<Link>` component
|
||||
- Never use traditional `<a>` tags for internal navigation
|
||||
|
||||
|
|
@ -98,7 +82,7 @@ show.url(1) // "/posts/1"
|
|||
|
||||
- Eloquent relationships with return type hints
|
||||
- User model uses UUID primary keys
|
||||
- Privacy-focused: no data shared with third parties
|
||||
- End-to-end encryption for sensitive financial data
|
||||
|
||||
===
|
||||
|
||||
|
|
@ -107,273 +91,542 @@ show.url(1) // "/posts/1"
|
|||
|
||||
# Laravel Boost Guidelines
|
||||
|
||||
The Laravel Boost guidelines are specifically curated by Laravel maintainers for this application. These guidelines should be followed closely to ensure the best experience when building Laravel applications.
|
||||
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.4
|
||||
- inertiajs/inertia-laravel (INERTIA_LARAVEL) - v2
|
||||
- laravel/ai (AI) - v0
|
||||
- php - 8.4.1
|
||||
- inertiajs/inertia-laravel (INERTIA) - v2
|
||||
- laravel/cashier (CASHIER) - v16
|
||||
- laravel/fortify (FORTIFY) - v1
|
||||
- laravel/framework (LARAVEL) - v13
|
||||
- laravel/pennant (PENNANT) - v1
|
||||
- laravel/framework (LARAVEL) - v12
|
||||
- laravel/prompts (PROMPTS) - v0
|
||||
- laravel/wayfinder (WAYFINDER) - v0
|
||||
- larastan/larastan (LARASTAN) - v3
|
||||
- laravel/boost (BOOST) - v2
|
||||
- laravel/mcp (MCP) - v0
|
||||
- laravel/pail (PAIL) - v1
|
||||
- laravel/pint (PINT) - v1
|
||||
- laravel/sail (SAIL) - v1
|
||||
- pestphp/pest (PEST) - v4
|
||||
- phpunit/phpunit (PHPUNIT) - v12
|
||||
- @inertiajs/react (INERTIA_REACT) - v2
|
||||
- @inertiajs/react (INERTIA) - v2
|
||||
- react (REACT) - v19
|
||||
- tailwindcss (TAILWINDCSS) - v4
|
||||
- @laravel/vite-plugin-wayfinder (WAYFINDER_VITE) - v0
|
||||
- @laravel/vite-plugin-wayfinder (WAYFINDER) - v0
|
||||
- eslint (ESLINT) - v9
|
||||
- prettier (PRETTIER) - v3
|
||||
|
||||
## Skills Activation
|
||||
|
||||
This project has domain-specific skills available. You MUST activate the relevant skill whenever you work in that domain—don't wait until you're stuck.
|
||||
|
||||
- `cashier-stripe-development` — Handles Laravel Cashier Stripe integration including subscriptions, webhooks, Stripe Checkout, invoices, charges, refunds, trials, coupons, metered billing, and payment failure handling. Triggered when a user mentions Cashier, Billable, IncompletePayment, stripe_id, newSubscription, Stripe subscriptions, or billing. Also applies when setting up webhooks, handling SCA/3DS payment failures, testing with Stripe test cards, or troubleshooting incomplete subscriptions, CSRF webhook errors, or migration publish issues.
|
||||
- `pennant-development` — Use when working with Laravel Pennant the official Laravel feature flag package. Trigger whenever the query mentions Pennant by name or involves feature flags or feature toggles in a Laravel project. Tasks include defining feature flags checking whether features are active creating class based features in `app/Features` using Blade `@feature` directives scoping flags to users or teams building custom Pennant storage drivers protecting routes with feature flags testing feature flags with Pest or PHPUnit and implementing A B testing or gradual rollouts with feature flags. Do not trigger for generic Laravel configuration authorization policies authentication or non Pennant feature management systems.
|
||||
- `wayfinder-development` — Activates whenever referencing backend routes in frontend components. Use when importing from @/actions or @/routes, calling Laravel routes from TypeScript, or working with Wayfinder route functions.
|
||||
- `pest-testing` — Use this skill for Pest PHP testing in Laravel projects only. Trigger whenever any test is being written, edited, fixed, or refactored — including fixing tests that broke after a code change, adding assertions, converting PHPUnit to Pest, adding datasets, and TDD workflows. Always activate when the user asks how to write something in Pest, mentions test files or directories (tests/Feature, tests/Unit, tests/Browser), or needs browser testing, smoke testing multiple pages for JS errors, or architecture tests. Covers: it()/expect() syntax, datasets, mocking, browser testing (visit/click/fill), smoke testing, arch(), Livewire component tests, RefreshDatabase, and all Pest 4 features. Do not use for factories, seeders, migrations, controllers, models, or non-test PHP code.
|
||||
- `inertia-react-development` — Develops Inertia.js v2 React client-side applications. Activates when creating React pages, forms, or navigation; using <Link>, <Form>, useForm, or router; working with deferred props, prefetching, or polling; or when user mentions React with Inertia, React pages, React forms, or React navigation.
|
||||
- `tailwindcss-development` — Always invoke when the user's message includes 'tailwind' in any form. Also invoke for: building responsive grid layouts (multi-column card grids, product grids), flex/grid page structures (dashboards with sidebars, fixed topbars, mobile-toggle navs), styling UI components (cards, tables, navbars, pricing sections, forms, inputs, badges), adding dark mode variants, fixing spacing or typography, and Tailwind v3/v4 work. The core use case: writing or fixing Tailwind utility classes in HTML templates (Blade, JSX, Vue). Skip for backend PHP logic, database queries, API routes, JavaScript with no HTML/CSS component, CSS file audits, build tool configuration, and vanilla CSS.
|
||||
- `ai-sdk-development` — TRIGGER when working with ai-sdk which is Laravel official first-party AI SDK. Activate when building, editing AI agents, chatbots, text generation, image generation, audio/TTS, transcription/STT, embeddings, RAG, vector stores, reranking, structured output, streaming, conversation memory, tools, queueing, broadcasting, and provider failover across OpenAI, Anthropic, Gemini, Azure, Groq, xAI, DeepSeek, Mistral, Ollama, ElevenLabs, Cohere, Jina, and VoyageAI. Invoke when the user references ai-sdk, the `Laravel\Ai\` namespace, or this project's AI features — not for other AI packages used directly.
|
||||
- `fortify-development` — Laravel Fortify headless authentication backend development. Activate when implementing authentication features including login, registration, password reset, email verification, two-factor authentication (2FA/TOTP), profile updates, headless auth, authentication scaffolding, or auth guards in Laravel applications.
|
||||
|
||||
## 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, and naming.
|
||||
- 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 they work. Unit and feature tests are more important.
|
||||
- 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.
|
||||
- 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.
|
||||
|
||||
## Documentation Files
|
||||
## 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.
|
||||
|
||||
## Replies
|
||||
|
||||
- Be concise in your explanations - focus on what's important rather than explaining obvious details.
|
||||
|
||||
=== boost rules ===
|
||||
|
||||
# Laravel Boost
|
||||
|
||||
## Laravel Boost
|
||||
- Laravel Boost is an MCP server that comes with powerful tools designed specifically for this application. Use them.
|
||||
|
||||
## Artisan Commands
|
||||
|
||||
- Run Artisan commands directly via the command line (e.g., `php artisan route:list`, `php artisan tinker --execute "..."`).
|
||||
- Use `php artisan list` to discover available commands and `php artisan [command] --help` to check parameters.
|
||||
## 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.
|
||||
|
||||
- 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.
|
||||
|
||||
## Debugging
|
||||
|
||||
## 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.
|
||||
- Use the `database-schema` tool to inspect table structure before writing migrations or models.
|
||||
- To execute PHP code for debugging, run `php artisan tinker --execute "your code here"` directly.
|
||||
- To read configuration values, read the config files directly or run `php artisan config:show [key]`.
|
||||
- To inspect routes, run `php artisan route:list` directly.
|
||||
- To check environment variables, read the `.env` file directly.
|
||||
|
||||
## 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 trying other approaches when working with Laravel or Laravel ecosystem packages. This tool automatically passes a list of installed packages and their versions to the remote Boost API, so it returns only version-specific documentation for the user's circumstance. You should pass an array of packages to filter on if you know you need docs for particular packages.
|
||||
- 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 at once. For example: `['rate limiting', 'routing rate limiting', 'routing']`. The most relevant results will be returned first.
|
||||
- Do not add package names to queries; package information is already shared. For example, use `test resource table`, not `filament 4 test resource table`.
|
||||
- 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
|
||||
|
||||
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
|
||||
## PHP
|
||||
|
||||
- Always use curly braces for control structures, even for single-line bodies.
|
||||
|
||||
## Constructors
|
||||
- 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 unless the constructor is private.
|
||||
|
||||
## Type Declarations
|
||||
- <code-snippet>public function __construct(public GitHub $github) { }</code-snippet>
|
||||
- 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.
|
||||
|
||||
<!-- Explicit Return Types and Method Params -->
|
||||
```php
|
||||
<code-snippet name="Explicit Return Types and Method Params" lang="php">
|
||||
protected function isAccessible(User $user, ?string $path = null): bool
|
||||
{
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
## Enums
|
||||
|
||||
- Typically, keys in an Enum should be TitleCase. For example: `FavoritePerson`, `BestLake`, `Monthly`.
|
||||
</code-snippet>
|
||||
|
||||
## Comments
|
||||
|
||||
- Prefer PHPDoc blocks over inline comments. Never use comments within the code itself unless the logic is exceptionally complex.
|
||||
- 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.
|
||||
|
||||
- Add useful array shape type definitions when appropriate.
|
||||
## Enums
|
||||
- Typically, keys in an Enum should be TitleCase. For example: `FavoritePerson`, `BestLake`, `Monthly`.
|
||||
|
||||
=== tests rules ===
|
||||
|
||||
# Test Enforcement
|
||||
|
||||
- Every change must be programmatically tested. Write a new test or update an existing test, then run the affected tests to make sure they pass.
|
||||
- Run the minimum number of tests needed to ensure code quality and speed. Use `php artisan test --compact` with a specific filename or filter.
|
||||
|
||||
=== inertia-laravel/core rules ===
|
||||
|
||||
# Inertia
|
||||
## Inertia Core
|
||||
|
||||
- Inertia creates fully client-side rendered SPAs without modern SPA complexity, leveraging existing server-side patterns.
|
||||
- Components live in `resources/js/pages` (unless specified in `vite.config.js`). Use `Inertia::render()` for server-side routing instead of Blade views.
|
||||
- ALWAYS use `search-docs` tool for version-specific Inertia documentation and updated code examples.
|
||||
- IMPORTANT: Activate `inertia-react-development` when working with Inertia client-side patterns.
|
||||
- Inertia.js components should be placed in the `resources/js/Pages` directory unless specified differently in the JS bundler (vite.config.js).
|
||||
- Use `Inertia::render()` for server-side routing instead of traditional Blade views.
|
||||
- Use `search-docs` for accurate guidance on all things Inertia.
|
||||
|
||||
# Inertia v2
|
||||
<code-snippet lang="php" name="Inertia::render Example">
|
||||
// routes/web.php example
|
||||
Route::get('/users', function () {
|
||||
return Inertia::render('Users/Index', [
|
||||
'users' => User::all()
|
||||
]);
|
||||
});
|
||||
</code-snippet>
|
||||
|
||||
|
||||
=== inertia-laravel/v2 rules ===
|
||||
|
||||
## Inertia v2
|
||||
|
||||
- Make use of all Inertia features from v1 & v2. Check the documentation before making any changes to ensure we are taking the correct approach.
|
||||
|
||||
### Inertia v2 New Features
|
||||
- Polling
|
||||
- Prefetching
|
||||
- Deferred props
|
||||
- Infinite scrolling using merging props and `WhenVisible`
|
||||
- Lazy loading data on scroll
|
||||
|
||||
### Deferred Props & Empty States
|
||||
- When using deferred props on the frontend, you should add a nice empty state with pulsing / animated skeleton.
|
||||
|
||||
### Inertia Form General Guidance
|
||||
- The recommended way to build forms when using Inertia is with the `<Form>` component - a useful example is below. Use `search-docs` with a query of `form component` for guidance.
|
||||
- Forms can also be built using the `useForm` helper for more programmatic control, or to follow existing conventions. Use `search-docs` with a query of `useForm helper` for guidance.
|
||||
- `resetOnError`, `resetOnSuccess`, and `setDefaultsOnSuccess` are available on the `<Form>` component. Use `search-docs` with a query of 'form component resetting' for guidance.
|
||||
|
||||
- Use all Inertia features from v1 and v2. Check the documentation before making changes to ensure the correct approach.
|
||||
- New features: deferred props, infinite scroll, merging props, polling, prefetching, once props, flash data.
|
||||
- When using deferred props, add an empty state with a pulsing or animated skeleton.
|
||||
|
||||
=== laravel/core rules ===
|
||||
|
||||
# Do Things the Laravel Way
|
||||
## 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 `php artisan list` and check their parameters with `php artisan [command] --help`.
|
||||
- If you're creating a generic PHP class, use `php artisan make:class`.
|
||||
- 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
|
||||
|
||||
### 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.
|
||||
- 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 `php artisan make:model --help` to check the available options.
|
||||
- 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
|
||||
|
||||
### 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.
|
||||
|
||||
## 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.
|
||||
|
||||
## Queues
|
||||
|
||||
### Queues
|
||||
- Use queued jobs for time-consuming operations with the `ShouldQueue` interface.
|
||||
|
||||
## Configuration
|
||||
### 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
|
||||
|
||||
### 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] {name}` to create a feature test, and pass `--unit` to create a unit test. Most tests should be feature tests.
|
||||
|
||||
## Vite Error
|
||||
- When creating tests, make use of `php artisan make:test [options] <name>` 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/v12 rules ===
|
||||
|
||||
## Laravel 12
|
||||
|
||||
- Use the `search-docs` tool to get version specific documentation.
|
||||
- Since Laravel 11, Laravel has a new streamlined file structure which this project uses.
|
||||
|
||||
### Laravel 12 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.
|
||||
|
||||
|
||||
=== wayfinder/core rules ===
|
||||
|
||||
# Laravel Wayfinder
|
||||
## Laravel Wayfinder
|
||||
|
||||
Wayfinder generates TypeScript functions for Laravel routes. Import from `@/actions/` (controllers) or `@/routes/` (named routes).
|
||||
Wayfinder generates TypeScript functions and types for Laravel controllers and routes which you can import into your client side code. It provides type safety and automatic synchronization between backend routes and frontend code.
|
||||
|
||||
### Development Guidelines
|
||||
- Always use `search-docs` to check wayfinder correct usage before implementing any features.
|
||||
- Always Prefer named imports for tree-shaking (e.g., `import { show } from '@/actions/...'`)
|
||||
- Avoid default controller imports (prevents tree-shaking)
|
||||
- Run `wayfinder:generate` after route changes if Vite plugin isn't installed
|
||||
|
||||
### Feature Overview
|
||||
- Form Support: Use `.form()` with `--with-form` flag for HTML form attributes — `<form {...store.form()}>` → `action="/posts" method="post"`
|
||||
- HTTP Methods: Call `.get()`, `.post()`, `.patch()`, `.put()`, `.delete()` for specific methods — `show.head(1)` → `{ url: "/posts/1", method: "head" }`
|
||||
- Invokable Controllers: Import and invoke directly as functions. For example, `import StorePost from '@/actions/.../StorePostController'; StorePost()`
|
||||
- Named Routes: Import from `@/routes/` for non-controller routes. For example, `import { show } from '@/routes/post'; show(1)` for route name `post.show`
|
||||
- Parameter Binding: Detects route keys (e.g., `{post:slug}`) and accepts matching object properties — `show("my-post")` or `show({ slug: "my-post" })`
|
||||
- Query Merging: Use `mergeQuery` to merge with `window.location.search`, set values to `null` to remove — `show(1, { mergeQuery: { page: 2, sort: null } })`
|
||||
- Query Parameters: Pass `{ query: {...} }` in options to append params — `show(1, { query: { page: 1 } })` → `"/posts/1?page=1"`
|
||||
- Route Objects: Functions return `{ url, method }` shaped objects — `show(1)` → `{ url: "/posts/1", method: "get" }`
|
||||
- URL Extraction: Use `.url()` to get URL string — `show.url(1)` → `"/posts/1"`
|
||||
|
||||
### Example Usage
|
||||
|
||||
<code-snippet name="Wayfinder Basic Usage" lang="typescript">
|
||||
// Import controller methods (tree-shakable)
|
||||
import { show, store, update } from '@/actions/App/Http/Controllers/PostController'
|
||||
|
||||
// Get route object with URL and method...
|
||||
show(1) // { url: "/posts/1", method: "get" }
|
||||
|
||||
// Get just the URL...
|
||||
show.url(1) // "/posts/1"
|
||||
|
||||
// Use specific HTTP methods...
|
||||
show.get(1) // { url: "/posts/1", method: "get" }
|
||||
show.head(1) // { url: "/posts/1", method: "head" }
|
||||
|
||||
// Import named routes...
|
||||
import { show as postShow } from '@/routes/post' // For route name 'post.show'
|
||||
postShow(1) // { url: "/posts/1", method: "get" }
|
||||
</code-snippet>
|
||||
|
||||
|
||||
### Wayfinder + Inertia
|
||||
If your application uses the `<Form>` component from Inertia, you can use Wayfinder to generate form action and method automatically.
|
||||
<code-snippet name="Wayfinder Form Component (React)" lang="typescript">
|
||||
|
||||
<Form {...store.form()}><input name="title" /></Form>
|
||||
|
||||
</code-snippet>
|
||||
|
||||
- IMPORTANT: Activate `wayfinder-development` skill whenever referencing backend routes in frontend components.
|
||||
- Invokable Controllers: `import StorePost from '@/actions/.../StorePostController'; StorePost()`.
|
||||
- Parameter Binding: Detects route keys (`{post:slug}`) — `show({ slug: "my-post" })`.
|
||||
- Query Merging: `show(1, { mergeQuery: { page: 2, sort: null } })` merges with current URL, `null` removes params.
|
||||
- Inertia: Use `.form()` with `<Form>` component or `form.submit(store())` with useForm.
|
||||
|
||||
=== pint/core rules ===
|
||||
|
||||
# Laravel Pint Code Formatter
|
||||
## 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.
|
||||
|
||||
- If you have modified any PHP files, you must run `vendor/bin/pint --dirty --format agent` before finalizing changes to ensure your code matches the project's expected style.
|
||||
- Do not run `vendor/bin/pint --test --format agent`, simply run `vendor/bin/pint --format agent` to fix any formatting issues.
|
||||
|
||||
=== pest/core rules ===
|
||||
|
||||
## Pest
|
||||
|
||||
- This project uses Pest for testing. Create tests: `php artisan make:test --pest {name}`.
|
||||
- Run tests: `php artisan test --compact` or filter: `php artisan test --compact --filter=testName`.
|
||||
- Do NOT delete tests without approval.
|
||||
### Testing
|
||||
- If you need to verify a feature is working, write or update a Unit / Feature test.
|
||||
|
||||
### Pest Tests
|
||||
- All tests must be written using Pest. Use `php artisan make:test --pest <name>`.
|
||||
- You must not remove any tests or test files from the tests directory without approval. These are not temporary or helper files - these are core to the application.
|
||||
- Tests should test all of the happy paths, failure paths, and weird paths.
|
||||
- Tests live in the `tests/Feature` and `tests/Unit` directories.
|
||||
- Pest tests look and behave like this:
|
||||
<code-snippet name="Basic Pest Test Example" lang="php">
|
||||
it('is true', function () {
|
||||
expect(true)->toBeTrue();
|
||||
});
|
||||
</code-snippet>
|
||||
|
||||
### Running Tests
|
||||
- Run the minimal number of tests using an appropriate filter before finalizing code edits.
|
||||
- To run all tests: `php artisan test`.
|
||||
- To run all tests in a file: `php artisan test tests/Feature/ExampleTest.php`.
|
||||
- To filter on a particular test name: `php artisan test --filter=testName` (recommended after making a change to a related file).
|
||||
- When the tests relating to your changes are passing, ask the user if they would like to run the entire test suite to ensure everything is still passing.
|
||||
|
||||
### Pest Assertions
|
||||
- When asserting status codes on a response, use the specific method like `assertForbidden` and `assertNotFound` instead of using `assertStatus(403)` or similar, e.g.:
|
||||
<code-snippet name="Pest Example Asserting postJson Response" lang="php">
|
||||
it('returns all', function () {
|
||||
$response = $this->postJson('/api/docs', []);
|
||||
|
||||
$response->assertSuccessful();
|
||||
});
|
||||
</code-snippet>
|
||||
|
||||
### Mocking
|
||||
- Mocking can be very helpful when appropriate.
|
||||
- When mocking, you can use the `Pest\Laravel\mock` Pest function, but always import it via `use function Pest\Laravel\mock;` before using it. Alternatively, you can use `$this->mock()` if existing tests do.
|
||||
- You can also create partial mocks using the same import or self method.
|
||||
|
||||
### Datasets
|
||||
- Use datasets in Pest to simplify tests which have a lot of duplicated data. This is often the case when testing validation rules, so consider going with this solution when writing tests for validation rules.
|
||||
|
||||
<code-snippet name="Pest Dataset Example" lang="php">
|
||||
it('has emails', function (string $email) {
|
||||
expect($email)->not->toBeEmpty();
|
||||
})->with([
|
||||
'james' => 'james@laravel.com',
|
||||
'taylor' => 'taylor@laravel.com',
|
||||
]);
|
||||
</code-snippet>
|
||||
|
||||
|
||||
=== pest/v4 rules ===
|
||||
|
||||
## Pest 4
|
||||
|
||||
- Pest v4 is a huge upgrade to Pest and offers: browser testing, smoke testing, visual regression testing, test sharding, and faster type coverage.
|
||||
- Browser testing is incredibly powerful and useful for this project.
|
||||
- Browser tests should live in `tests/Browser/`.
|
||||
- Use the `search-docs` tool for detailed guidance on utilizing these features.
|
||||
|
||||
### Browser Testing
|
||||
- You can use Laravel features like `Event::fake()`, `assertAuthenticated()`, and model factories within Pest v4 browser tests, as well as `RefreshDatabase` (when needed) to ensure a clean state for each test.
|
||||
- Interact with the page (click, type, scroll, select, submit, drag-and-drop, touch gestures, etc.) when appropriate to complete the test.
|
||||
- If requested, test on multiple browsers (Chrome, Firefox, Safari).
|
||||
- If requested, test on different devices and viewports (like iPhone 14 Pro, tablets, or custom breakpoints).
|
||||
- Switch color schemes (light/dark mode) when appropriate.
|
||||
- Take screenshots or pause tests for debugging when appropriate.
|
||||
|
||||
### Example Tests
|
||||
|
||||
<code-snippet name="Pest Browser Test Example" lang="php">
|
||||
it('may reset the password', function () {
|
||||
Notification::fake();
|
||||
|
||||
$this->actingAs(User::factory()->create());
|
||||
|
||||
$page = visit('/sign-in'); // Visit on a real browser...
|
||||
|
||||
$page->assertSee('Sign In')
|
||||
->assertNoJavascriptErrors() // or ->assertNoConsoleLogs()
|
||||
->click('Forgot Password?')
|
||||
->fill('email', 'nuno@laravel.com')
|
||||
->click('Send Reset Link')
|
||||
->assertSee('We have emailed your password reset link!')
|
||||
|
||||
Notification::assertSent(ResetPassword::class);
|
||||
});
|
||||
</code-snippet>
|
||||
|
||||
<code-snippet name="Pest Smoke Testing Example" lang="php">
|
||||
$pages = visit(['/', '/about', '/contact']);
|
||||
|
||||
$pages->assertNoJavascriptErrors()->assertNoConsoleLogs();
|
||||
</code-snippet>
|
||||
|
||||
|
||||
=== inertia-react/core rules ===
|
||||
|
||||
# Inertia + React
|
||||
## Inertia + React
|
||||
|
||||
- Use `router.visit()` or `<Link>` for navigation instead of traditional links.
|
||||
|
||||
<code-snippet name="Inertia Client Navigation" lang="react">
|
||||
|
||||
import { Link } from '@inertiajs/react'
|
||||
<Link href="/">Home</Link>
|
||||
|
||||
</code-snippet>
|
||||
|
||||
|
||||
=== inertia-react/v2/forms rules ===
|
||||
|
||||
## Inertia + React Forms
|
||||
|
||||
<code-snippet name="`<Form>` Component Example" lang="react">
|
||||
|
||||
import { Form } from '@inertiajs/react'
|
||||
|
||||
export default () => (
|
||||
<Form action="/users" method="post">
|
||||
{({
|
||||
errors,
|
||||
hasErrors,
|
||||
processing,
|
||||
wasSuccessful,
|
||||
recentlySuccessful,
|
||||
clearErrors,
|
||||
resetAndClearErrors,
|
||||
defaults
|
||||
}) => (
|
||||
<>
|
||||
<input type="text" name="name" />
|
||||
|
||||
{errors.name && <div>{errors.name}</div>}
|
||||
|
||||
<button type="submit" disabled={processing}>
|
||||
{processing ? 'Creating...' : 'Create User'}
|
||||
</button>
|
||||
|
||||
{wasSuccessful && <div>User created successfully!</div>}
|
||||
</>
|
||||
)}
|
||||
</Form>
|
||||
)
|
||||
|
||||
</code-snippet>
|
||||
|
||||
|
||||
=== 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.
|
||||
|
||||
<code-snippet name="Valid Flex Gap Spacing Example" lang="html">
|
||||
<div class="flex gap-8">
|
||||
<div>Superior</div>
|
||||
<div>Michigan</div>
|
||||
<div>Erie</div>
|
||||
</div>
|
||||
</code-snippet>
|
||||
|
||||
|
||||
### 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/v4 rules ===
|
||||
|
||||
## Tailwind 4
|
||||
|
||||
- Always use Tailwind CSS v4 - do not use the deprecated utilities.
|
||||
- `corePlugins` is not supported in Tailwind v4.
|
||||
- In Tailwind v4, configuration is CSS-first using the `@theme` directive — no separate `tailwind.config.js` file is needed.
|
||||
<code-snippet name="Extending Theme in CSS" lang="css">
|
||||
@theme {
|
||||
--color-brand: oklch(0.72 0.11 178);
|
||||
}
|
||||
</code-snippet>
|
||||
|
||||
- In Tailwind v4, you import Tailwind using a regular CSS `@import` statement, not using the `@tailwind` directives used in v3:
|
||||
|
||||
<code-snippet name="Tailwind v4 Import Tailwind Diff" lang="diff">
|
||||
- @tailwind base;
|
||||
- @tailwind components;
|
||||
- @tailwind utilities;
|
||||
+ @import "tailwindcss";
|
||||
</code-snippet>
|
||||
|
||||
|
||||
### Replaced Utilities
|
||||
- Tailwind v4 removed deprecated utilities. Do not use the deprecated option - use the replacement.
|
||||
- Opacity values are still numeric.
|
||||
|
||||
| Deprecated | Replacement |
|
||||
|------------+--------------|
|
||||
| bg-opacity-* | bg-black/* |
|
||||
| text-opacity-* | text-black/* |
|
||||
| border-opacity-* | border-black/* |
|
||||
| divide-opacity-* | divide-black/* |
|
||||
| ring-opacity-* | ring-black/* |
|
||||
| placeholder-opacity-* | placeholder-black/* |
|
||||
| flex-shrink-* | shrink-* |
|
||||
| flex-grow-* | grow-* |
|
||||
| overflow-ellipsis | text-ellipsis |
|
||||
| decoration-slice | box-decoration-slice |
|
||||
| decoration-clone | box-decoration-clone |
|
||||
|
||||
|
||||
=== tests rules ===
|
||||
|
||||
## Test Enforcement
|
||||
|
||||
- Every change must be programmatically tested. Write a new test or update an existing test, then run the affected tests to make sure they pass.
|
||||
- Run the minimum number of tests needed to ensure code quality and speed. Use `php artisan test` with a specific filename or filter.
|
||||
|
||||
- IMPORTANT: Activate `inertia-react-development` when working with Inertia React client-side patterns.
|
||||
|
||||
=== laravel/fortify rules ===
|
||||
|
||||
# Laravel Fortify
|
||||
## Laravel Fortify
|
||||
|
||||
- Fortify is a headless authentication backend that provides authentication routes and controllers for Laravel applications.
|
||||
- IMPORTANT: Always use the `search-docs` tool for detailed Laravel Fortify patterns and documentation.
|
||||
- IMPORTANT: Activate `developing-with-fortify` skill when working with Fortify authentication features.
|
||||
Fortify is a headless authentication backend that provides authentication routes and controllers for Laravel applications.
|
||||
|
||||
**Before implementing any authentication features, use the `search-docs` tool to get the latest docs for that specific feature.**
|
||||
|
||||
### Configuration & Setup
|
||||
- Check `config/fortify.php` to see what's enabled. Use `search-docs` for detailed information on specific features.
|
||||
- Enable features by adding them to the `'features' => []` array: `Features::registration()`, `Features::resetPasswords()`, etc.
|
||||
- To see the all Fortify registered routes, use the `list-routes` tool with the `only_vendor: true` and `action: "Fortify"` parameters.
|
||||
- Fortify includes view routes by default (login, register). Set `'views' => false` in the configuration file to disable them if you're handling views yourself.
|
||||
|
||||
### Customization
|
||||
- Views can be customized in `FortifyServiceProvider`'s `boot()` method using `Fortify::loginView()`, `Fortify::registerView()`, etc.
|
||||
- Customize authentication logic with `Fortify::authenticateUsing()` for custom user retrieval / validation.
|
||||
- Actions in `app/Actions/Fortify/` handle business logic (user creation, password reset, etc.). They're fully customizable, so you can modify them to change feature behavior.
|
||||
|
||||
## Available Features
|
||||
- `Features::registration()` for user registration.
|
||||
- `Features::emailVerification()` to verify new user emails.
|
||||
- `Features::twoFactorAuthentication()` for 2FA with QR codes and recovery codes.
|
||||
- Add options: `['confirmPassword' => true, 'confirm' => true]` to require password confirmation and OTP confirmation before enabling 2FA.
|
||||
- `Features::updateProfileInformation()` to let users update their profile.
|
||||
- `Features::updatePasswords()` to let users change their passwords.
|
||||
- `Features::resetPasswords()` for password reset via email.
|
||||
</laravel-boost-guidelines>
|
||||
|
|
|
|||
70
Dockerfile
70
Dockerfile
|
|
@ -1,5 +1,7 @@
|
|||
FROM php:8.4-cli
|
||||
# Use the official PHP 8.4 FPM image
|
||||
FROM php:8.4-fpm
|
||||
|
||||
# Install system dependencies
|
||||
RUN apt-get update && apt-get install -y \
|
||||
git \
|
||||
curl \
|
||||
|
|
@ -10,25 +12,71 @@ RUN apt-get update && apt-get install -y \
|
|||
libicu-dev \
|
||||
zip \
|
||||
unzip \
|
||||
procps \
|
||||
net-tools \
|
||||
netcat-openbsd \
|
||||
nginx \
|
||||
supervisor \
|
||||
libmemcached-dev \
|
||||
zlib1g-dev \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
RUN docker-php-ext-install pdo_mysql mbstring exif pcntl bcmath gd zip intl sockets
|
||||
RUN pecl install redis \
|
||||
&& docker-php-ext-enable redis
|
||||
# Install PHP extensions
|
||||
RUN docker-php-ext-install pdo_mysql mbstring exif pcntl bcmath gd zip intl
|
||||
RUN pecl install memcached redis \
|
||||
&& docker-php-ext-enable memcached redis
|
||||
|
||||
# Install Composer
|
||||
COPY --from=composer:latest /usr/bin/composer /usr/bin/composer
|
||||
|
||||
# Install Node.js
|
||||
RUN curl -fsSL https://deb.nodesource.com/setup_20.x | bash - \
|
||||
&& apt-get install -y nodejs
|
||||
|
||||
# Install Bun
|
||||
RUN curl -fsSL https://bun.com/install | bash
|
||||
ENV PATH="/root/.bun/bin:${PATH}"
|
||||
|
||||
# Install memcached and redis servers
|
||||
RUN apt-get update && apt-get install -y memcached redis-server && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Set working directory
|
||||
WORKDIR /app
|
||||
|
||||
# Install Playwright browsers for Pest browser tests
|
||||
# Note: This requires node_modules to be installed first, so it's done at runtime
|
||||
# or you can copy package files and run: bun install && bunx playwright install --with-deps chromium
|
||||
# Copy application files
|
||||
COPY . /app/.
|
||||
|
||||
CMD ["php", "artisan", "serve", "--host=0.0.0.0", "--port=8000"]
|
||||
# Create necessary directories
|
||||
RUN mkdir -p /var/log/nginx && mkdir -p /var/cache/nginx
|
||||
|
||||
# Install PHP dependencies
|
||||
RUN composer install --no-dev --optimize-autoloader
|
||||
|
||||
# Install Node.js dependencies
|
||||
RUN bun install --frozen-lockfile
|
||||
|
||||
# Build assets with HIDE_AUTH_BUTTONS=false so Wayfinder generates all routes
|
||||
# (HIDE_AUTH_BUTTONS will be set to true at runtime via .env)
|
||||
RUN HIDE_AUTH_BUTTONS=false bun run build:ssr
|
||||
|
||||
# Copy supervisor configuration
|
||||
COPY docker/supervisor/supervisord.conf /etc/supervisor/conf.d/supervisord.conf
|
||||
|
||||
# Copy nginx configuration
|
||||
COPY docker/nginx/nginx.conf /etc/nginx/nginx.conf
|
||||
|
||||
# Set permissions
|
||||
RUN chown -R www-data:www-data /app \
|
||||
&& chmod -R 755 /app/storage \
|
||||
&& chmod -R 755 /app/bootstrap/cache
|
||||
|
||||
# Expose port
|
||||
EXPOSE 80
|
||||
|
||||
# Health check
|
||||
HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \
|
||||
CMD curl -f http://localhost/up || exit 1
|
||||
|
||||
# Copy entrypoint script
|
||||
COPY docker/entrypoint.sh /entrypoint.sh
|
||||
RUN chmod +x /entrypoint.sh
|
||||
|
||||
# Run entrypoint script (handles migrations, caching, then starts supervisor)
|
||||
CMD ["/entrypoint.sh"]
|
||||
|
|
|
|||
|
|
@ -1,92 +0,0 @@
|
|||
# Use the official PHP 8.4 FPM image
|
||||
FROM php:8.4-fpm
|
||||
|
||||
ARG SENTRY_RELEASE
|
||||
|
||||
# Install system dependencies
|
||||
RUN apt-get update && apt-get install -y \
|
||||
git \
|
||||
curl \
|
||||
libpng-dev \
|
||||
libonig-dev \
|
||||
libxml2-dev \
|
||||
libzip-dev \
|
||||
libicu-dev \
|
||||
zip \
|
||||
unzip \
|
||||
nginx \
|
||||
supervisor \
|
||||
libmemcached-dev \
|
||||
zlib1g-dev \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Install PHP extensions
|
||||
RUN docker-php-ext-install pdo_mysql mbstring exif pcntl bcmath gd zip intl
|
||||
RUN pecl install memcached redis \
|
||||
&& docker-php-ext-enable memcached redis
|
||||
|
||||
# Install Composer
|
||||
COPY --from=composer:latest /usr/bin/composer /usr/bin/composer
|
||||
|
||||
# Install Node.js
|
||||
RUN curl -fsSL https://deb.nodesource.com/setup_20.x | bash - \
|
||||
&& apt-get install -y nodejs
|
||||
|
||||
# Install Bun in a non-root path so supervisor workers running as www-data can execute it
|
||||
ENV BUN_INSTALL="/usr/local/bun"
|
||||
RUN curl -fsSL https://bun.com/install | bash \
|
||||
&& ln -sf /usr/local/bun/bin/bun /usr/local/bin/bun \
|
||||
&& chmod -R a+rx /usr/local/bun
|
||||
ENV PATH="/usr/local/bun/bin:${PATH}"
|
||||
|
||||
# Install memcached and redis servers
|
||||
RUN apt-get update && apt-get install -y memcached redis-server && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Set working directory
|
||||
WORKDIR /app
|
||||
|
||||
ENV SENTRY_RELEASE=${SENTRY_RELEASE}
|
||||
|
||||
# Copy dependency manifests first so Docker can reuse install layers across code changes
|
||||
COPY composer.json composer.lock ./
|
||||
RUN composer install --no-dev --no-scripts --no-interaction --prefer-dist --optimize-autoloader
|
||||
|
||||
COPY package.json bun.lock ./
|
||||
COPY scripts ./scripts
|
||||
RUN bun install --frozen-lockfile
|
||||
|
||||
# Copy application files
|
||||
COPY . /app/.
|
||||
RUN composer dump-autoload --no-dev --optimize
|
||||
|
||||
# Create necessary directories
|
||||
RUN mkdir -p /var/log/nginx && mkdir -p /var/cache/nginx
|
||||
|
||||
# Build assets with HIDE_AUTH_BUTTONS=false so Wayfinder generates all routes
|
||||
# (HIDE_AUTH_BUTTONS will be set to true at runtime via .env)
|
||||
RUN HIDE_AUTH_BUTTONS=false bun run build:ssr
|
||||
|
||||
# Copy supervisor configuration
|
||||
COPY docker/supervisor/supervisord.conf /etc/supervisor/conf.d/supervisord.conf
|
||||
|
||||
# Copy nginx configuration
|
||||
COPY docker/nginx/nginx.conf /etc/nginx/nginx.conf
|
||||
|
||||
# Set permissions
|
||||
RUN chown -R www-data:www-data /app \
|
||||
&& chmod -R 755 /app/storage \
|
||||
&& chmod -R 755 /app/bootstrap/cache
|
||||
|
||||
# Expose port
|
||||
EXPOSE 80
|
||||
|
||||
# Health check
|
||||
HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \
|
||||
CMD curl -f http://localhost/up || exit 1
|
||||
|
||||
# Copy entrypoint script
|
||||
COPY docker/entrypoint.sh /entrypoint.sh
|
||||
RUN chmod +x /entrypoint.sh
|
||||
|
||||
# Run entrypoint script (handles migrations, caching, then starts supervisor)
|
||||
CMD ["/entrypoint.sh"]
|
||||
273
LOCALIZATION.md
273
LOCALIZATION.md
|
|
@ -1,273 +0,0 @@
|
|||
# Frontend Localization Guide
|
||||
|
||||
This document describes the localization system for the Whisper Money React frontend and the automated tools available.
|
||||
|
||||
## Overview
|
||||
|
||||
The application now supports multilingual UI text using a simple translation system:
|
||||
|
||||
- **Hook**: `useTranslations()` (aliased as `__()`)
|
||||
- **Translation Files**: `lang/es.json` (Spanish), with English as fallback
|
||||
- **Coverage**: 118+ React components have been localized
|
||||
- **Total Strings**: 730+ translatable strings
|
||||
|
||||
## How It Works
|
||||
|
||||
### Backend (Laravel)
|
||||
|
||||
The `HandleInertiaRequests` middleware loads the appropriate translation file (`lang/{locale}.json`) and shares it with all Inertia page props:
|
||||
|
||||
```php
|
||||
protected function getTranslations(): array
|
||||
{
|
||||
$locale = app()->getLocale();
|
||||
$translationFile = lang_path("{$locale}.json");
|
||||
return json_decode(file_get_contents($translationFile), true) ?? [];
|
||||
}
|
||||
```
|
||||
|
||||
### Frontend (React)
|
||||
|
||||
Components use the `__()` function directly to translate strings:
|
||||
|
||||
```tsx
|
||||
import { __ } from '@/utils/i18n';
|
||||
|
||||
export default function MyComponent() {
|
||||
return (
|
||||
<div>
|
||||
<h1>{__('Welcome to Whisper Money')}</h1>
|
||||
<Button>{__('Save')}</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
**Note**: The `__()` function uses React's `usePage()` hook internally, so it must be called within React components.
|
||||
|
||||
### Frontend (React)
|
||||
|
||||
Components use the `__()` hook to translate strings:
|
||||
|
||||
```tsx
|
||||
import { __ } from '@/utils/i18n';
|
||||
|
||||
export default function MyComponent() {
|
||||
const t = __();
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1>{t('Welcome to Whisper Money')}</h1>
|
||||
<Button>{t('Save')}</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Automated Localization Tools
|
||||
|
||||
### Main Localization Script (Local Development Only)
|
||||
|
||||
**File**: `scripts/localize-frontend.mjs` (not committed to repo)
|
||||
|
||||
This script automatically wraps hardcoded strings with the `t()` translation function across all React components.
|
||||
|
||||
**What it does**:
|
||||
|
||||
- Scans all `.tsx` and `.ts` files in `resources/js/`
|
||||
- Wraps JSX text content and specific props (title, placeholder, etc.) with `t()`
|
||||
- Auto-imports the `__()` hook and adds `const t = __()` to components
|
||||
- Extracts all unique strings to `lang/es.json`
|
||||
- Generates a detailed report of changes
|
||||
|
||||
**What it skips**:
|
||||
|
||||
- Technical identifiers (class names, data-test attributes, etc.)
|
||||
- URLs, email addresses
|
||||
- Currency codes (USD, EUR, etc.)
|
||||
- Brand names (Whisper Money, Discord, etc.)
|
||||
- Single-character strings
|
||||
- Code-like patterns
|
||||
|
||||
**Usage** (local development only):
|
||||
|
||||
```bash
|
||||
# Run the localization script (safe to run multiple times)
|
||||
node scripts/localize-frontend.mjs
|
||||
|
||||
# Format the modified files
|
||||
bun run format
|
||||
|
||||
# Review changes
|
||||
git diff resources/js/
|
||||
git diff lang/es.json
|
||||
cat localization-report.txt
|
||||
```
|
||||
|
||||
**Note**: The script is for local development only and not committed to the repository. Translations must be added manually to `lang/es.json`.
|
||||
|
||||
## Current Status
|
||||
|
||||
### Completed (✅)
|
||||
|
||||
- ✅ 118 React components localized
|
||||
- ✅ 730 strings extracted and wrapped with `t()`
|
||||
- ✅ Translation infrastructure in place
|
||||
- ✅ Automated extraction tool created
|
||||
- ✅ English fallbacks for all strings
|
||||
|
||||
### Pending (⏳)
|
||||
|
||||
- ⏳ Spanish translations (currently English placeholders - manual translation needed)
|
||||
- ⏳ Some files may need manual review for complex JSX patterns
|
||||
|
||||
## Adding New Translatable Strings
|
||||
|
||||
### Option 1: Manual (Recommended for new code)
|
||||
|
||||
```tsx
|
||||
import { __ } from '@/utils/i18n';
|
||||
|
||||
export default function NewComponent() {
|
||||
return (
|
||||
<div>
|
||||
<h1>{__('My New Title')}</h1>
|
||||
<Input placeholder={__('Enter your name')} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
Then add the translations to `lang/es.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"My New Title": "Mi Nuevo Título",
|
||||
"Enter your name": "Ingresa tu nombre"
|
||||
}
|
||||
```
|
||||
|
||||
Then add the translations to `lang/es.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"My New Title": "Mi Nuevo Título",
|
||||
"Enter your name": "Ingresa tu nombre"
|
||||
}
|
||||
```
|
||||
|
||||
### Option 2: Automated (For bulk updates - local development)
|
||||
|
||||
1. Write your component with hardcoded English strings
|
||||
2. Run `node scripts/localize-frontend.mjs` (if available locally)
|
||||
3. Run `bun run format`
|
||||
4. Manually translate new entries in `lang/es.json`
|
||||
|
||||
## Translation with Placeholders
|
||||
|
||||
Support for dynamic values:
|
||||
|
||||
```tsx
|
||||
// In your component
|
||||
const message = __('Hello :name, you have :count unread messages', {
|
||||
name: user.name,
|
||||
count: unreadCount
|
||||
});
|
||||
|
||||
// In lang/es.json
|
||||
{
|
||||
"Hello :name, you have :count unread messages": "Hola :name, tienes :count mensajes sin leer"
|
||||
}
|
||||
```
|
||||
|
||||
## Testing Translations
|
||||
|
||||
1. **Change language**: Go to Settings → Language → Select "Español"
|
||||
2. **Test UI**: Navigate through the app and verify translations
|
||||
3. **Check console**: Look for missing translation warnings (if implemented)
|
||||
|
||||
## Translation Guidelines
|
||||
|
||||
### For Spanish (es)
|
||||
|
||||
- Use informal "tú" form (not formal "usted")
|
||||
- Financial terms should be accurate and professional
|
||||
- Preserve brand names in English
|
||||
- Keep the friendly, approachable tone
|
||||
- Use sentence case, not title case
|
||||
|
||||
### Examples
|
||||
|
||||
- ✅ "Guardar cambios" (Save changes)
|
||||
- ❌ "Guardar Cambios" (wrong capitalization)
|
||||
- ✅ "Iniciar sesión" (Log in)
|
||||
- ❌ "Entrar" (too casual)
|
||||
|
||||
## CI/CD Integration
|
||||
|
||||
Consider adding translation validation to your CI pipeline in the future to ensure all strings are translated.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Issue: Translations not showing
|
||||
|
||||
**Solution**: Check that:
|
||||
|
||||
1. The string exists in `lang/es.json`
|
||||
2. User's language setting is "Español" or auto-detected to Spanish
|
||||
3. Browser cache is cleared
|
||||
|
||||
### Issue: String wrapped incorrectly by script
|
||||
|
||||
**Solution**:
|
||||
|
||||
1. Revert the file: `git checkout <file>`
|
||||
2. Manually wrap the string
|
||||
3. Add the string pattern to skip list in `scripts/localize-frontend.mjs`
|
||||
|
||||
### Issue: Script breaks complex JSX
|
||||
|
||||
**Solution**: The script has limitations with very complex nested JSX. For these files:
|
||||
|
||||
1. Revert the auto-changes
|
||||
2. Manually localize
|
||||
3. Or simplify the JSX structure
|
||||
|
||||
## File Structure
|
||||
|
||||
```
|
||||
lang/
|
||||
├── es.json # Spanish translations
|
||||
├── es/ # Laravel backend translations (PHP)
|
||||
│ ├── auth.php
|
||||
│ ├── pagination.php
|
||||
│ └── ...
|
||||
└── en/ # English backend translations
|
||||
|
||||
scripts/
|
||||
├── localize-frontend.mjs # Main localization tool
|
||||
└── translate-to-spanish.mjs # AI translation helper
|
||||
|
||||
resources/js/
|
||||
├── utils/i18n.ts # Translation hook
|
||||
└── types/index.d.ts # SharedData type (includes translations)
|
||||
```
|
||||
|
||||
## Next Steps
|
||||
|
||||
1. **Manual translation**: Translate English placeholders in `lang/es.json` to Spanish
|
||||
2. **Review translations**: Check accuracy, tone, and proper Spanish conventions
|
||||
3. **Test thoroughly**: Test the entire app in Spanish
|
||||
4. **Add more languages**: Create `lang/fr.json`, `lang/de.json`, etc.
|
||||
|
||||
## Resources
|
||||
|
||||
- Translation hook: `resources/js/utils/i18n.ts`
|
||||
- Example localized component: `resources/js/pages/auth/login.tsx`
|
||||
- Example localized settings: `resources/js/pages/settings/account.tsx`
|
||||
- Translation middleware: `app/Http/Middleware/HandleInertiaRequests.php`
|
||||
- Spanish translations: `lang/es.json`
|
||||
|
||||
## Development Scripts
|
||||
|
||||
The localization automation script (`scripts/localize-frontend.mjs`) is available for local development only and is not committed to the repository. It uses Babel to parse and transform React components automatically. If you need to regenerate it, refer to the project's localization implementation history.
|
||||
|
|
@ -1,55 +0,0 @@
|
|||
# Welcome to Whisper Money
|
||||
|
||||
## How We Use Claude
|
||||
|
||||
Based on Víctor Falcón's usage over the last 30 days:
|
||||
|
||||
Work Type Breakdown:
|
||||
Debug Fix ████████████████████ 100%
|
||||
|
||||
Top Skills & Commands:
|
||||
/sentry-cli ████████████████████ 1x/month
|
||||
|
||||
Top MCP Servers:
|
||||
_None used in this window_
|
||||
|
||||
## Your Setup Checklist
|
||||
|
||||
### Codebases
|
||||
- [ ] whisper-money — https://github.com/whisper-money/whisper-money
|
||||
|
||||
### MCP Servers to Activate
|
||||
- [ ] _None required yet_ — the team hasn't leaned on MCP servers in this window. Sentry access is via the `/sentry-cli` skill (see below).
|
||||
|
||||
### Skills to Know About
|
||||
- [ ] `/sentry-cli` — drives Sentry from the command line (view issues, events, projects, orgs, make API calls). The team uses it when triaging and fixing flagged errors before opening a PR.
|
||||
|
||||
## Team Tips
|
||||
|
||||
_TODO_
|
||||
|
||||
## Get Started
|
||||
|
||||
_TODO_
|
||||
|
||||
<!-- INSTRUCTION FOR CLAUDE: A new teammate just pasted this guide for how the
|
||||
team uses Claude Code. You're their onboarding buddy — warm, conversational,
|
||||
not lecture-y.
|
||||
|
||||
Open with a warm welcome — include the team name from the title. Then: "Your
|
||||
teammate uses Claude Code for [list all the work types]. Let's get you started."
|
||||
|
||||
Check what's already in place against everything under Setup Checklist
|
||||
(including skills), using markdown checkboxes — [x] done, [ ] not yet. Lead
|
||||
with what they already have. One sentence per item, all in one message.
|
||||
|
||||
Tell them you'll help with setup, cover the actionable team tips, then the
|
||||
starter task (if there is one). Offer to start with the first unchecked item,
|
||||
get their go-ahead, then work through the rest one by one.
|
||||
|
||||
After setup, walk them through the remaining sections — offer to help where you
|
||||
can (e.g. link to channels), and just surface the purely informational bits.
|
||||
|
||||
Don't invent sections or summaries that aren't in the guide. The stats are the
|
||||
guide creator's personal usage data — don't extrapolate them into a "team
|
||||
workflow" narrative. -->
|
||||
123
README.md
123
README.md
|
|
@ -1,14 +1,15 @@
|
|||
<!-- <p align="center"><a href="https://www.producthunt.com/products/whisper-money?embed=true&utm_source=badge-featured&utm_medium=badge&utm_source=badge-whisper-money" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=1042250&theme=light&t=1764232132380" alt="Whisper Money - The most secure way to understand your finances | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a></p> -->
|
||||
<p align="center">
|
||||
<img src="https://whisper.money/images/og_whisper_money.png?20260215075346" alt="Whisper Money" width="100%">
|
||||
<img src="https://whisper.money/images/og_whisper_money.png" alt="Whisper Money" width="100%">
|
||||
</p>
|
||||
<p align="center">
|
||||
<a href="https://zdoc.app/de/whisper-money/whisper-money">Deutsch</a> |
|
||||
<a href="https://zdoc.app/es/whisper-money/whisper-money">Español</a> |
|
||||
<a href="https://zdoc.app/fr/whisper-money/whisper-money">français</a> |
|
||||
<a href="https://zdoc.app/ja/whisper-money/whisper-money">日本語</a> |
|
||||
<a href="https://zdoc.app/ko/whisper-money/whisper-money">한국어</a> |
|
||||
<a href="https://zdoc.app/pt/whisper-money/whisper-money">Português</a> |
|
||||
<a href="https://zdoc.app/ru/whisper-money/whisper-money">Русский</a> |
|
||||
<a href="https://zdoc.app/de/whisper-money/whisper-money">Deutsch</a> |
|
||||
<a href="https://zdoc.app/es/whisper-money/whisper-money">Español</a> |
|
||||
<a href="https://zdoc.app/fr/whisper-money/whisper-money">français</a> |
|
||||
<a href="https://zdoc.app/ja/whisper-money/whisper-money">日本語</a> |
|
||||
<a href="https://zdoc.app/ko/whisper-money/whisper-money">한국어</a> |
|
||||
<a href="https://zdoc.app/pt/whisper-money/whisper-money">Português</a> |
|
||||
<a href="https://zdoc.app/ru/whisper-money/whisper-money">Русский</a> |
|
||||
<a href="https://zdoc.app/zh/whisper-money/whisper-money">中文</a>
|
||||
</p>
|
||||
|
||||
|
|
@ -22,11 +23,11 @@ Whisper Money is a privacy-first personal finance application that helps you tra
|
|||
|
||||
> 🎮 **Try the Demo:** Experience Whisper Money with our [demo account](https://whisper.money/login?demo=1) - no registration required!
|
||||
|
||||
> 💬 **Join our Community:** Whether you're a user looking for help or a developer wanting to contribute, we'd love to have you in our [Discord server](https://discord.gg/m8hUhx6D9D)! Share feedback, ask questions, discuss new features, or just hang out with fellow privacy enthusiasts.
|
||||
> 💬 **Join our Community:** Whether you're a user looking for help or a developer wanting to contribute, we'd love to have you in our [Discord server](https://discord.gg/Zj4TWMX55y)! Share feedback, ask questions, discuss new features, or just hang out with fellow privacy enthusiasts.
|
||||
|
||||
## Features
|
||||
|
||||
- 🔐 **Privacy-first** — Your data is never shared with third parties. You own it
|
||||
- 🔐 **End-to-end encryption** — Your financial data stays private
|
||||
- 🏦 **Bank account management** — Track multiple accounts in one place
|
||||
- 📊 **Transaction categorization** — Automatic and manual categorization
|
||||
- 🤖 **Automation rules** — Set up rules to auto-categorize transactions
|
||||
|
|
@ -43,56 +44,40 @@ Whisper Money is a privacy-first personal finance application that helps you tra
|
|||
|
||||
## Running Locally
|
||||
|
||||
### Quick Start (Recommended)
|
||||
### Prerequisites
|
||||
|
||||
The easiest way to get started is using our automated setup script:
|
||||
- Docker & Docker Compose
|
||||
- Composer
|
||||
- Node.js / Bun
|
||||
|
||||
```bash
|
||||
bash <(curl -fsSL https://whisper.money/setup.sh)
|
||||
```
|
||||
|
||||
After installation, just visit **<https://whisper.money.localhost>** in your browser.
|
||||
|
||||
### Manual Setup
|
||||
|
||||
If you prefer to set up manually:
|
||||
### Setup
|
||||
|
||||
1. **Clone the repository:**
|
||||
|
||||
```bash
|
||||
git clone https://github.com/whisper-money/whisper-money.git
|
||||
cd whisper-money
|
||||
cd whisper_money
|
||||
```
|
||||
|
||||
1. **Run the setup script:**
|
||||
2. **Copy the environment file:**
|
||||
|
||||
```bash
|
||||
whispermoney install
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
### Available Commands
|
||||
|
||||
> **Important:** You must run `whispermoney install` before using any other command. If you skip the install step, commands like `start` will not work.
|
||||
|
||||
Once installed, you can use the `whispermoney` command for common tasks:
|
||||
3. **Start the Docker services:**
|
||||
|
||||
```bash
|
||||
# Start all services
|
||||
whispermoney start
|
||||
|
||||
# Stop all services
|
||||
whispermoney stop
|
||||
|
||||
# Upgrade to latest version
|
||||
whispermoney upgrade
|
||||
|
||||
# Interactive menu
|
||||
whispermoney
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
### Development Server
|
||||
4. **Install dependencies and setup the application:**
|
||||
|
||||
For active development with hot reloading:
|
||||
```bash
|
||||
composer setup
|
||||
```
|
||||
|
||||
5. **Start the development server:**
|
||||
|
||||
```bash
|
||||
composer run dev
|
||||
|
|
@ -100,12 +85,12 @@ composer run dev
|
|||
|
||||
This will concurrently start:
|
||||
|
||||
- PHP development server (via [Portless](https://portless.sh) HTTPS proxy)
|
||||
- PHP development server
|
||||
- Queue worker
|
||||
- Log viewer (Pail)
|
||||
- Vite dev server
|
||||
|
||||
The application will be available at **<https://dev.whisper.money.localhost>**. In git worktrees, the branch name is automatically prepended (e.g. `https://fix-ui.dev.whisper.money.localhost`).
|
||||
The application will be available at `https://whispermoney.test`.
|
||||
|
||||
## Running with Docker (Production Image)
|
||||
|
||||
|
|
@ -117,7 +102,7 @@ For testing the production Docker image locally:
|
|||
cp .env.production.example .env
|
||||
```
|
||||
|
||||
1. **Start the services:**
|
||||
2. **Start the services:**
|
||||
|
||||
```bash
|
||||
docker compose -f docker-compose.production.yml up -d
|
||||
|
|
@ -144,7 +129,6 @@ Whisper Money can be easily deployed to [Coolify](https://coolify.io) using our
|
|||
4. Deploy!
|
||||
|
||||
The template includes:
|
||||
|
||||
- Whisper Money application container
|
||||
- MySQL 8.0 database with health checks
|
||||
- Persistent volumes for data and storage
|
||||
|
|
@ -163,56 +147,11 @@ The template includes:
|
|||
| Variable | Default | Description |
|
||||
| ----------------------- | ------- | -------------------------------------------------- |
|
||||
| `DRIP_EMAILS_ENABLED` | `true` | Enable drip emails (welcome, onboarding, feedback) |
|
||||
| `REGISTRATION_ENABLED` | `true` | Set to `false` to close public sign-ups (the `/register` routes return a 403 and every registration CTA is hidden) while keeping `/login` open |
|
||||
| `HIDE_AUTH_BUTTONS` | `false` | Hide login/register buttons on landing page |
|
||||
| `SUBSCRIPTIONS_ENABLED` | `false` | Enable Stripe subscriptions |
|
||||
| `STRIPE_KEY` | - | Stripe publishable key |
|
||||
| `STRIPE_SECRET` | - | Stripe secret key |
|
||||
| `STRIPE_WEBHOOK_SECRET` | - | Stripe webhook signing secret |
|
||||
| `AI_PROVIDER` | `gemini`| AI provider for every AI feature (`gemini`, `ollama`, `openai`, ...) |
|
||||
|
||||
## AI Provider
|
||||
|
||||
Whisper Money's AI features (transaction categorization and automation-rule
|
||||
suggestions) run on [`laravel/ai`](https://github.com/laravel/ai) and default to
|
||||
Google **Gemini**. The provider is configurable independently of the model, so
|
||||
you can point the app at **any text provider `laravel/ai` supports** — `gemini`,
|
||||
`openai`, `anthropic`, `azure`, `groq`, `xai`, `deepseek`, `mistral`, or a
|
||||
self-hosted **[Ollama](https://ollama.com)** server. Ollama is the headline case
|
||||
because it keeps AI processing fully local and private — data never leaves your
|
||||
infrastructure — but the switch is generic.
|
||||
|
||||
Each provider needs its own credentials configured for `laravel/ai` (e.g.
|
||||
`GEMINI_API_KEY`, `OPENAI_API_KEY`, `ANTHROPIC_API_KEY`, or `OLLAMA_URL`). An
|
||||
unknown or non-text provider fails fast when the AI feature runs.
|
||||
|
||||
| Variable | Default | Description |
|
||||
| ---------------------------- | -------------------- | --------------------------------------------------------------------- |
|
||||
| `AI_PROVIDER` | `gemini` | Provider for all AI features. Set once to switch everything. |
|
||||
| `AI_SUGGESTIONS_PROVIDER` | `AI_PROVIDER` | Override the provider for rule suggestions only. |
|
||||
| `AI_CATEGORIZATION_PROVIDER` | `AI_PROVIDER` | Override the provider for transaction categorization only. |
|
||||
| `AI_SUGGESTIONS_MODEL` | `gemini-flash-latest`| Model used for rule suggestions. |
|
||||
| `AI_CATEGORIZATION_MODEL` | `gemini-flash-latest`| Model used for transaction categorization. |
|
||||
| `GEMINI_API_KEY` | - | Required when the provider is `gemini`. |
|
||||
| `OLLAMA_URL` | `http://localhost:11434` | Ollama server URL (used when the provider is `ollama`). |
|
||||
| `OLLAMA_API_KEY` | - | Optional; only needed behind an authenticating proxy. |
|
||||
|
||||
### Example: fully local AI with Ollama
|
||||
|
||||
```dotenv
|
||||
AI_PROVIDER=ollama
|
||||
OLLAMA_URL=http://ollama.example.local:11434
|
||||
AI_SUGGESTIONS_MODEL=gemma3:12b
|
||||
AI_CATEGORIZATION_MODEL=gemma3:12b
|
||||
```
|
||||
|
||||
Make sure the model is pulled on the Ollama server first (`ollama pull gemma3:12b`).
|
||||
Any other provider follows the same pattern: set `AI_PROVIDER`, that provider's
|
||||
credentials, and the `*_MODEL` vars to one of its models. Gemini remains the
|
||||
default, so existing deployments are unaffected.
|
||||
|
||||
## Star History
|
||||
|
||||
[](https://www.star-history.com/?type=date&legend=top-left&repos=whisper-money%2Fwhisper-money)
|
||||
|
||||
## License
|
||||
|
||||
|
|
|
|||
|
|
@ -1,49 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Actions\Ai;
|
||||
|
||||
use App\Jobs\CategorizeUncategorizedTransactionsJob;
|
||||
use App\Models\Transaction;
|
||||
use App\Models\User;
|
||||
use App\Services\Ai\AiCategorizationGate;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class StartCategorizationBackfill
|
||||
{
|
||||
public function __construct(private readonly AiCategorizationGate $gate) {}
|
||||
|
||||
/**
|
||||
* Dispatch a categorization backfill when the user is eligible and has
|
||||
* something to categorize, seeding the progress cache the client polls.
|
||||
*
|
||||
* @return array{job_id: string, total: int}|null
|
||||
*/
|
||||
public function handle(User $user): ?array
|
||||
{
|
||||
if (! $this->gate->allows($user)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$total = Transaction::query()
|
||||
->where('user_id', $user->id)
|
||||
->pendingAiCategorization()
|
||||
->count();
|
||||
|
||||
if ($total === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$jobId = (string) Str::uuid();
|
||||
|
||||
Cache::put(
|
||||
CategorizeUncategorizedTransactionsJob::cacheKeyForJobId($user->id, $jobId),
|
||||
['status' => 'pending', 'processed' => 0, 'total' => $total, 'applied' => 0],
|
||||
now()->addHour(),
|
||||
);
|
||||
|
||||
CategorizeUncategorizedTransactionsJob::dispatch($user, $jobId);
|
||||
|
||||
return ['job_id' => $jobId, 'total' => $total];
|
||||
}
|
||||
}
|
||||
|
|
@ -2,104 +2,31 @@
|
|||
|
||||
namespace App\Actions;
|
||||
|
||||
use App\Enums\CategoryCashflowDirection;
|
||||
use App\Enums\CategoryType;
|
||||
use App\Models\Category;
|
||||
use App\Models\Space;
|
||||
use App\Models\User;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class CreateDefaultCategories
|
||||
{
|
||||
/**
|
||||
* Create default categories for a newly registered user, nesting child
|
||||
* categories under their configured parent. Categories live in a space, so
|
||||
* seeding targets the given space (defaulting to the user's active one).
|
||||
* Create default categories for a newly registered user.
|
||||
*/
|
||||
public function handle(User $user, ?Space $space = null): void
|
||||
public function handle(User $user): void
|
||||
{
|
||||
$space ??= $user->activeSpace();
|
||||
$locale = $user->locale ?? app()->getLocale();
|
||||
$defaultCategories = self::getDefaultCategories($locale);
|
||||
$defaultCategories = self::getDefaultCategories();
|
||||
|
||||
$existingCategories = $space->categories()
|
||||
->whereIn('name', array_column($defaultCategories, 'name'))
|
||||
->pluck('id', 'name');
|
||||
|
||||
$now = now();
|
||||
$pending = collect($defaultCategories)
|
||||
->reject(fn (array $category): bool => $existingCategories->has($category['name']))
|
||||
->map(fn (array $category): array => [
|
||||
...$category,
|
||||
'cashflow_direction' => $category['cashflow_direction'] ?? CategoryCashflowDirection::Hidden->value,
|
||||
'id' => (string) Str::uuid(),
|
||||
'user_id' => $user->id,
|
||||
'space_id' => $space->id,
|
||||
'created_at' => $now,
|
||||
'updated_at' => $now,
|
||||
]);
|
||||
|
||||
if ($pending->isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$idsByName = $existingCategories->merge($pending->pluck('id', 'name'));
|
||||
|
||||
[$children, $roots] = $pending->partition(fn (array $category): bool => isset($category['parent']));
|
||||
|
||||
if ($roots->isNotEmpty()) {
|
||||
Category::query()->insert(
|
||||
$roots->map(fn (array $category): array => Arr::except($category, 'parent'))->values()->all()
|
||||
);
|
||||
}
|
||||
|
||||
if ($children->isNotEmpty()) {
|
||||
Category::query()->insert(
|
||||
$children
|
||||
->map(fn (array $category): array => [
|
||||
...Arr::except($category, 'parent'),
|
||||
'parent_id' => $idsByName[$category['parent']] ?? null,
|
||||
])
|
||||
->values()
|
||||
->all()
|
||||
foreach ($defaultCategories as $category) {
|
||||
$user->categories()->firstOrCreate(
|
||||
['name' => $category['name']],
|
||||
$category
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the default categories configuration for a given locale.
|
||||
* Get the default categories configuration.
|
||||
*
|
||||
* @return array<int, array{name: string, icon: string, color: string, type: string, cashflow_direction?: string, parent?: string}>
|
||||
* @return array<int, array{name: string, icon: string, color: string, type: string}>
|
||||
*/
|
||||
public static function getDefaultCategories(string $locale = 'en'): array
|
||||
{
|
||||
$categories = self::getBaseCategories();
|
||||
|
||||
if ($locale === 'es') {
|
||||
$translations = self::getSpanishTranslations();
|
||||
|
||||
return array_map(function (array $category) use ($translations) {
|
||||
$category['name'] = $translations[$category['name']] ?? $category['name'];
|
||||
|
||||
if (isset($category['parent'])) {
|
||||
$category['parent'] = $translations[$category['parent']] ?? $category['parent'];
|
||||
}
|
||||
|
||||
return $category;
|
||||
}, $categories);
|
||||
}
|
||||
|
||||
return $categories;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the base (English) categories configuration. A `parent` entry nests
|
||||
* the category under the default category with that (English) name.
|
||||
*
|
||||
* @return array<int, array{name: string, icon: string, color: string, type: string, cashflow_direction?: string, parent?: string}>
|
||||
*/
|
||||
private static function getBaseCategories(): array
|
||||
public static function getDefaultCategories(): array
|
||||
{
|
||||
return [
|
||||
[
|
||||
|
|
@ -110,35 +37,30 @@ class CreateDefaultCategories
|
|||
],
|
||||
[
|
||||
'name' => 'Cafes, restaurants, bars',
|
||||
'parent' => 'Food',
|
||||
'icon' => 'Wine',
|
||||
'color' => 'red',
|
||||
'type' => 'expense',
|
||||
],
|
||||
[
|
||||
'name' => 'Groceries',
|
||||
'parent' => 'Food',
|
||||
'icon' => 'ShoppingBasket',
|
||||
'color' => 'red',
|
||||
'type' => 'expense',
|
||||
],
|
||||
[
|
||||
'name' => 'Tobacco and alcohol',
|
||||
'parent' => 'Food',
|
||||
'icon' => 'Cigarette',
|
||||
'color' => 'red',
|
||||
'type' => 'expense',
|
||||
],
|
||||
[
|
||||
'name' => 'Other groceries',
|
||||
'parent' => 'Food',
|
||||
'icon' => 'ShoppingBasket',
|
||||
'color' => 'red',
|
||||
'type' => 'expense',
|
||||
],
|
||||
[
|
||||
'name' => 'Food delivery',
|
||||
'parent' => 'Food',
|
||||
'icon' => 'Pizza',
|
||||
'color' => 'red',
|
||||
'type' => 'expense',
|
||||
|
|
@ -151,49 +73,42 @@ class CreateDefaultCategories
|
|||
],
|
||||
[
|
||||
'name' => 'Electricity',
|
||||
'parent' => 'Utility services',
|
||||
'icon' => 'Bolt',
|
||||
'color' => 'orange',
|
||||
'type' => 'expense',
|
||||
],
|
||||
[
|
||||
'name' => 'Natural gas',
|
||||
'parent' => 'Utility services',
|
||||
'icon' => 'Flame',
|
||||
'color' => 'orange',
|
||||
'type' => 'expense',
|
||||
],
|
||||
[
|
||||
'name' => 'Rent and maintanence',
|
||||
'parent' => 'Utility services',
|
||||
'icon' => 'Wrench',
|
||||
'color' => 'orange',
|
||||
'type' => 'expense',
|
||||
],
|
||||
[
|
||||
'name' => 'Telephone, internet, TV, computer',
|
||||
'parent' => 'Utility services',
|
||||
'icon' => 'Wifi',
|
||||
'color' => 'orange',
|
||||
'type' => 'expense',
|
||||
],
|
||||
[
|
||||
'name' => 'Water',
|
||||
'parent' => 'Utility services',
|
||||
'icon' => 'Droplets',
|
||||
'color' => 'orange',
|
||||
'type' => 'expense',
|
||||
],
|
||||
[
|
||||
'name' => 'Other utility expenses',
|
||||
'parent' => 'Utility services',
|
||||
'icon' => 'Receipt',
|
||||
'color' => 'orange',
|
||||
'type' => 'expense',
|
||||
],
|
||||
[
|
||||
'name' => 'Household goods',
|
||||
'parent' => 'Utility services',
|
||||
'icon' => 'Home',
|
||||
'color' => 'orange',
|
||||
'type' => 'expense',
|
||||
|
|
@ -206,28 +121,24 @@ class CreateDefaultCategories
|
|||
],
|
||||
[
|
||||
'name' => 'Parking',
|
||||
'parent' => 'Transportation',
|
||||
'icon' => 'ParkingMeter',
|
||||
'color' => 'amber',
|
||||
'type' => 'expense',
|
||||
],
|
||||
[
|
||||
'name' => 'Fuel',
|
||||
'parent' => 'Transportation',
|
||||
'icon' => 'Fuel',
|
||||
'color' => 'amber',
|
||||
'type' => 'expense',
|
||||
],
|
||||
[
|
||||
'name' => 'Transportation expenses',
|
||||
'parent' => 'Transportation',
|
||||
'icon' => 'Ticket',
|
||||
'color' => 'amber',
|
||||
'type' => 'expense',
|
||||
],
|
||||
[
|
||||
'name' => 'Vehicle purchase, maintenance',
|
||||
'parent' => 'Transportation',
|
||||
'icon' => 'Car',
|
||||
'color' => 'amber',
|
||||
'type' => 'expense',
|
||||
|
|
@ -246,42 +157,36 @@ class CreateDefaultCategories
|
|||
],
|
||||
[
|
||||
'name' => 'Gifts',
|
||||
'parent' => 'Leisure activities, traveling',
|
||||
'icon' => 'Gift',
|
||||
'color' => 'violet',
|
||||
'type' => 'expense',
|
||||
],
|
||||
[
|
||||
'name' => 'Books, newspapers, magazines',
|
||||
'parent' => 'Leisure activities, traveling',
|
||||
'icon' => 'BookOpen',
|
||||
'color' => 'violet',
|
||||
'type' => 'expense',
|
||||
],
|
||||
[
|
||||
'name' => 'Accommodation, travel expenses',
|
||||
'parent' => 'Leisure activities, traveling',
|
||||
'icon' => 'Hotel',
|
||||
'color' => 'violet',
|
||||
'type' => 'expense',
|
||||
],
|
||||
[
|
||||
'name' => 'Sport and sports goods',
|
||||
'parent' => 'Leisure activities, traveling',
|
||||
'icon' => 'Dumbbell',
|
||||
'color' => 'violet',
|
||||
'type' => 'expense',
|
||||
],
|
||||
[
|
||||
'name' => 'Theatre, music, cinema',
|
||||
'parent' => 'Leisure activities, traveling',
|
||||
'icon' => 'Clapperboard',
|
||||
'color' => 'violet',
|
||||
'type' => 'expense',
|
||||
],
|
||||
[
|
||||
'name' => 'Hobbies and other leisure time activites',
|
||||
'parent' => 'Leisure activities, traveling',
|
||||
'icon' => 'Puzzle',
|
||||
'color' => 'violet',
|
||||
'type' => 'expense',
|
||||
|
|
@ -294,21 +199,18 @@ class CreateDefaultCategories
|
|||
],
|
||||
[
|
||||
'name' => 'Education and courses',
|
||||
'parent' => 'Education, health and beauty',
|
||||
'icon' => 'GraduationCap',
|
||||
'color' => 'rose',
|
||||
'type' => 'expense',
|
||||
],
|
||||
[
|
||||
'name' => 'Beauty, cosmetics',
|
||||
'parent' => 'Education, health and beauty',
|
||||
'icon' => 'Sparkles',
|
||||
'color' => 'rose',
|
||||
'type' => 'expense',
|
||||
],
|
||||
[
|
||||
'name' => 'Health and pharmaceuticals',
|
||||
'parent' => 'Education, health and beauty',
|
||||
'icon' => 'HeartPulse',
|
||||
'color' => 'rose',
|
||||
'type' => 'expense',
|
||||
|
|
@ -321,7 +223,6 @@ class CreateDefaultCategories
|
|||
],
|
||||
[
|
||||
'name' => 'Online services',
|
||||
'parent' => 'Online transactions',
|
||||
'icon' => 'Server',
|
||||
'color' => 'fuchsia',
|
||||
'type' => 'expense',
|
||||
|
|
@ -336,23 +237,19 @@ class CreateDefaultCategories
|
|||
'name' => 'Investments',
|
||||
'icon' => 'LineChart',
|
||||
'color' => 'lime',
|
||||
'type' => CategoryType::Investment->value,
|
||||
'cashflow_direction' => CategoryCashflowDirection::Outflow->value,
|
||||
'type' => 'expense',
|
||||
],
|
||||
[
|
||||
'name' => 'Savings',
|
||||
'icon' => 'PiggyBank',
|
||||
'color' => 'lime',
|
||||
'type' => CategoryType::Savings->value,
|
||||
'cashflow_direction' => CategoryCashflowDirection::Outflow->value,
|
||||
'type' => 'expense',
|
||||
],
|
||||
[
|
||||
'name' => 'Other investments',
|
||||
'parent' => 'Investments',
|
||||
'icon' => 'TrendingUp',
|
||||
'color' => 'lime',
|
||||
'type' => CategoryType::Investment->value,
|
||||
'cashflow_direction' => CategoryCashflowDirection::Outflow->value,
|
||||
'type' => 'expense',
|
||||
],
|
||||
[
|
||||
'name' => 'Financial services and commission',
|
||||
|
|
@ -392,7 +289,6 @@ class CreateDefaultCategories
|
|||
],
|
||||
[
|
||||
'name' => 'Lottery',
|
||||
'parent' => 'Gambling',
|
||||
'icon' => 'TicketPercent',
|
||||
'color' => 'purple',
|
||||
'type' => 'expense',
|
||||
|
|
@ -417,7 +313,6 @@ class CreateDefaultCategories
|
|||
],
|
||||
[
|
||||
'name' => 'Other personal transfers',
|
||||
'parent' => 'Personal transfers',
|
||||
'icon' => 'ArrowLeftRight',
|
||||
'color' => 'cyan',
|
||||
'type' => 'transfer',
|
||||
|
|
@ -493,7 +388,6 @@ class CreateDefaultCategories
|
|||
'icon' => 'Users',
|
||||
'color' => 'blue',
|
||||
'type' => 'transfer',
|
||||
'cashflow_direction' => CategoryCashflowDirection::Inflow->value,
|
||||
],
|
||||
[
|
||||
'name' => 'Returned payments',
|
||||
|
|
@ -507,12 +401,6 @@ class CreateDefaultCategories
|
|||
'color' => 'green',
|
||||
'type' => 'expense',
|
||||
],
|
||||
[
|
||||
'name' => 'Self-Employment Income',
|
||||
'icon' => 'Briefcase',
|
||||
'color' => 'green',
|
||||
'type' => 'income',
|
||||
],
|
||||
[
|
||||
'name' => 'Other incoming payments',
|
||||
'icon' => 'DollarSign',
|
||||
|
|
@ -521,79 +409,4 @@ class CreateDefaultCategories
|
|||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the Spanish translations for category names.
|
||||
*
|
||||
* @return array<string, string>
|
||||
*/
|
||||
private static function getSpanishTranslations(): array
|
||||
{
|
||||
return [
|
||||
'Food' => 'Alimentación',
|
||||
'Cafes, restaurants, bars' => 'Cafeterías, restaurantes, bares',
|
||||
'Groceries' => 'Supermercado',
|
||||
'Tobacco and alcohol' => 'Tabaco y alcohol',
|
||||
'Other groceries' => 'Otras compras de alimentación',
|
||||
'Food delivery' => 'Comida a domicilio',
|
||||
'Utility services' => 'Servicios del hogar',
|
||||
'Electricity' => 'Electricidad',
|
||||
'Natural gas' => 'Gas natural',
|
||||
'Rent and maintanence' => 'Alquiler y mantenimiento',
|
||||
'Telephone, internet, TV, computer' => 'Teléfono, internet, TV, ordenador',
|
||||
'Water' => 'Agua',
|
||||
'Other utility expenses' => 'Otros gastos del hogar',
|
||||
'Household goods' => 'Artículos del hogar',
|
||||
'Transportation' => 'Transporte',
|
||||
'Parking' => 'Aparcamiento',
|
||||
'Fuel' => 'Combustible',
|
||||
'Transportation expenses' => 'Gastos de transporte',
|
||||
'Vehicle purchase, maintenance' => 'Compra y mantenimiento de vehículo',
|
||||
'Clothing and shoes' => 'Ropa y calzado',
|
||||
'Leisure activities, traveling' => 'Ocio y viajes',
|
||||
'Gifts' => 'Regalos',
|
||||
'Books, newspapers, magazines' => 'Libros, periódicos, revistas',
|
||||
'Accommodation, travel expenses' => 'Alojamiento y gastos de viaje',
|
||||
'Sport and sports goods' => 'Deporte y artículos deportivos',
|
||||
'Theatre, music, cinema' => 'Teatro, música, cine',
|
||||
'Hobbies and other leisure time activites' => 'Hobbies y otras actividades de ocio',
|
||||
'Education, health and beauty' => 'Educación, salud y belleza',
|
||||
'Education and courses' => 'Educación y cursos',
|
||||
'Beauty, cosmetics' => 'Belleza y cosmética',
|
||||
'Health and pharmaceuticals' => 'Salud y farmacia',
|
||||
'Online transactions' => 'Transacciones en línea',
|
||||
'Online services' => 'Servicios en línea',
|
||||
'Insurance' => 'Seguros',
|
||||
'Investments' => 'Inversiones',
|
||||
'Savings' => 'Ahorros',
|
||||
'Other investments' => 'Otras inversiones',
|
||||
'Financial services and commission' => 'Servicios financieros y comisiones',
|
||||
'Fines' => 'Multas',
|
||||
'Mortgage' => 'Hipoteca',
|
||||
'Credit card repayment' => 'Pago de tarjeta de crédito',
|
||||
'Cash withdrawal' => 'Retiro de efectivo',
|
||||
'Gambling' => 'Apuestas',
|
||||
'Lottery' => 'Lotería',
|
||||
'Taxes and government fees' => 'Impuestos y tasas',
|
||||
'Invoices' => 'Facturas',
|
||||
'Personal transfers' => 'Transferencias personales',
|
||||
'Other personal transfers' => 'Otras transferencias personales',
|
||||
'Administrative violations' => 'Infracciones administrativas',
|
||||
'Other transfers' => 'Otras transferencias',
|
||||
'Other payments' => 'Otros pagos',
|
||||
'Salary' => 'Salario',
|
||||
'Regular income' => 'Ingresos regulares',
|
||||
'Work on demand' => 'Trabajo por encargo',
|
||||
'Income from rent' => 'Ingresos por alquiler',
|
||||
'Unemployment benefit' => 'Prestación por desempleo',
|
||||
'Tax return' => 'Devolución de impuestos',
|
||||
'Return debit' => 'Devolución de débito',
|
||||
'Own account' => 'Cuenta propia',
|
||||
'From account of relatives' => 'Desde cuenta de familiares',
|
||||
'Returned payments' => 'Pagos devueltos',
|
||||
'Credit cards' => 'Tarjetas de crédito',
|
||||
'Self-Employment Income' => 'Ingresos por trabajo autónomo',
|
||||
'Other incoming payments' => 'Otros ingresos',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@
|
|||
|
||||
namespace App\Actions\Fortify;
|
||||
|
||||
use App\Enums\Locale;
|
||||
use App\Models\User;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
|
@ -19,8 +18,6 @@ class CreateNewUser implements CreatesNewUsers
|
|||
*/
|
||||
public function create(array $input): User
|
||||
{
|
||||
abort_if(! config('auth.registration_enabled'), 403);
|
||||
|
||||
Validator::make($input, [
|
||||
'name' => ['required', 'string', 'max:255'],
|
||||
'email' => [
|
||||
|
|
@ -31,38 +28,12 @@ class CreateNewUser implements CreatesNewUsers
|
|||
Rule::unique(User::class),
|
||||
],
|
||||
'password' => $this->passwordRules(),
|
||||
'timezone' => ['nullable', 'string', 'max:255'],
|
||||
])->validate();
|
||||
|
||||
$user = User::create([
|
||||
return User::create([
|
||||
'name' => $input['name'],
|
||||
'email' => $input['email'],
|
||||
'password' => $input['password'],
|
||||
'locale' => Locale::detectFromHeader(request()->header('Accept-Language'))->value,
|
||||
'timezone' => $this->normalizeTimezone($input['timezone'] ?? null),
|
||||
]);
|
||||
|
||||
if (! config('mail.email_verification_enabled')) {
|
||||
$user->markEmailAsVerified();
|
||||
}
|
||||
|
||||
return $user;
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize a browser-detected timezone, discarding identifiers PHP does
|
||||
* not recognize so a hidden auto-detected field can never block registration.
|
||||
*/
|
||||
protected function normalizeTimezone(?string $timezone): ?string
|
||||
{
|
||||
if ($timezone === null || $timezone === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (! in_array($timezone, timezone_identifiers_list(\DateTimeZone::ALL_WITH_BC), true)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $timezone;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@
|
|||
|
||||
namespace App\Actions\Fortify;
|
||||
|
||||
use Illuminate\Contracts\Validation\Rule;
|
||||
use Illuminate\Validation\Rules\Password;
|
||||
|
||||
trait PasswordValidationRules
|
||||
|
|
@ -10,7 +9,7 @@ trait PasswordValidationRules
|
|||
/**
|
||||
* Get the validation rules used to validate passwords.
|
||||
*
|
||||
* @return array<int, Rule|array<mixed>|string>
|
||||
* @return array<int, \Illuminate\Contracts\Validation\Rule|array<mixed>|string>
|
||||
*/
|
||||
protected function passwordRules(): array
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,44 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Actions\OpenBanking;
|
||||
|
||||
use App\Contracts\BankingProviderInterface;
|
||||
use App\Enums\BankingConnectionStatus;
|
||||
use App\Models\BankingConnection;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class DisconnectBankingConnection
|
||||
{
|
||||
public function __construct(private BankingProviderInterface $provider) {}
|
||||
|
||||
public function handle(BankingConnection $connection, bool $deleteAccounts = false): void
|
||||
{
|
||||
if ($connection->isEnableBanking() && $connection->session_id && $connection->isActive()) {
|
||||
try {
|
||||
$this->provider->revokeSession($connection->session_id);
|
||||
} catch (\Throwable $e) {
|
||||
Log::warning('Failed to revoke EnableBanking session', [
|
||||
'connection_id' => $connection->id,
|
||||
'session_id' => $connection->session_id,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
if ($deleteAccounts) {
|
||||
$connection->accounts->each(function ($account): void {
|
||||
$account->transactions()->delete();
|
||||
$account->balances()->delete();
|
||||
$account->delete();
|
||||
});
|
||||
} else {
|
||||
$connection->accounts()->update([
|
||||
'banking_connection_id' => null,
|
||||
'external_account_id' => null,
|
||||
]);
|
||||
}
|
||||
|
||||
$connection->update(['status' => BankingConnectionStatus::Revoked]);
|
||||
$connection->delete();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,54 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Actions\Subscription;
|
||||
|
||||
use App\Actions\OpenBanking\DisconnectBankingConnection;
|
||||
use App\Models\BankingConnection;
|
||||
use App\Models\User;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
/**
|
||||
* Self-service "money-back guarantee" for the pay_now experiment variant:
|
||||
* refund the upfront charge, cancel the subscription immediately, and revoke
|
||||
* the user's bank connections (keeping the data they already imported).
|
||||
*
|
||||
* Eligibility is enforced by the caller via ExperimentOffer::canSelfRefund().
|
||||
* The refund is stamped before the cancel/disconnect steps run so that a
|
||||
* failure in those steps can never leave a refunded-but-active subscription
|
||||
* that could be refunded a second time; the cleanup is best-effort and logged.
|
||||
*/
|
||||
class RefundSelfServe
|
||||
{
|
||||
public function __construct(private DisconnectBankingConnection $disconnect) {}
|
||||
|
||||
public function handle(User $user): void
|
||||
{
|
||||
$subscription = $user->subscription('default');
|
||||
|
||||
if ($subscription === null || $subscription->refunded_at !== null) {
|
||||
return;
|
||||
}
|
||||
|
||||
$payment = $subscription->latestPayment();
|
||||
|
||||
if ($payment !== null) {
|
||||
$user->refund($payment->asStripePaymentIntent()->id);
|
||||
}
|
||||
|
||||
$subscription->forceFill(['refunded_at' => now()])->save();
|
||||
|
||||
try {
|
||||
$subscription->cancelNow();
|
||||
|
||||
$user->bankingConnections()->get()->each(function (BankingConnection $connection): void {
|
||||
$this->disconnect->handle($connection, deleteAccounts: false);
|
||||
});
|
||||
} catch (\Throwable $exception) {
|
||||
Log::error('Self-serve refund issued but post-refund cleanup failed', [
|
||||
'user_id' => $user->getKey(),
|
||||
'subscription_id' => $subscription->getKey(),
|
||||
'error' => $exception->getMessage(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,76 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Ai\Agents;
|
||||
|
||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||
use Laravel\Ai\Contracts\Agent;
|
||||
use Laravel\Ai\Contracts\HasStructuredOutput;
|
||||
use Laravel\Ai\Promptable;
|
||||
use Stringable;
|
||||
|
||||
/**
|
||||
* Maps pre-aggregated transaction groups to categorization rules. The agent
|
||||
* only ever sees merchant/description signals and the user's own category list
|
||||
* — never full account context — and returns a strictly-typed suggestion set.
|
||||
*/
|
||||
class RuleSuggestionAgent implements Agent, HasStructuredOutput
|
||||
{
|
||||
use Promptable;
|
||||
|
||||
public function instructions(): Stringable|string
|
||||
{
|
||||
return <<<'PROMPT'
|
||||
You help a personal-finance app suggest automation rules that auto-categorize
|
||||
a user's transactions. You are given a JSON object with:
|
||||
- "transaction_groups": recurring groups of uncategorized transactions. Each has
|
||||
a "field" (description, creditor_name or debtor_name), a "key", an example
|
||||
"samples" list, an occurrence "count", an "avg_amount" and a "direction"
|
||||
(outflow = money spent, inflow = money received).
|
||||
- "categories": the user's existing categories, each with an "id", a "path"
|
||||
(parent > child), a "type" and a "direction". Prefer leaf categories.
|
||||
|
||||
For each group that clearly belongs to a single category, return one suggestion:
|
||||
- "group_key": echo the group's "key".
|
||||
- "match_field": which field to match on (use the group's field).
|
||||
- "match_operator": "contains" for free-text descriptions, "equals" for clean
|
||||
counterparty names.
|
||||
- "match_token": a short, lowercase, DISTINCTIVE substring that appears VERBATIM
|
||||
in the group's samples (e.g. "mercadona", "netflix"). Never invent text that is
|
||||
not present. Never use a token so generic it would match unrelated transactions
|
||||
(avoid words like "compra", "payment", "card").
|
||||
- "category_id": the id of the best-fitting existing category. An outflow group
|
||||
must map to a spending category, an inflow group to an income category.
|
||||
- If, and only if, no existing category fits, leave "category_id" empty and instead
|
||||
propose "new_category_name" and "new_category_direction" (inflow or outflow).
|
||||
- "confidence": 0.0–1.0, how sure you are.
|
||||
|
||||
Aim for broad coverage: return a suggestion for EVERY group you can map to a
|
||||
category with reasonable confidence. Only skip a group when it is genuinely
|
||||
ambiguous — for example internal transfers between the user's own accounts, or a
|
||||
catch-all group mixing unrelated transactions. Never invent a category you are
|
||||
unsure about; instead let "confidence" honestly reflect your certainty (the app
|
||||
filters out low-confidence suggestions itself).
|
||||
PROMPT;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function schema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'suggestions' => $schema->array()->items(
|
||||
$schema->object(fn (JsonSchema $schema): array => [
|
||||
'group_key' => $schema->string()->required(),
|
||||
'match_field' => $schema->string()->enum(['description', 'creditor_name', 'debtor_name'])->required(),
|
||||
'match_operator' => $schema->string()->enum(['contains', 'equals'])->required(),
|
||||
'match_token' => $schema->string()->required(),
|
||||
'category_id' => $schema->string(),
|
||||
'new_category_name' => $schema->string(),
|
||||
'new_category_direction' => $schema->string()->enum(['inflow', 'outflow']),
|
||||
'confidence' => $schema->number()->required(),
|
||||
])
|
||||
)->required(),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
@ -1,66 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Ai\Agents;
|
||||
|
||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||
use Laravel\Ai\Contracts\Agent;
|
||||
use Laravel\Ai\Contracts\HasStructuredOutput;
|
||||
use Laravel\Ai\Promptable;
|
||||
use Stringable;
|
||||
|
||||
/**
|
||||
* Assigns each given transaction to one of the user's existing leaf categories.
|
||||
* The agent only ever sees merchant/description signals and the user's own
|
||||
* category list — never full account context. Categories are referenced by a
|
||||
* numeric "index" (not their id) so the model cannot hallucinate an identifier,
|
||||
* and the caller maps the index back to a real category.
|
||||
*/
|
||||
class TransactionCategorizationAgent implements Agent, HasStructuredOutput
|
||||
{
|
||||
use Promptable;
|
||||
|
||||
public function instructions(): Stringable|string
|
||||
{
|
||||
return <<<'PROMPT'
|
||||
You categorize a personal-finance app user's bank transactions. You are given a
|
||||
JSON object with:
|
||||
- "transactions": the transactions to categorize. Each has a "ref" (echo it back
|
||||
verbatim), a "text" (the bank description), an "amount", a "direction"
|
||||
(outflow = money spent, inflow = money received) and optional "creditor_name"
|
||||
/ "debtor_name" counterparties.
|
||||
- "categories": the user's existing LEAF categories. Each has an "index", a "path"
|
||||
(parent > child), a "type" and a "direction". You may ONLY choose from these.
|
||||
|
||||
For each transaction you can confidently place, return one result:
|
||||
- "ref": echo the transaction's "ref".
|
||||
- "category_index": the "index" of the best-fitting category. An outflow MUST map
|
||||
to a spending category, an inflow to an income category. If no category fits,
|
||||
OMIT this field (do not guess).
|
||||
- "confidence": 0.0–1.0, how sure you are of this category for THIS transaction.
|
||||
- "merchant_unambiguous": true only if the counterparty/merchant reliably maps to
|
||||
this ONE category for every future transaction (e.g. "Netflix" → Subscriptions,
|
||||
"Mercadona" → Groceries). false when the right category depends on the specific
|
||||
purchase (e.g. "Amazon", "PayPal", a generic bank transfer, an ATM withdrawal).
|
||||
|
||||
Let "confidence" honestly reflect your certainty — the app filters out low-confidence
|
||||
results itself. Never invent a category that is not in the provided list.
|
||||
PROMPT;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function schema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'results' => $schema->array()->items(
|
||||
$schema->object(fn (JsonSchema $schema): array => [
|
||||
'ref' => $schema->string()->required(),
|
||||
'category_index' => $schema->integer(),
|
||||
'confidence' => $schema->number()->required(),
|
||||
'merchant_unambiguous' => $schema->boolean()->required(),
|
||||
])
|
||||
)->required(),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
@ -1,57 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Throwable;
|
||||
|
||||
class AgentDatabaseCommand extends Command
|
||||
{
|
||||
protected $signature = 'agent:db
|
||||
{query : The SQL query to run}
|
||||
{--format=json : Output format: "json" or "table"}
|
||||
{--prod : Run against the production database connection}';
|
||||
|
||||
protected $description = 'Run a database query and return the result in the selected format';
|
||||
|
||||
public function handle(): int
|
||||
{
|
||||
$format = strtolower((string) $this->option('format'));
|
||||
|
||||
if (! in_array($format, ['json', 'table'], true)) {
|
||||
$this->error('Invalid format. Use "json" or "table".');
|
||||
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
$connection = $this->option('prod') ? 'prod' : config('database.default');
|
||||
|
||||
try {
|
||||
$rows = array_map(
|
||||
static fn (object $row): array => (array) $row,
|
||||
DB::connection($connection)->select($this->argument('query')),
|
||||
);
|
||||
} catch (Throwable $exception) {
|
||||
$this->error($exception->getMessage());
|
||||
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
if ($format === 'json') {
|
||||
$this->line((string) json_encode($rows, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
if ($rows === []) {
|
||||
$this->info('Empty result set.');
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
$this->table(array_keys($rows[0]), $rows);
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,73 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Models\RealEstateDetail;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
class ApplyRealEstateRevaluationCommand extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'real-estate:apply-revaluation';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Apply monthly revaluation to real estate accounts based on their annual revaluation percentage';
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*/
|
||||
public function handle(): int
|
||||
{
|
||||
$details = RealEstateDetail::query()
|
||||
->whereNotNull('revaluation_percentage')
|
||||
->where('revaluation_percentage', '!=', 0)
|
||||
->with('account')
|
||||
->get();
|
||||
|
||||
$applied = 0;
|
||||
$skipped = 0;
|
||||
|
||||
foreach ($details as $detail) {
|
||||
$account = $detail->account;
|
||||
|
||||
if (! $account) {
|
||||
$skipped++;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$latestBalance = $account->balances()
|
||||
->orderByDesc('balance_date')
|
||||
->first();
|
||||
|
||||
if (! $latestBalance) {
|
||||
$skipped++;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$annualRate = (float) $detail->revaluation_percentage / 100;
|
||||
$monthlyMultiplier = (1 + $annualRate) ** (1 / 12);
|
||||
$newBalance = (int) round($latestBalance->balance * $monthlyMultiplier);
|
||||
|
||||
$account->balances()->updateOrCreate(
|
||||
['balance_date' => now()->toDateString()],
|
||||
['balance' => $newBalance],
|
||||
);
|
||||
|
||||
$applied++;
|
||||
}
|
||||
|
||||
$this->info("Revaluation applied to {$applied} account(s), skipped {$skipped}.");
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,114 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Contracts\BankingProviderInterface;
|
||||
use App\Enums\BankingProvider;
|
||||
use App\Models\Account;
|
||||
use App\Models\User;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Http\Client\RequestException;
|
||||
use Throwable;
|
||||
|
||||
class BackfillAccountIbans extends Command
|
||||
{
|
||||
protected $signature = 'banking:backfill-ibans
|
||||
{--user= : Filter by user email address}
|
||||
{--connection= : Filter by banking connection ID}
|
||||
{--dry-run : Preview what would be updated without making changes}';
|
||||
|
||||
protected $description = 'Backfill missing IBAN values for Enable Banking accounts by fetching them from the API';
|
||||
|
||||
public function handle(BankingProviderInterface $provider): int
|
||||
{
|
||||
$isDryRun = $this->option('dry-run');
|
||||
$userEmail = $this->option('user');
|
||||
$connectionId = $this->option('connection');
|
||||
|
||||
if ($isDryRun) {
|
||||
$this->warn('DRY RUN — no changes will be saved.');
|
||||
}
|
||||
|
||||
$query = Account::query()
|
||||
->whereNull('iban')
|
||||
->whereNotNull('external_account_id')
|
||||
->whereNotNull('banking_connection_id')
|
||||
->whereHas('bankingConnection', fn ($q) => $q->where('provider', BankingProvider::EnableBanking));
|
||||
|
||||
if ($connectionId) {
|
||||
$query->where('banking_connection_id', $connectionId);
|
||||
}
|
||||
|
||||
if ($userEmail) {
|
||||
$user = User::query()->where('email', $userEmail)->first();
|
||||
|
||||
if (! $user) {
|
||||
$this->error("User with email '{$userEmail}' not found.");
|
||||
|
||||
return Command::FAILURE;
|
||||
}
|
||||
|
||||
$query->where('user_id', $user->id);
|
||||
}
|
||||
|
||||
$accounts = $query->get();
|
||||
|
||||
if ($accounts->isEmpty()) {
|
||||
$this->info('No accounts found with missing IBAN.');
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
$this->info("Found {$accounts->count()} account(s) with missing IBAN.");
|
||||
|
||||
$bar = $this->output->createProgressBar($accounts->count());
|
||||
$bar->start();
|
||||
|
||||
$updated = 0;
|
||||
$skipped = 0;
|
||||
$expiredSessions = 0;
|
||||
$failed = 0;
|
||||
|
||||
foreach ($accounts as $account) {
|
||||
try {
|
||||
$data = $provider->getAccount($account->external_account_id);
|
||||
$iban = $data['account_id']['iban'] ?? null;
|
||||
|
||||
if (! $iban) {
|
||||
$skipped++;
|
||||
$bar->advance();
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (! $isDryRun) {
|
||||
$account->update(['iban' => $iban]);
|
||||
}
|
||||
|
||||
$updated++;
|
||||
} catch (RequestException $e) {
|
||||
if ($e->response->status() === 404) {
|
||||
$expiredSessions++;
|
||||
} else {
|
||||
$this->newLine();
|
||||
$this->warn("Failed for account {$account->id} ({$account->external_account_id}): {$e->getMessage()}");
|
||||
$failed++;
|
||||
}
|
||||
} catch (Throwable $e) {
|
||||
$this->newLine();
|
||||
$this->warn("Failed for account {$account->id} ({$account->external_account_id}): {$e->getMessage()}");
|
||||
$failed++;
|
||||
}
|
||||
|
||||
$bar->advance();
|
||||
}
|
||||
|
||||
$bar->finish();
|
||||
$this->newLine();
|
||||
|
||||
$verb = $isDryRun ? 'would be updated' : 'updated';
|
||||
$this->info("IBAN {$verb} for {$updated} account(s). Skipped (no IBAN in API response): {$skipped}. Skipped (expired/revoked session): {$expiredSessions}. Failed: {$failed}.");
|
||||
|
||||
return $failed > 0 ? Command::FAILURE : Command::SUCCESS;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,82 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Models\Account;
|
||||
use App\Models\AutomationRule;
|
||||
use App\Models\BankingConnection;
|
||||
use App\Models\Budget;
|
||||
use App\Models\Category;
|
||||
use App\Models\Label;
|
||||
use App\Models\SavedFilter;
|
||||
use App\Models\Space;
|
||||
use App\Models\Transaction;
|
||||
use App\Models\User;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class BackfillSpaces extends Command
|
||||
{
|
||||
protected $signature = 'spaces:backfill {--chunk=500 : Users processed per batch}';
|
||||
|
||||
protected $description = 'Provision a personal space for every user and stamp their existing rows with it';
|
||||
|
||||
/**
|
||||
* Owned tables to backfill. Each row inherits the personal space of its
|
||||
* user_id. Idempotent: only rows with a null space_id are touched.
|
||||
*
|
||||
* @var list<class-string<Model>>
|
||||
*/
|
||||
private array $models = [
|
||||
Account::class,
|
||||
BankingConnection::class,
|
||||
Transaction::class,
|
||||
Category::class,
|
||||
Label::class,
|
||||
Budget::class,
|
||||
AutomationRule::class,
|
||||
SavedFilter::class,
|
||||
];
|
||||
|
||||
public function handle(): int
|
||||
{
|
||||
$chunk = (int) $this->option('chunk');
|
||||
|
||||
// Soft-deleted users are included: their accounts/transactions still
|
||||
// exist and must be stamped too, so a restored account keeps its data
|
||||
// and the column can eventually go NOT NULL.
|
||||
$this->info('Provisioning personal spaces…');
|
||||
User::withTrashed()->whereNull('current_space_id')->chunkById($chunk, function ($users): void {
|
||||
foreach ($users as $user) {
|
||||
$user->provisionPersonalSpace();
|
||||
}
|
||||
});
|
||||
|
||||
$this->info('Backfilling space_id on owned rows…');
|
||||
Space::query()->where('personal', true)->chunkById($chunk, function ($spaces): void {
|
||||
foreach ($spaces as $space) {
|
||||
foreach ($this->models as $model) {
|
||||
$this->stamp($model, $space->owner_id, $space->id);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
$this->info('Done.');
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param class-string<Model> $model
|
||||
*/
|
||||
private function stamp(string $model, string $ownerId, string $spaceId): void
|
||||
{
|
||||
// Go through the query builder rather than Eloquent so soft-deleted rows
|
||||
// are stamped too (no soft-delete global scope to work around).
|
||||
DB::table((new $model)->getTable())
|
||||
->whereNull('space_id')
|
||||
->where('user_id', $ownerId)
|
||||
->update(['space_id' => $spaceId]);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,70 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
class BackfillUserCurrencyCode extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'app:backfill-user-currency-code';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Backfill currency_code for users based on their first account';
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$this->info('Starting currency code backfill...');
|
||||
|
||||
// Get all users without a currency_code
|
||||
$users = User::whereNull('currency_code')
|
||||
->has('accounts')
|
||||
->with(['accounts' => function ($query) {
|
||||
$query->orderBy('created_at', 'asc')->limit(1);
|
||||
}])
|
||||
->get();
|
||||
|
||||
if ($users->isEmpty()) {
|
||||
$this->info('No users found that need currency code backfill.');
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
$this->info("Found {$users->count()} users to update.");
|
||||
|
||||
$bar = $this->output->createProgressBar($users->count());
|
||||
$bar->start();
|
||||
|
||||
$updated = 0;
|
||||
|
||||
foreach ($users as $user) {
|
||||
$firstAccount = $user->accounts->first();
|
||||
|
||||
if ($firstAccount && $firstAccount->currency_code) {
|
||||
$user->update(['currency_code' => $firstAccount->currency_code]);
|
||||
$updated++;
|
||||
}
|
||||
|
||||
$bar->advance();
|
||||
}
|
||||
|
||||
$bar->finish();
|
||||
$this->newLine();
|
||||
|
||||
$this->info("Successfully updated {$updated} users with currency codes.");
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,74 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Actions\OpenBanking\DisconnectBankingConnection;
|
||||
use App\Enums\BankingConnectionStatus;
|
||||
use App\Enums\BankingProvider;
|
||||
use App\Mail\EnableBankingConnectionsCancelledEmail;
|
||||
use App\Models\BankingConnection;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\Mail;
|
||||
|
||||
class CancelFreeEnableBankingConnectionsCommand extends Command
|
||||
{
|
||||
protected $signature = 'banking:cancel-free-enablebanking';
|
||||
|
||||
protected $description = 'Close Enable Banking connections for free users at month end';
|
||||
|
||||
public function handle(DisconnectBankingConnection $disconnectBankingConnection): int
|
||||
{
|
||||
$cutoff = now()->subHours(6);
|
||||
|
||||
$connections = BankingConnection::query()
|
||||
->with(['user', 'accounts'])
|
||||
->whereHas('user')
|
||||
->where('provider', BankingProvider::EnableBanking)
|
||||
->where('status', '!=', BankingConnectionStatus::Revoked)
|
||||
->where('created_at', '<=', $cutoff)
|
||||
->get();
|
||||
|
||||
$count = $connections->count();
|
||||
|
||||
if ($count === 0) {
|
||||
$this->info('No eligible Enable Banking connections found for free users.');
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
$revoked = 0;
|
||||
$skipped = 0;
|
||||
|
||||
$connections
|
||||
->groupBy('user_id')
|
||||
->each(function ($userConnections) use ($disconnectBankingConnection, &$revoked, &$skipped): void {
|
||||
$user = $userConnections->first()?->user;
|
||||
|
||||
if (! $user) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($user->hasProPlan()) {
|
||||
$skipped += $userConnections->count();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($userConnections as $connection) {
|
||||
$disconnectBankingConnection->handle($connection);
|
||||
$revoked++;
|
||||
}
|
||||
|
||||
if ($user->canReceiveEmails()) {
|
||||
Mail::to($user)->send(new EnableBankingConnectionsCancelledEmail(
|
||||
$user,
|
||||
$userConnections->count(),
|
||||
));
|
||||
}
|
||||
});
|
||||
|
||||
$this->info("Revoked {$revoked} Enable Banking connection(s). Skipped paid users: {$skipped}.");
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,87 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Enums\RuleOrigin;
|
||||
use App\Models\AutomationRule;
|
||||
use App\Models\Transaction;
|
||||
use App\Models\User;
|
||||
use App\Services\Ai\AiCategorizationGate;
|
||||
use App\Services\Ai\AiCategorizer;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
|
||||
class CategorizeBackfillCommand extends Command
|
||||
{
|
||||
protected $signature = 'ai:categorize-backfill {user : User id or email}';
|
||||
|
||||
protected $description = 'Categorize a user\'s existing uncategorized transactions with AI (explicit, opt-in backfill)';
|
||||
|
||||
public function handle(AiCategorizationGate $gate, AiCategorizer $categorizer): int
|
||||
{
|
||||
$user = $this->resolveUser((string) $this->argument('user'));
|
||||
|
||||
if ($user === null) {
|
||||
$this->error('User not found.');
|
||||
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
if (! $gate->allows($user)) {
|
||||
$this->warn('User is not eligible (needs the feature enabled, a pro plan and active AI consent).');
|
||||
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
$pendingIds = Transaction::query()
|
||||
->where('user_id', $user->id)
|
||||
->whereNull('category_id')
|
||||
->whereNull('description_iv')
|
||||
->pluck('id');
|
||||
|
||||
if ($pendingIds->isEmpty()) {
|
||||
$this->info('No uncategorized transactions to backfill.');
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
$rulesBefore = $this->aiRuleCount($user);
|
||||
$applied = 0;
|
||||
$batchSize = max(1, (int) config('ai_categorization.group_batch_size'));
|
||||
|
||||
// Chunk a fixed snapshot of ids so transactions left blank (below the
|
||||
// confidence bar) are never re-processed on a later iteration.
|
||||
$this->withProgressBar(
|
||||
$pendingIds->chunk($batchSize),
|
||||
function ($chunkIds) use ($user, $categorizer, &$applied): void {
|
||||
$chunk = Transaction::query()->whereIn('id', $chunkIds->all())->get();
|
||||
|
||||
$outcomes = $categorizer->run($user, new Collection($chunk->all()));
|
||||
$applied += count(array_filter($outcomes, fn ($outcome): bool => $outcome->applied));
|
||||
},
|
||||
);
|
||||
|
||||
$this->newLine(2);
|
||||
$this->components->info(sprintf(
|
||||
'Backfill complete: %d transaction(s) categorized, %d AI rule(s) learned.',
|
||||
$applied,
|
||||
$this->aiRuleCount($user) - $rulesBefore,
|
||||
));
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
private function aiRuleCount(User $user): int
|
||||
{
|
||||
return AutomationRule::query()
|
||||
->where('user_id', $user->id)
|
||||
->origin(RuleOrigin::Ai)
|
||||
->count();
|
||||
}
|
||||
|
||||
private function resolveUser(string $identifier): ?User
|
||||
{
|
||||
return User::query()->where('email', $identifier)->first()
|
||||
?? User::query()->find($identifier);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,112 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Mail\BrokenBankLogosReportEmail;
|
||||
use App\Models\Bank;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Http\Client\ConnectionException;
|
||||
use Illuminate\Http\Client\Response;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Facades\Mail;
|
||||
|
||||
class CheckBankLogosCommand extends Command
|
||||
{
|
||||
protected $signature = 'banks:check-logos';
|
||||
|
||||
protected $description = 'Validate bank logo URLs and clear broken links';
|
||||
|
||||
public function handle(): int
|
||||
{
|
||||
$banks = Bank::query()
|
||||
->whereNotNull('logo')
|
||||
->get(['id', 'name', 'logo']);
|
||||
|
||||
if ($banks->isEmpty()) {
|
||||
$this->info('No bank logos found to validate.');
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
$updatedBanks = [];
|
||||
|
||||
foreach ($banks as $bank) {
|
||||
$logoUrl = (string) $bank->logo;
|
||||
|
||||
if ($this->hasWorkingImage($logoUrl)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$bank->update(['logo' => null]);
|
||||
|
||||
$updatedBanks[] = [
|
||||
'id' => $bank->id,
|
||||
'name' => $bank->name,
|
||||
'previous_logo' => $logoUrl,
|
||||
];
|
||||
|
||||
$this->warn("Cleared broken logo for {$bank->name}.");
|
||||
}
|
||||
|
||||
if ($updatedBanks === []) {
|
||||
$this->info('All bank logos are valid.');
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
$updatedCount = count($updatedBanks);
|
||||
|
||||
$this->info("Cleared broken logos for {$updatedCount} bank(s).");
|
||||
|
||||
$adminEmail = (string) config('mail.admin_email');
|
||||
|
||||
if ($adminEmail === '') {
|
||||
$this->warn('ADMIN_EMAIL is not configured. Skipping report email.');
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
Mail::to($adminEmail)->send(new BrokenBankLogosReportEmail($updatedBanks));
|
||||
|
||||
$this->info("Sent broken logo report to {$adminEmail}.");
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
private function hasWorkingImage(string $logoUrl): bool
|
||||
{
|
||||
if (! filter_var($logoUrl, FILTER_VALIDATE_URL)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
$headResponse = Http::timeout(10)->head($logoUrl);
|
||||
|
||||
if ($headResponse->successful() && $this->isImageResponse($headResponse)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ($headResponse->failed() && $headResponse->status() !== 405) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$getResponse = Http::timeout(10)->get($logoUrl);
|
||||
|
||||
return $getResponse->successful() && $this->isImageResponse($getResponse);
|
||||
} catch (ConnectionException) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private function isImageResponse(Response $response): bool
|
||||
{
|
||||
$contentType = strtolower((string) $response->header('Content-Type'));
|
||||
|
||||
if ($contentType === '') {
|
||||
return false;
|
||||
}
|
||||
|
||||
return str_starts_with($contentType, 'image/')
|
||||
|| str_contains($contentType, 'application/octet-stream');
|
||||
}
|
||||
}
|
||||
|
|
@ -1,43 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Console\Commands\Concerns;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
trait FindsUsersWithLegacyEncryption
|
||||
{
|
||||
/**
|
||||
* Users who still have at least one client-side encrypted transaction or account
|
||||
* (a non-null `*_iv` column is the source of truth, not the stale `accounts.encrypted` flag).
|
||||
*
|
||||
* Subscriptions are eager-loaded so callers can filter with {@see excludeBilledUsers()}
|
||||
* without an N+1 query.
|
||||
*
|
||||
* @return Builder<User>
|
||||
*/
|
||||
protected function usersWithLegacyEncryption(): Builder
|
||||
{
|
||||
return User::query()->with('subscriptions')->where(function (Builder $query): void {
|
||||
$query->whereHas('transactions', function (Builder $transactions): void {
|
||||
$transactions->whereNotNull('description_iv')
|
||||
->orWhereNotNull('notes_iv');
|
||||
})->orWhereHas('accounts', function (Builder $accounts): void {
|
||||
$accounts->whereNotNull('name_iv');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Drop users who are still being billed. These accounts must never be emailed a
|
||||
* deletion warning nor deleted while a subscription or trial is active.
|
||||
*
|
||||
* @param Collection<int, User> $users
|
||||
* @return Collection<int, User>
|
||||
*/
|
||||
protected function excludeBilledUsers(Collection $users): Collection
|
||||
{
|
||||
return $users->reject(fn (User $user): bool => $user->hasActiveSubscriptionOrTrial())->values();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,45 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Console\Commands\Concerns;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
/**
|
||||
* Render Discord report embeds as plain console output, for the --no-discord
|
||||
* flag on the stats:* report commands.
|
||||
*
|
||||
* @mixin Command
|
||||
*/
|
||||
trait RendersReportToConsole
|
||||
{
|
||||
/**
|
||||
* @param array<int, array<string, mixed>> $embeds
|
||||
*/
|
||||
protected function printEmbeds(array $embeds): void
|
||||
{
|
||||
foreach ($embeds as $embed) {
|
||||
if (! empty($embed['title'])) {
|
||||
$this->newLine();
|
||||
$this->line('<options=bold>'.$this->plainText($embed['title']).'</>');
|
||||
}
|
||||
|
||||
if (! empty($embed['description'])) {
|
||||
$this->line($this->plainText($embed['description']));
|
||||
}
|
||||
|
||||
foreach ($embed['fields'] ?? [] as $field) {
|
||||
$this->newLine();
|
||||
$this->line('<options=bold>'.$this->plainText($field['name']).'</>');
|
||||
$this->line($this->plainText($field['value']));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Strip Discord markdown (code fences, bold) for readable console output.
|
||||
*/
|
||||
private function plainText(string $text): string
|
||||
{
|
||||
return trim(str_replace(['```', '**'], '', $text));
|
||||
}
|
||||
}
|
||||
|
|
@ -14,41 +14,24 @@ trait ResolvesFeatures
|
|||
return $featureClass;
|
||||
}
|
||||
|
||||
if ($this->isStringBasedFeature($name)) {
|
||||
return $name;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private function getAvailableFeatures(): string
|
||||
{
|
||||
$features = [];
|
||||
|
||||
$featuresPath = app_path('Features');
|
||||
|
||||
if (File::isDirectory($featuresPath)) {
|
||||
$files = File::files($featuresPath);
|
||||
|
||||
foreach ($files as $file) {
|
||||
$features[] = $file->getFilenameWithoutExtension();
|
||||
}
|
||||
if (! File::isDirectory($featuresPath)) {
|
||||
return 'None';
|
||||
}
|
||||
|
||||
$stringFeatures = $this->getStringBasedFeatures();
|
||||
$files = File::files($featuresPath);
|
||||
$features = [];
|
||||
|
||||
$allFeatures = array_merge($features, $stringFeatures);
|
||||
foreach ($files as $file) {
|
||||
$features[] = $file->getFilenameWithoutExtension();
|
||||
}
|
||||
|
||||
return implode(', ', array_unique($allFeatures)) ?: 'None';
|
||||
}
|
||||
|
||||
private function isStringBasedFeature(string $name): bool
|
||||
{
|
||||
return in_array($name, $this->getStringBasedFeatures(), true);
|
||||
}
|
||||
|
||||
private function getStringBasedFeatures(): array
|
||||
{
|
||||
return [];
|
||||
return implode(', ', $features) ?: 'None';
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,38 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
class DeleteAiConsentCommand extends Command
|
||||
{
|
||||
protected $signature = 'ai:delete-consent {email : The email address of the user whose AI consent to delete}';
|
||||
|
||||
protected $description = "Delete a user's AI consent records, as if they had never granted it";
|
||||
|
||||
public function handle(): int
|
||||
{
|
||||
$email = $this->argument('email');
|
||||
|
||||
$user = User::where('email', $email)->first();
|
||||
|
||||
if (! $user) {
|
||||
$this->error("User with email '{$email}' not found.");
|
||||
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
$deleted = $user->aiConsents()->delete();
|
||||
|
||||
if ($deleted === 0) {
|
||||
$this->info("User '{$email}' has no AI consent on record. Nothing to do.");
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
$this->info("Deleted AI consent for '{$email}' ({$deleted} record(s)). It's now as if they had never granted it.");
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,87 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Console\Commands\Concerns\FindsUsersWithLegacyEncryption;
|
||||
use App\Models\User;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
|
||||
class DeleteEncryptedDataAccountsCommand extends Command
|
||||
{
|
||||
use FindsUsersWithLegacyEncryption;
|
||||
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'encryption:delete-accounts
|
||||
{--days=7 : Spare users active within the last N days}
|
||||
{--dry-run : List the accounts that would be deleted without touching them}
|
||||
{--force : Skip the confirmation prompt}';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Soft-delete and anonymize users who still have legacy encrypted data and did not sign in within the grace window';
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*/
|
||||
public function handle(): int
|
||||
{
|
||||
$days = (int) $this->option('days');
|
||||
$cutoff = now()->subDays($days);
|
||||
|
||||
$users = $this->excludeBilledUsers(
|
||||
$this->usersWithLegacyEncryption()
|
||||
->where('email', '!=', config('app.demo.email'))
|
||||
->where(function (Builder $query) use ($cutoff): void {
|
||||
$query->whereNull('last_active_at')
|
||||
->orWhere('last_active_at', '<', $cutoff);
|
||||
})
|
||||
->get()
|
||||
);
|
||||
|
||||
if ($users->isEmpty()) {
|
||||
$this->info('No accounts to delete.');
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
$this->info("Found {$users->count()} account(s) with encrypted data and no activity in the last {$days} day(s).");
|
||||
|
||||
if ($this->option('dry-run')) {
|
||||
$this->table(['Email', 'Last active'], $users->map(fn (User $user) => [
|
||||
$user->email,
|
||||
$user->last_active_at?->toDateTimeString() ?? 'never',
|
||||
])->all());
|
||||
$this->info('[dry-run] No accounts deleted.');
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
if (! $this->option('force') && ! $this->confirm("Soft-delete and anonymize {$users->count()} account(s)?", false)) {
|
||||
$this->info('Cancelled.');
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
$progressBar = $this->output->createProgressBar($users->count());
|
||||
$progressBar->start();
|
||||
|
||||
foreach ($users as $user) {
|
||||
$user->markAsDeleted();
|
||||
$progressBar->advance();
|
||||
}
|
||||
|
||||
$progressBar->finish();
|
||||
$this->newLine();
|
||||
$this->info("Deleted {$users->count()} account(s).");
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,69 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Models\AccountBalance;
|
||||
use App\Models\Transaction;
|
||||
use App\Models\User;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class DeleteManualAccountDataCommand extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'user:delete-manual-account-data {email : The email address of the user}';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Delete all transactions and balances of a user\'s non-connected accounts';
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*/
|
||||
public function handle(): int
|
||||
{
|
||||
$email = $this->argument('email');
|
||||
|
||||
$user = User::withTrashed()->where('email', $email)->first();
|
||||
|
||||
if (! $user) {
|
||||
$this->error("User with email '{$email}' not found.");
|
||||
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
$accountIds = $user->accounts()->whereNull('banking_connection_id')->pluck('id');
|
||||
|
||||
if ($accountIds->isEmpty()) {
|
||||
$this->info("User '{$email}' has no non-connected accounts.");
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
$transactionCount = Transaction::withTrashed()->whereIn('account_id', $accountIds)->count();
|
||||
$balanceCount = AccountBalance::query()->whereIn('account_id', $accountIds)->count();
|
||||
|
||||
if (! $this->confirm("Delete {$transactionCount} transaction(s) and {$balanceCount} balance(s) across {$accountIds->count()} non-connected account(s) of '{$user->email}'?")) {
|
||||
$this->info('Deletion cancelled.');
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
DB::transaction(function () use ($accountIds): void {
|
||||
// forceDelete purges soft-deleted transactions too; balances have no SoftDeletes, so delete() is already a hard delete.
|
||||
Transaction::query()->whereIn('account_id', $accountIds)->forceDelete();
|
||||
AccountBalance::query()->whereIn('account_id', $accountIds)->delete();
|
||||
});
|
||||
|
||||
$this->info("Deleted {$transactionCount} transaction(s) and {$balanceCount} balance(s) for '{$user->email}'.");
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
}
|
||||
|
|
@ -2,11 +2,9 @@
|
|||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Actions\OpenBanking\DisconnectBankingConnection;
|
||||
use App\Enums\BankingProvider;
|
||||
use App\Models\User;
|
||||
use Illuminate\Console\Command;
|
||||
use Laravel\Cashier\Subscription;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class DeleteUserCommand extends Command
|
||||
{
|
||||
|
|
@ -22,16 +20,16 @@ class DeleteUserCommand extends Command
|
|||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Mark a user as deleted while preserving their data';
|
||||
protected $description = 'Delete a user and all their associated data';
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*/
|
||||
public function handle(DisconnectBankingConnection $disconnectBankingConnection): int
|
||||
public function handle(): int
|
||||
{
|
||||
$email = $this->argument('email');
|
||||
|
||||
$user = User::withTrashed()->where('email', $email)->first();
|
||||
$user = User::query()->where('email', $email)->first();
|
||||
|
||||
if (! $user) {
|
||||
$this->error("User with email '{$email}' not found.");
|
||||
|
|
@ -39,75 +37,48 @@ class DeleteUserCommand extends Command
|
|||
return self::FAILURE;
|
||||
}
|
||||
|
||||
if ($user->trashed()) {
|
||||
$this->info("User '{$email}' is already marked as deleted.");
|
||||
// Check for active subscriptions
|
||||
if ($user->subscribed('default')) {
|
||||
$this->error('Cannot delete user with an active subscription. Please cancel the subscription first.');
|
||||
|
||||
return self::SUCCESS;
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
if (! $this->confirm("Are you sure you want to mark user '{$user->name}' ({$user->email}) as deleted? Their data will be preserved.")) {
|
||||
if (! $this->confirm("Are you sure you want to delete user '{$user->name}' ({$user->email}) and all their data?")) {
|
||||
$this->info('Deletion cancelled.');
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
$subscription = $this->activeSubscription($user);
|
||||
$enableBankingConnections = $user->bankingConnections()
|
||||
->with('accounts')
|
||||
->where('provider', BankingProvider::EnableBanking)
|
||||
->get();
|
||||
DB::transaction(function () use ($user) {
|
||||
// Delete account balances through accounts
|
||||
foreach ($user->accounts as $account) {
|
||||
$account->balances()->delete();
|
||||
}
|
||||
|
||||
if ($subscription && ! $this->confirm("User '{$user->email}' has an active Stripe subscription. Cancel it before deleting the user?")) {
|
||||
$this->info('Deletion cancelled.');
|
||||
// Delete all related data
|
||||
$user->encryptedMessage()->delete();
|
||||
$user->transactions()->delete();
|
||||
$user->accounts()->delete();
|
||||
$user->categories()->delete();
|
||||
$user->automationRules()->delete();
|
||||
$user->labels()->delete();
|
||||
$user->mailLogs()->delete();
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
// Delete user's banks
|
||||
DB::table('banks')->where('user_id', $user->id)->delete();
|
||||
|
||||
if ($enableBankingConnections->isNotEmpty() && ! $this->confirm("User '{$user->email}' has {$enableBankingConnections->count()} Enable Banking connection(s). Revoke them and keep linked accounts as manual accounts?")) {
|
||||
$this->info('Deletion cancelled.');
|
||||
// Delete Cashier subscription data if exists
|
||||
if ($user->subscriptions()->exists()) {
|
||||
$user->subscriptions()->delete();
|
||||
}
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
// Delete the user
|
||||
$user->delete();
|
||||
});
|
||||
|
||||
if ($subscription) {
|
||||
$this->cancelSubscription($user, $subscription);
|
||||
$this->info("Cancelled active Stripe subscription for '{$user->email}'.");
|
||||
}
|
||||
|
||||
foreach ($enableBankingConnections as $connection) {
|
||||
$disconnectBankingConnection->handle($connection, deleteAccounts: false);
|
||||
}
|
||||
|
||||
if ($enableBankingConnections->isNotEmpty()) {
|
||||
$this->info("Revoked {$enableBankingConnections->count()} Enable Banking connection(s) for '{$user->email}'.");
|
||||
}
|
||||
|
||||
$user->markAsDeleted();
|
||||
|
||||
$this->info("User '{$email}' has been marked as deleted. Their data remains in the database.");
|
||||
$this->info("User '{$email}' and all associated data have been deleted successfully.");
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
private function activeSubscription(User $user): ?Subscription
|
||||
{
|
||||
$subscription = $user->subscription('default');
|
||||
|
||||
if (! $subscription || ! $subscription->valid()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $subscription;
|
||||
}
|
||||
|
||||
private function cancelSubscription(User $user, Subscription $subscription): void
|
||||
{
|
||||
if ($user->hasStripeId()) {
|
||||
$subscription->cancelNow();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$subscription->markAsCanceled();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,68 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Actions\OpenBanking\DisconnectBankingConnection;
|
||||
use App\Models\BankingConnection;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
class DisconnectBankingConnectionsCommand extends Command
|
||||
{
|
||||
protected $signature = 'banking:disconnect
|
||||
{ids : One or more banking connection IDs, comma-separated}
|
||||
{--delete-accounts : Also delete linked accounts, transactions and balances}';
|
||||
|
||||
protected $description = 'Disconnect (soft-delete) banking connections and revoke their session on the provider';
|
||||
|
||||
public function handle(DisconnectBankingConnection $disconnectBankingConnection): int
|
||||
{
|
||||
$ids = collect(explode(',', (string) $this->argument('ids')))
|
||||
->map(fn (string $id): string => trim($id))
|
||||
->filter()
|
||||
->unique()
|
||||
->values();
|
||||
|
||||
if ($ids->isEmpty()) {
|
||||
$this->error('No connection IDs provided.');
|
||||
|
||||
return Command::FAILURE;
|
||||
}
|
||||
|
||||
$deleteAccounts = $this->option('delete-accounts');
|
||||
|
||||
$connections = BankingConnection::query()
|
||||
->with('accounts')
|
||||
->whereIn('id', $ids)
|
||||
->get();
|
||||
|
||||
$missing = $ids->diff($connections->pluck('id'));
|
||||
|
||||
foreach ($missing as $id) {
|
||||
$this->warn("Connection not found: {$id}");
|
||||
}
|
||||
|
||||
if ($connections->isEmpty()) {
|
||||
$this->error('No matching banking connections found.');
|
||||
|
||||
return Command::FAILURE;
|
||||
}
|
||||
|
||||
$disconnected = 0;
|
||||
|
||||
foreach ($connections as $connection) {
|
||||
try {
|
||||
$disconnectBankingConnection->handle($connection, $deleteAccounts);
|
||||
$this->info("Disconnected connection {$connection->id} ({$connection->aspsp_name}).");
|
||||
$disconnected++;
|
||||
} catch (\Throwable $e) {
|
||||
$this->error("Failed to disconnect {$connection->id}: {$e->getMessage()}");
|
||||
}
|
||||
}
|
||||
|
||||
$this->info("Disconnected {$disconnected} of {$connections->count()} connection(s).");
|
||||
|
||||
return $missing->isEmpty() && $disconnected === $connections->count()
|
||||
? Command::SUCCESS
|
||||
: Command::FAILURE;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,164 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Enums\BankingConnectionStatus;
|
||||
use App\Models\BankingConnection;
|
||||
use App\Models\Transaction;
|
||||
use App\Models\User;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
|
||||
/**
|
||||
* Support command for the live Enable Banking e2e script
|
||||
* (tests/Browser/live/connect-bank.mjs). It seeds deterministic users, inspects the
|
||||
* resulting connection, and can force a connection into the expired state so the
|
||||
* reconnect flow can be exercised. Output is JSON so the script can parse it.
|
||||
*
|
||||
* Restricted to local/testing environments — it creates passwordful users and fake
|
||||
* subscriptions and must never run against production data.
|
||||
*/
|
||||
class E2eBankingFixtureCommand extends Command
|
||||
{
|
||||
protected $signature = 'e2e:banking-fixture {action : seed|inspect|expire} {email? : target user email for inspect/expire}';
|
||||
|
||||
protected $description = 'Seed/inspect fixtures for the live Enable Banking e2e script (local only).';
|
||||
|
||||
private const ONBOARDING_EMAIL = 'e2e-onboarding@example.test';
|
||||
|
||||
private const SETTINGS_EMAIL = 'e2e-settings@example.test';
|
||||
|
||||
private const PASSWORD = 'password';
|
||||
|
||||
public function handle(): int
|
||||
{
|
||||
if (! app()->environment('local', 'testing')) {
|
||||
$this->error('e2e:banking-fixture is only available in local/testing environments.');
|
||||
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
return match ($this->argument('action')) {
|
||||
'seed' => $this->seed(),
|
||||
'inspect' => $this->inspect(),
|
||||
'expire' => $this->expire(),
|
||||
default => $this->invalidAction(),
|
||||
};
|
||||
}
|
||||
|
||||
private function seed(): int
|
||||
{
|
||||
$onboarding = $this->resetUser(self::ONBOARDING_EMAIL, onboarded: false, pro: false);
|
||||
$settings = $this->resetUser(self::SETTINGS_EMAIL, onboarded: true, pro: true);
|
||||
|
||||
$this->line((string) json_encode([
|
||||
'onboarding' => ['email' => $onboarding->email, 'password' => self::PASSWORD],
|
||||
'settings' => ['email' => $settings->email, 'password' => self::PASSWORD],
|
||||
], JSON_PRETTY_PRINT));
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
private function inspect(): int
|
||||
{
|
||||
$connection = $this->latestConnection();
|
||||
|
||||
if (! $connection) {
|
||||
$this->line((string) json_encode(['connection' => null]));
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
$accountIds = $connection->accounts()->pluck('id');
|
||||
|
||||
$this->line((string) json_encode([
|
||||
'connection' => [
|
||||
'id' => $connection->id,
|
||||
'status' => $connection->status->value,
|
||||
'aspsp_name' => $connection->aspsp_name,
|
||||
'session_id' => $connection->session_id !== null,
|
||||
'valid_until' => $connection->valid_until?->toIso8601String(),
|
||||
'accounts_count' => $accountIds->count(),
|
||||
'accounts_without_external_id' => $connection->accounts()->whereNull('external_account_id')->count(),
|
||||
'transactions_count' => Transaction::whereIn('account_id', $accountIds)->count(),
|
||||
],
|
||||
], JSON_PRETTY_PRINT));
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
private function expire(): int
|
||||
{
|
||||
$connection = $this->latestConnection();
|
||||
|
||||
if (! $connection) {
|
||||
$this->error('No connection found for '.$this->argument('email'));
|
||||
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
$connection->update([
|
||||
'status' => BankingConnectionStatus::Expired,
|
||||
'valid_until' => now()->subDay(),
|
||||
]);
|
||||
|
||||
$this->line((string) json_encode(['expired' => $connection->id]));
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
private function resetUser(string $email, bool $onboarded, bool $pro): User
|
||||
{
|
||||
// Reuse the user across runs (the model soft-deletes, so recreating the same
|
||||
// email would collide). Clear everything the flows produce so each run starts
|
||||
// from a clean slate.
|
||||
$user = User::withTrashed()->firstWhere('email', $email)
|
||||
?? User::factory()->create(['email' => $email]);
|
||||
|
||||
$user->accounts()->forceDelete();
|
||||
$user->bankingConnections()->forceDelete();
|
||||
$user->subscriptions()->delete();
|
||||
|
||||
$user->restore();
|
||||
$user->forceFill([
|
||||
'email_verified_at' => now(),
|
||||
'password' => Hash::make(self::PASSWORD),
|
||||
'onboarded_at' => $onboarded ? now() : null,
|
||||
'two_factor_secret' => null,
|
||||
'two_factor_recovery_codes' => null,
|
||||
'two_factor_confirmed_at' => null,
|
||||
])->save();
|
||||
|
||||
if ($pro) {
|
||||
$user->subscriptions()->create([
|
||||
'type' => 'default',
|
||||
'stripe_id' => 'sub_e2e_'.$user->id,
|
||||
'stripe_status' => 'active',
|
||||
'stripe_price' => 'price_e2e',
|
||||
'quantity' => 1,
|
||||
]);
|
||||
}
|
||||
|
||||
return $user;
|
||||
}
|
||||
|
||||
private function latestConnection(): ?BankingConnection
|
||||
{
|
||||
$email = $this->argument('email');
|
||||
|
||||
$user = User::where('email', $email)->first();
|
||||
|
||||
if (! $user) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $user->bankingConnections()->latest()->first();
|
||||
}
|
||||
|
||||
private function invalidAction(): int
|
||||
{
|
||||
$this->error('Unknown action. Use one of: seed, inspect, expire.');
|
||||
|
||||
return self::FAILURE;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,126 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use Laravel\Cashier\Cashier;
|
||||
use Stripe\Coupon;
|
||||
use Stripe\Exception\ApiErrorException;
|
||||
use Stripe\Exception\InvalidRequestException;
|
||||
|
||||
class EnsureLaunchCouponsCommand extends Command
|
||||
{
|
||||
private const COUPON_FOUNDER_FOREVER = 'wm_founder_forever';
|
||||
|
||||
private const COUPON_EARLYBIRD_MONTHLY = 'wm_earlybird_monthly';
|
||||
|
||||
private const COUPON_EARLYBIRD_YEARLY = 'wm_earlybird_yearly';
|
||||
|
||||
protected $signature = 'stripe:ensure-launch-coupons {--dry-run : Show what would happen without writing to Stripe}';
|
||||
|
||||
protected $description = 'Idempotently create the launch invitation coupons in Stripe';
|
||||
|
||||
public function handle(): int
|
||||
{
|
||||
$stripe = Cashier::stripe();
|
||||
$dryRun = (bool) $this->option('dry-run');
|
||||
|
||||
$monthlyPriceId = $this->resolvePriceId(config('subscriptions.plans.monthly.stripe_lookup_key'));
|
||||
$yearlyPriceId = $this->resolvePriceId(config('subscriptions.plans.yearly.stripe_lookup_key'));
|
||||
|
||||
if ($monthlyPriceId === null || $yearlyPriceId === null) {
|
||||
$this->error('Unable to resolve monthly/yearly Stripe price IDs. Run `php artisan stripe:sync-prices` first.');
|
||||
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
$monthlyProductId = $stripe->prices->retrieve($monthlyPriceId)->product;
|
||||
$yearlyProductId = $stripe->prices->retrieve($yearlyPriceId)->product;
|
||||
|
||||
$coupons = [
|
||||
[
|
||||
'id' => self::COUPON_FOUNDER_FOREVER,
|
||||
'name' => 'WM Founder – 100% off forever',
|
||||
'percent_off' => 100,
|
||||
'duration' => 'forever',
|
||||
'applies_to' => null,
|
||||
],
|
||||
[
|
||||
'id' => self::COUPON_EARLYBIRD_MONTHLY,
|
||||
'name' => 'WM Early Bird – 2 months free',
|
||||
'percent_off' => 100,
|
||||
'duration' => 'repeating',
|
||||
'duration_in_months' => 2,
|
||||
'applies_to' => ['products' => [$monthlyProductId]],
|
||||
],
|
||||
[
|
||||
'id' => self::COUPON_EARLYBIRD_YEARLY,
|
||||
'name' => 'WM Early Bird – 25% off year 1',
|
||||
'percent_off' => 25,
|
||||
'duration' => 'once',
|
||||
'applies_to' => ['products' => [$yearlyProductId]],
|
||||
],
|
||||
];
|
||||
|
||||
foreach ($coupons as $config) {
|
||||
$existing = $this->findCoupon($config['id']);
|
||||
|
||||
if ($existing !== null) {
|
||||
$this->info("✔ Coupon `{$config['id']}` already exists.");
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($dryRun) {
|
||||
$this->line("[dry-run] Would create coupon `{$config['id']}`.");
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$payload = array_filter([
|
||||
'id' => $config['id'],
|
||||
'name' => $config['name'],
|
||||
'percent_off' => $config['percent_off'],
|
||||
'duration' => $config['duration'],
|
||||
'duration_in_months' => $config['duration_in_months'] ?? null,
|
||||
'applies_to' => $config['applies_to'] ?? null,
|
||||
], fn ($value): bool => $value !== null);
|
||||
|
||||
try {
|
||||
$stripe->coupons->create($payload);
|
||||
} catch (ApiErrorException $exception) {
|
||||
$this->error("Failed creating `{$config['id']}`: {$exception->getMessage()}");
|
||||
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
$this->info("+ Created coupon `{$config['id']}`.");
|
||||
}
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
private function findCoupon(string $id): ?Coupon
|
||||
{
|
||||
try {
|
||||
return Cashier::stripe()->coupons->retrieve($id);
|
||||
} catch (InvalidRequestException) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private function resolvePriceId(?string $lookupKey): ?string
|
||||
{
|
||||
if ($lookupKey === null || $lookupKey === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
$prices = Cashier::stripe()->prices->all([
|
||||
'lookup_keys' => [$lookupKey],
|
||||
'limit' => 1,
|
||||
'expand' => ['data.product'],
|
||||
]);
|
||||
|
||||
return $prices->data[0]->id ?? null;
|
||||
}
|
||||
}
|
||||
|
|
@ -11,7 +11,7 @@ class FeatureDisableCommand extends Command
|
|||
{
|
||||
use ResolvesFeatures;
|
||||
|
||||
protected $signature = 'feature:disable {feature : The feature name (class name or string-based feature)} {target : User email or "all" for everyone}';
|
||||
protected $signature = 'feature:disable {feature : The feature class name} {target : User email or "all" for everyone}';
|
||||
|
||||
protected $description = 'Disable a feature for a specific user or all users';
|
||||
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ class FeatureEnableCommand extends Command
|
|||
{
|
||||
use ResolvesFeatures;
|
||||
|
||||
protected $signature = 'feature:enable {feature : The feature name (class name or string-based feature)} {target : User email, "all" for everyone, or a percentage like "25%" for a random rollout}';
|
||||
protected $signature = 'feature:enable {feature : The feature class name} {target : User email or "all" for everyone}';
|
||||
|
||||
protected $description = 'Enable a feature for a specific user or all users';
|
||||
|
||||
|
|
@ -36,10 +36,6 @@ class FeatureEnableCommand extends Command
|
|||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
if (preg_match('/^(\d{1,3})%$/', $target, $matches)) {
|
||||
return $this->enableForPercentage($featureClass, $featureName, (int) $matches[1]);
|
||||
}
|
||||
|
||||
$user = User::where('email', $target)->first();
|
||||
|
||||
if (! $user) {
|
||||
|
|
@ -53,23 +49,4 @@ class FeatureEnableCommand extends Command
|
|||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
private function enableForPercentage(string $featureClass, string $featureName, int $percentage): int
|
||||
{
|
||||
if ($percentage < 1 || $percentage > 100) {
|
||||
$this->error('Percentage must be between 1 and 100.');
|
||||
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
$count = (int) ceil(User::count() * $percentage / 100);
|
||||
|
||||
User::inRandomOrder()
|
||||
->take($count)
|
||||
->each(fn (User $user) => Feature::for($user)->activate($featureClass));
|
||||
|
||||
$this->info("Feature '{$featureName}' enabled for {$count} users (~{$percentage}% of current users).");
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,64 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Models\Budget;
|
||||
use App\Models\BudgetPeriod;
|
||||
use App\Services\BudgetPeriodService;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
class GenerateBudgetPeriods extends Command
|
||||
{
|
||||
protected $signature = 'budgets:generate-periods';
|
||||
|
||||
protected $description = 'Generate upcoming budget periods and close completed ones';
|
||||
|
||||
public function __construct(protected BudgetPeriodService $budgetPeriodService)
|
||||
{
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
public function handle(): int
|
||||
{
|
||||
$this->info('Generating budget periods...');
|
||||
|
||||
$budgets = Budget::with('periods')->get();
|
||||
$generatedCount = 0;
|
||||
$closedCount = 0;
|
||||
|
||||
foreach ($budgets as $budget) {
|
||||
$currentPeriod = $budget->getCurrentPeriod();
|
||||
|
||||
if (! $currentPeriod) {
|
||||
$this->budgetPeriodService->generatePeriod($budget);
|
||||
$generatedCount++;
|
||||
$this->info("Generated initial period for budget: {$budget->name}");
|
||||
}
|
||||
|
||||
$completedPeriods = BudgetPeriod::where('budget_id', $budget->id)
|
||||
->where('end_date', '<', today())
|
||||
->get();
|
||||
|
||||
foreach ($completedPeriods as $period) {
|
||||
if ($period->end_date < today()) {
|
||||
$this->budgetPeriodService->closePeriod($period);
|
||||
$closedCount++;
|
||||
}
|
||||
}
|
||||
|
||||
$futurePeriods = $budget->periods()
|
||||
->where('start_date', '>', today())
|
||||
->count();
|
||||
|
||||
if ($futurePeriods < 2) {
|
||||
$this->budgetPeriodService->generatePeriod($budget);
|
||||
$generatedCount++;
|
||||
}
|
||||
}
|
||||
|
||||
$this->info("Generated {$generatedCount} new periods");
|
||||
$this->info("Closed {$closedCount} completed periods");
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,70 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Enums\AccountType;
|
||||
use App\Models\Account;
|
||||
use App\Models\AccountBalance;
|
||||
use App\Services\LoanAmortizationService;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
class GenerateMonthlyLoanBalances extends Command
|
||||
{
|
||||
protected $signature = 'loans:generate-balances';
|
||||
|
||||
protected $description = 'Generate monthly balance entries for loan accounts with amortization details';
|
||||
|
||||
public function __construct(protected LoanAmortizationService $amortizationService)
|
||||
{
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
public function handle(): int
|
||||
{
|
||||
$this->info('Generating monthly loan balances...');
|
||||
|
||||
$accounts = Account::query()
|
||||
->where('type', AccountType::Loan)
|
||||
->whereHas('loanDetail')
|
||||
->with('loanDetail')
|
||||
->get();
|
||||
|
||||
$generatedCount = 0;
|
||||
$skippedCount = 0;
|
||||
|
||||
$balanceDate = now()->startOfMonth()->toDateString();
|
||||
|
||||
foreach ($accounts as $account) {
|
||||
$loanDetail = $account->loanDetail;
|
||||
|
||||
$existingBalance = AccountBalance::query()
|
||||
->where('account_id', $account->id)
|
||||
->where('balance_date', $balanceDate)
|
||||
->exists();
|
||||
|
||||
if ($existingBalance) {
|
||||
$skippedCount++;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$projectedBalance = $this->amortizationService->getBalanceAtDate(
|
||||
$loanDetail,
|
||||
now(),
|
||||
);
|
||||
|
||||
AccountBalance::create([
|
||||
'account_id' => $account->id,
|
||||
'balance_date' => $balanceDate,
|
||||
'balance' => $projectedBalance,
|
||||
]);
|
||||
|
||||
$generatedCount++;
|
||||
$this->info("Generated balance for: {$account->name} ({$projectedBalance} cents)");
|
||||
}
|
||||
|
||||
$this->info("Generated {$generatedCount} balance entries, skipped {$skippedCount} (already exist)");
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,103 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Str;
|
||||
use Laravel\Cashier\Cashier;
|
||||
use RuntimeException;
|
||||
use Stripe\Exception\ApiErrorException;
|
||||
use Stripe\PromotionCode;
|
||||
|
||||
class GenerateStripePromotionCodesCommand extends Command
|
||||
{
|
||||
private const DEFAULT_COUPON_ID = '0E5fAsXG';
|
||||
|
||||
protected $signature = 'stripe:generate-promotion-codes
|
||||
{count : Number of promotion codes to generate}
|
||||
{--coupon='.self::DEFAULT_COUPON_ID.' : Stripe coupon ID to attach to each code}';
|
||||
|
||||
protected $description = 'Generate single-use Stripe promotion codes';
|
||||
|
||||
public function handle(): int
|
||||
{
|
||||
$count = filter_var($this->argument('count'), FILTER_VALIDATE_INT);
|
||||
|
||||
if ($count === false || $count < 1) {
|
||||
$this->error('Count must be a positive integer.');
|
||||
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
$couponId = (string) $this->option('coupon');
|
||||
$generatedCodes = [];
|
||||
$rows = [];
|
||||
|
||||
$this->info("Generating {$count} single-use promotion codes for coupon {$couponId}...");
|
||||
|
||||
for ($index = 1; $index <= $count; $index++) {
|
||||
try {
|
||||
$promotionCode = $this->createPromotionCode($couponId, $generatedCodes);
|
||||
} catch (ApiErrorException $exception) {
|
||||
$this->error("Failed generating promotion code {$index}: {$exception->getMessage()}");
|
||||
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
$generatedCodes[] = $promotionCode->code;
|
||||
$rows[] = [$index, $promotionCode->id, $promotionCode->code];
|
||||
}
|
||||
|
||||
$this->newLine();
|
||||
$this->table(['#', 'Stripe ID', 'Code'], $rows);
|
||||
$this->newLine();
|
||||
$this->info('Generated '.$count.' promotion code'.($count === 1 ? '' : 's').'.');
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<string> $generatedCodes
|
||||
*/
|
||||
private function createPromotionCode(string $couponId, array $generatedCodes): PromotionCode
|
||||
{
|
||||
for ($attempt = 1; $attempt <= 5; $attempt++) {
|
||||
$code = $this->makeCode($generatedCodes);
|
||||
|
||||
try {
|
||||
return Cashier::stripe()->promotionCodes->create([
|
||||
'coupon' => $couponId,
|
||||
'code' => $code,
|
||||
'max_redemptions' => 1,
|
||||
]);
|
||||
} catch (ApiErrorException $exception) {
|
||||
if ($attempt < 5 && $this->isDuplicateCodeError($exception)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
throw $exception;
|
||||
}
|
||||
}
|
||||
|
||||
throw new RuntimeException('Unable to generate a unique promotion code after 5 attempts.');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<string> $generatedCodes
|
||||
*/
|
||||
private function makeCode(array $generatedCodes): string
|
||||
{
|
||||
do {
|
||||
$code = 'WM-'.Str::upper(Str::random(10));
|
||||
} while (in_array($code, $generatedCodes, true));
|
||||
|
||||
return $code;
|
||||
}
|
||||
|
||||
private function isDuplicateCodeError(ApiErrorException $exception): bool
|
||||
{
|
||||
$message = Str::lower($exception->getMessage());
|
||||
|
||||
return str_contains($message, 'code') && str_contains($message, 'exist');
|
||||
}
|
||||
}
|
||||
|
|
@ -1,99 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Console\Commands\Concerns\FindsUsersWithLegacyEncryption;
|
||||
use App\Jobs\SendUpdateEmailJob;
|
||||
use App\Models\User;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
class NotifyEncryptedDataRemovalCommand extends Command
|
||||
{
|
||||
use FindsUsersWithLegacyEncryption;
|
||||
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'encryption:notify-removal
|
||||
{--dry-run : List affected users without sending anything}
|
||||
{--force : Skip the confirmation prompt}';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Warn users with legacy encrypted data that their account will be deleted unless they sign in within 7 days';
|
||||
|
||||
private const VIEW = 'encrypted-data-removal';
|
||||
|
||||
private const IDENTIFIER = 'encrypted-data-removal';
|
||||
|
||||
private const SUBJECT = 'Action required: sign in to keep your Whisper Money account';
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*/
|
||||
public function handle(): int
|
||||
{
|
||||
$encryptedUsers = $this->usersWithLegacyEncryption()->get();
|
||||
|
||||
// Always report the total scope of legacy encrypted data (soft-deleted users are
|
||||
// excluded by the model's default scope). This is the signal for deciding whether
|
||||
// the browser-side encryption code can finally be removed, independent of who we
|
||||
// actually email below.
|
||||
$this->info("{$encryptedUsers->count()} non-deleted user(s) still have encrypted data.");
|
||||
|
||||
$users = $this->excludeBilledUsers($encryptedUsers);
|
||||
|
||||
if ($users->isEmpty()) {
|
||||
$this->info('No users to warn.');
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
if ($this->option('dry-run')) {
|
||||
$this->renderTable($users);
|
||||
$this->info('[dry-run] No emails sent.');
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
if (! $this->option('force') && ! $this->confirm("Send the deletion warning to {$users->count()} user(s)?", true)) {
|
||||
$this->info('Cancelled.');
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
$progressBar = $this->output->createProgressBar($users->count());
|
||||
$progressBar->start();
|
||||
|
||||
foreach ($users->values() as $index => $user) {
|
||||
// Spread over the 'emails' queue at 50/day to match the existing bulk-email convention.
|
||||
SendUpdateEmailJob::dispatch($user, self::VIEW, self::IDENTIFIER, self::SUBJECT)
|
||||
->delay(now()->addDays((int) floor($index / 50)));
|
||||
|
||||
$progressBar->advance();
|
||||
}
|
||||
|
||||
$progressBar->finish();
|
||||
$this->newLine();
|
||||
$this->info("Queued {$users->count()} warning email(s) to the 'emails' queue (50/day).");
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Collection<int, User> $users
|
||||
*/
|
||||
private function renderTable($users): void
|
||||
{
|
||||
$this->table(['Email', 'Last active'], $users->map(fn (User $user) => [
|
||||
$user->email,
|
||||
$user->last_active_at?->toDateTimeString() ?? 'never',
|
||||
])->all());
|
||||
}
|
||||
}
|
||||
|
|
@ -1,62 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Models\User;
|
||||
use App\Services\ResendService;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
class ResendSyncCommand extends Command
|
||||
{
|
||||
protected $signature = 'resend:sync';
|
||||
|
||||
protected $description = 'Sync all users to Resend contacts';
|
||||
|
||||
public function handle(ResendService $resendService): int
|
||||
{
|
||||
if (! config('services.resend.key')) {
|
||||
$this->error('Resend API key not configured.');
|
||||
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
$users = User::all();
|
||||
|
||||
if ($users->isEmpty()) {
|
||||
$this->info('No users to sync.');
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
$this->info("Syncing {$users->count()} users to Resend...");
|
||||
|
||||
$bar = $this->output->createProgressBar($users->count());
|
||||
$bar->start();
|
||||
|
||||
$failed = 0;
|
||||
|
||||
foreach ($users as $user) {
|
||||
try {
|
||||
$resendService->createContact($user);
|
||||
} catch (\Exception $e) {
|
||||
$failed++;
|
||||
$this->newLine();
|
||||
$this->warn("Failed to sync {$user->email}: {$e->getMessage()}");
|
||||
}
|
||||
|
||||
$bar->advance();
|
||||
}
|
||||
|
||||
$bar->finish();
|
||||
$this->newLine(2);
|
||||
|
||||
$synced = $users->count() - $failed;
|
||||
$this->info("Synced {$synced} users to Resend.");
|
||||
|
||||
if ($failed > 0) {
|
||||
$this->warn("Failed to sync {$failed} users.");
|
||||
}
|
||||
|
||||
return $failed > 0 ? self::FAILURE : self::SUCCESS;
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue