# Bongo Question Package - Cursor AI Instructions

## Overview

This is a Laravel package providing a **categorised FAQ system**. It manages FAQ questions and question categories with a many-to-many relationship, per-category ordering (`sort_order` pivot), backend CRUD, an attach/detach/reorder API, a frontend listing of active questions, and a `FaqSchema` helper that renders schema.org `FAQPage` JSON-LD.

**Key Features:**
- CRUD for FAQ questions (`Question`) and categories (`QuestionCategory`)
- Many-to-many `questions <-> categories` via `question_categories_pivot`, ordered per category by `sort_order`
- API to attach / detach / reorder questions within a category (`auth:sanctum`)
- Frontend and API listings of active questions ordered by name
- `FaqSchema::render()` — schema.org `FAQPage` JSON-LD from a question collection
- Status workflow (pending / active / inactive) via a package-local `HasStatus` trait
- SEO metadata, UUIDs, soft deletes via shared framework traits

**What this package does NOT include** (do not generate code that assumes these):
- No frontend submission form, ratings, mailables, spam protection, or artisan commands
- No cache listeners or event listeners of any kind (events are dispatched but unhandled here)
- No `duplicate` route or controller method

## Package Information

- **Package:** `bongo/question`
- **Namespace:** `Bongo\Question`
- **Requirements:** PHP >= 8.2, `illuminate/contracts` ^10.0, `bongo/framework` ^3.0
- **Service Provider:** `Bongo\Question\QuestionServiceProvider`
- Note: `composer.json` does **not** declare `extra.laravel.providers` — the consuming application registers the provider.
- **Undeclared runtime dependencies** (supplied by the framework or consuming app): `Bongo\Package` (used by `PackageSeeder`) and `spatie/schema-org` (used by `FaqSchema`).

## Project Structure

```
src/
├── Config/
│   └── question.php                        # Route prefixes + schema.allowed_answer_tags
├── Events/                                 # 6 plain data-carrier events (no listeners)
│   ├── QuestionCreated.php / QuestionUpdated.php / QuestionDeleted.php
│   └── QuestionCategoryCreated.php / ...Updated.php / ...Deleted.php
├── Factories/
│   └── QuestionFactory.php                 # Bongo\Question\Factories namespace
├── Http/
│   ├── Controllers/
│   │   ├── Api/
│   │   │   ├── QuestionController.php          # index (active questions)
│   │   │   └── QuestionCategoryController.php  # index, questions, attach, detach, reorder
│   │   ├── Backend/
│   │   │   ├── QuestionController.php              # CRUD + syncQuestionCategories()
│   │   │   ├── QuestionCategoryController.php      # CRUD
│   │   │   ├── QuestionDatatableController.php     # datatable feed
│   │   │   ├── QuestionCategoryDatatableController.php
│   │   │   └── QuestionCategoryQuestionController.php  # per-category question management view
│   │   └── Frontend/
│   │       └── QuestionController.php          # index (active questions)
│   ├── Requests/
│   │   ├── StoreQuestionRequest.php / UpdateQuestionRequest.php
│   │   ├── StoreQuestionCategoryRequest.php / UpdateQuestionCategoryRequest.php
│   │   └── Api/
│   │       ├── AttachQuestionRequest.php
│   │       └── ReorderQuestionsRequest.php
│   ├── Resources/
│   │   ├── QuestionResource.php
│   │   └── QuestionCategoryResource.php
│   └── ViewComposers/
│       ├── QuestionComposer.php            # $questions for the question dropdown partial
│       └── QuestionCategoryComposer.php    # $questionCategories for the category dropdown partial
├── Migrations/
│   ├── 2026_01_01_000001_create_questions_table.php
│   ├── 2026_01_01_000002_create_question_categories_table.php
│   └── 2026_01_01_000003_create_question_categories_pivot_table.php
├── Models/
│   ├── Question.php                        # categories() BelongsToMany
│   └── QuestionCategory.php                # questions() BelongsToMany withPivot('sort_order')
├── Routes/
│   ├── api.php                             # auth:sanctum, name prefix api.
│   ├── backend.php                         # auth + employee, name prefix backend.
│   └── frontend.php                        # name prefix frontend.
├── Schema/
│   └── FaqSchema.php                       # FAQPage JSON-LD renderer (spatie/schema-org)
├── Seeders/
│   └── PackageSeeder.php                   # Registers module in admin navigation
├── Traits/
│   └── HasStatus.php                       # Package-local status scopes/checks
├── Translations/en/backend.php             # question::backend.* labels + flash messages
├── Views/
│   ├── backend/                            # index/create/edit/show + category/* + partials
│   └── frontend/                           # index + partials/question card
└── QuestionServiceProvider.php

database/factories/QuestionCategoryFactory.php   # Bongo\Question\Database\Factories namespace
tests/                                           # PHPUnit (Orchestra Testbench)
```

## Architecture Patterns

### Service Provider

`QuestionServiceProvider` extends `Bongo\Framework\Providers\AbstractServiceProvider`, which auto-registers config, routes, views, migrations, and translations from `src/` based on `$module = 'question'`:

```php
class QuestionServiceProvider extends AbstractServiceProvider
{
    protected string $module = 'question';

    protected array $composers = [
        QuestionComposer::class => [
            'question::backend.partials.dropdowns.question',
        ],
        QuestionCategoryComposer::class => [
            'question::backend.category.partials.dropdowns.category',
        ],
    ];

    public function boot(): void
    {
        parent::boot();
        AliasLoader::getInstance()->alias('QuestionCategory', QuestionCategory::class);
        AliasLoader::getInstance()->alias('FaqSchema', FaqSchema::class);
    }
}
```

Route file middleware (applied automatically by `AbstractServiceProvider` — never add it in the route files):
- `src/Routes/api.php` → `api.*` names, `auth:sanctum`
- `src/Routes/backend.php` → `backend.*` names, `auth` + `employee`, admin prefix
- `src/Routes/frontend.php` → `frontend.*` names

There are no `$commands`, `$listeners`, or `$subscribers`.

### Models

Both models extend `Bongo\Framework\Models\AbstractModel` with the same trait stack:

```php
use HasContent;   // framework
use HasFactory;
use HasKey;       // framework
use HasSeo;       // framework
use HasStatus;    // PACKAGE-LOCAL: Bongo\Question\Traits\HasStatus
use HasUUID;      // framework
use SoftDeletes;

protected $casts = ['status' => StatusEnum::class];

protected $fillable = [
    'name', 'slug', 'content', 'status',
    'meta_title', 'meta_description', 'meta_canonical', 'meta_index',
];
```

The ordered side of the relationship lives on the category:

```php
// QuestionCategory
public function questions(): BelongsToMany
{
    return $this->belongsToMany(Question::class, 'question_categories_pivot')
        ->withPivot('sort_order')
        ->orderByPivot('sort_order');
}

// Question
public function categories(): BelongsToMany
{
    return $this->belongsToMany(QuestionCategory::class, 'question_categories_pivot');
}
```

**Important:** the `HasStatus` trait here is `Bongo\Question\Traits\HasStatus` (package-local), NOT the framework trait. It provides `scopePending/scopeActive/scopeInactive` and `hasStatus/isPending/isActive/isInactive` built on `Bongo\Framework\Enums\StatusEnum`.

### Sort-order convention

New attachments always append: `sort_order` = (max existing `sort_order` in that category) + 1. Both `Backend\QuestionController::syncQuestionCategories()` and `Api\QuestionCategoryController::attach()` follow this. Reordering rewrites pivot rows with `updateExistingPivot()`.

### FaqSchema

```php
FaqSchema::render($questions); // '' for an empty collection, else a <script type="application/ld+json"> block
```

Builds `Schema::fAQPage()` with each question as `mainEntity`; the answer text comes from `$question->getContentAsPlainText($allowedTags)` with tags from `config('question.schema.allowed_answer_tags')`. Used in `frontend/index.blade.php` inside the `meta_schema` section.

## Coding Conventions

- `declare(strict_types=1)` in every PHP file
- Backend controllers extend `AbstractController`; API controllers extend `AbstractApiController`; datatable controllers extend `AbstractDatatableController` and implement `getBaseQuery(): Builder`
- Backend CRUD fires the matching event after each write: `QuestionCreated`/`Updated`/`Deleted`, `QuestionCategory*`
- Flash messages via `->success(trans('question::backend.store_success'))` etc. on redirects
- Form requests resolve table names dynamically: `(new Question())->getTable()`; unique rules exclude soft-deleted rows (`deleted_at,NULL`)
- API form requests (`Api/`) use array-notation rules; the backend requests currently use pipe strings — prefer array notation for new rules
- Route files use `Route::as(...)->prefix(config(...))->group(...)`; explicit route declarations, never `Route::resource()`
- View namespace `question::`, translation namespace `question::backend`
- Migrations guard with `Schema::hasTable()`, use `increments('id')` + `uuid()->index()`, and unsigned-integer foreign keys (never `foreignId()`)
- Name limits differ: questions `max:250`, categories `max:75` — validated against `slug` uniqueness

## Routes Reference

Backend (`backend.` names, `auth` + `employee`):
- `question.` group under `config('question.backend_prefix', 'questions')`: index, create, store, datatable, `{question}` show/edit/update, `ANY {question}/delete` → destroy
- `question_category.` group under `config('question.category.backend_prefix', 'question-categories')`: index, create, store, datatable, `{questionCategory}` show/edit/update, `DELETE {questionCategory}/delete` → destroy, plus `GET {questionCategory}/questions` → `backend.question_category.question.index`

API (`api.` names, `auth:sanctum`):
- `GET /questions` → `api.question.index`
- `GET /question-categories` → `api.question_category.index`
- `GET /question-categories/{questionCategory}/questions` → list attached with pivot sort_order
- `POST .../questions/attach` (`question_id`), `POST .../questions/{question}/detach`, `POST .../questions/reorder` (`questions[][question_id|sort_order]`)

Frontend (`frontend.` names):
- `GET /questions` → `frontend.question.index` (active questions ordered by name)

## Common Tasks

### Add a new field to questions

1. New migration in `src/Migrations/` (guard with `Schema::hasTable`/`hasColumn`)
2. Add to `$fillable` in `src/Models/Question.php` (+ `$casts` if non-string)
3. Update `StoreQuestionRequest` / `UpdateQuestionRequest`
4. Update `src/Views/backend/partials/form/details.blade.php` and `show.blade.php`
5. Add to `QuestionResource` if it should appear in the API

### Attach a question to a category programmatically

```php
$maxSortOrder = $category->questions()->max('question_categories_pivot.sort_order') ?? 0;

$category->questions()->syncWithoutDetaching([
    $questionId => ['sort_order' => $maxSortOrder + 1],
]);
```

### Render FAQ schema on a page

```blade
{!! FaqSchema::render($category->questions) !!}
```

## Testing and Commands

```bash
vendor/bin/phpunit          # PHPUnit test suite (Orchestra Testbench, sqlite :memory:)
vendor/bin/pint --test      # Check code style
vendor/bin/pint             # Fix code style
vendor/bin/phpstan analyse  # Static analysis (larastan)
composer install            # Install dependencies
```

Tests live in `tests/` — `Unit/Models/QuestionTest`, `Unit/Models/QuestionCategoryTest`, `Unit/Schema/FaqSchemaTest`, `Unit/ServiceProviderTest`, `Feature/Api/QuestionCategoryApiTest`. Use `#[Test]` attributes and snake_case method names. `tests/TestCase.php` registers the provider, aliases the `employee`/`noIndex` middleware, and creates the three tables inline. `tests/Helpers.php` stubs the `setting()`, `cookie_enabled()`, and `captcha()` helpers.

Factories: `Bongo\Question\Factories\QuestionFactory` (in `src/Factories/`, states `inactive()`, `pending()`) and `Bongo\Question\Database\Factories\QuestionCategoryFactory` (in `database/factories/`, state `inactive()`).

## How This Package Extends bongo/framework

- `AbstractServiceProvider` — auto-registration of config/routes/views/migrations/translations
- `AbstractModel` — base model with audit columns (`created_by`, `updated_by`, `deleted_by`)
- `AbstractController` / `AbstractApiController` / `AbstractDatatableController` — controller bases
- Traits: `HasContent` (provides `getContentAsPlainText()`), `HasKey`, `HasSeo`, `HasUUID`
- Enums: `StatusEnum` (status cast + defaults), `IndexEnum` (`meta_index` default)
- Frontend view extends `framework::frontend.layouts.app`
