# Technical Requirements: Go No Go

## 1. Overview

Baker Tilly International "Go/No Go" checklist application. Users complete scored questionnaires across categories to decide whether to pursue business opportunities. Each questionnaire session covers one category, with questions organized into groups. Scored questions produce a cumulative score that determines a Go / No Go / Consult Leadership result.

**Category note:** The original plan specifies 6 categories (Audit, Outsource, Solution, Digital Solutions, Legal, Tax). The question source documents contain 5 files. "Solution" may be separate from "Outsource Solutions" or merged -- seed all 6 per the plan and reconcile during question seeding.

## 2. Tech Stack

- **Backend:** Laravel 12
- **Frontend:** Vue 3 with Inertia.js v2
- **Admin Panel:** Laravel Nova 5 at `/cp`
- **Database:** MySQL (`go-no-go`)
- **Auth (frontend):** Laravel Socialite with Azure AD SSO
- **Auth (Nova):** Built-in Nova authentication
- **Excel Export:** `maatwebsite/laravel-nova-excel`

## 3. Two Separate Interfaces

The application has two distinct interfaces:

- **Frontend (questionnaire):** Custom Vue 3 + Inertia.js v2 application at `/`. All questionnaire flow, session management, and user-facing pages. Custom routes, controllers, and Vue page components.
- **Admin panel (Nova):** Laravel Nova 5 at `/cp`. Administrators log in here to manage questions and view sessions/answers. No questionnaire logic — strictly data management.

These are fully separate. The frontend has its own routes, controllers, and Vue pages. Nova handles administration only.

### Authentication

Each interface has its own authentication method:

- **Frontend:** Azure AD Single Sign-On via Laravel Socialite. Users visit `/login`, get redirected to Azure AD, and are returned to `/auth/callback`. Socialite handles the OAuth flow. Users are created or matched by email on first login. No password stored for frontend users.
- **Nova:** Built-in Nova authentication at `/cp/login`. Already configured and working. Only administrators access Nova.

## 4. Frontend Architecture (Vue + Inertia.js v2)

The entire user-facing frontend is built with Vue 3 and Inertia.js v2. No Blade templates for user pages.

### Inertia Features and Their Application

| Feature | Usage | Why |
|---------|-------|-----|
| **useForm helper** | All questionnaire form submissions | Provides form state, error handling, processing indicators, dirty tracking. Each questionnaire step uses useForm to submit answers. |
| **Form component** | Login, basic info entry | Declarative form handling for simpler forms that don't need granular control. |
| **Link component** | Navigation between steps, dashboard, session history | SPA navigation. Use `only`/`except` props for partial reloads. |
| **Shared data** | Via `HandleInertiaRequests` middleware | Authenticated user, current session progress, flash messages, scoring thresholds -- available on every page without explicit passing. |
| **Persistent layouts** | Main app layout | Layout persists across questionnaire steps, keeping sidebar/progress indicator state without remounting. |
| **Deferred props** | `Inertia::defer()` for session history, dashboard statistics | Load questionnaire data immediately; defer non-critical analytics data. |
| **WhenVisible** | Long questionnaire pages displayed as single scroll | Load question group data only when user scrolls to that section. |
| **Progress indicator** | Built-in NProgress bar | Visual feedback during page transitions between questionnaire steps. |
| **Head component** | Dynamic page titles | Per-step titles like "Step 3: Risk Assessment - Audit". |
| **Partial reloads** | Saving individual answers | When saving a single answer, only reload answers/score data, not the entire question set. |
| **History encryption** | `EncryptHistory` middleware | Questionnaire data may contain sensitive client information -- encrypt browser history state. |
| **Prefetching** | Next questionnaire step | Prefetch on hover/focus for instant navigation to the next step. |
| **Polling** | Optional/future | Not needed for core flow. Could poll for session lock status if multi-user editing is added later. |
| **Scroll management** | Save inline vs. navigate | Preserve scroll position when saving answers inline; scroll to top when navigating to a new question group. |

## 5. Database Tables

### categories

The 6 fixed assessment categories. Seeded, not user-managed.

| Column | Type | Constraints |
|--------|------|-------------|
| id | bigint unsigned | PK, auto-increment |
| name | varchar(255) | required, unique |
| sort_order | unsigned integer | required, default 0 |
| created_at | timestamp | nullable |
| updated_at | timestamp | nullable |

**Seed data (in order):**

1. Audit
2. Outsource
3. Solution
4. Digital Solutions
5. Legal
6. Tax

### question_groups

Groups of related questions, scoped to a single category. Each group becomes one step in the questionnaire flow.

| Column | Type | Constraints |
|--------|------|-------------|
| id | bigint unsigned | PK, auto-increment |
| category_id | bigint unsigned | FK to categories.id, required |
| name | varchar(255) | required |
| sort_order | unsigned integer | required, default 0 |
| description | text | nullable |
| scoring_instructions | text | nullable, for display text like "If you answer yes, you will score 1 point..." |
| created_at | timestamp | nullable |
| updated_at | timestamp | nullable |

**Groups per category (from question docs):**

- **Audit** (7 groups): Opportunity Details, Client Background and History, Financial Information, Regulatory Compliance, Risk Assessment, Resource Allocation, Reporting Requirements
- **Outsource Solutions** (6 groups): Opportunity Details, Client Background and History, Regulatory Compliance, Risk Assessment, Resource Allocation
- **Digital Solutions** (7 groups): Opportunity Details, Client Background and History, Regulatory Compliance, Risk Assessment, Resource Allocation, Technology & Innovation Fit
- **Legal** (8 groups): Opportunity Details, Client Background and History, Financial Information, Regulatory Compliance, Risk Assessment, Resource Allocation, Stakeholder Engagement, Fee Quote
- **Tax** (7 groups): Opportunity Details, Client Background and History, Financial Information, Regulatory Compliance, Risk Assessment, Resource Allocation, Stakeholder Engagement

All categories also have an "Additional Comments" section handled as the `additional_comments` field on the session, not as a separate question group.

### questions

Individual questions within a group. Each question is self-contained with boolean flags controlling which answer options are available and how details are handled.

| Column | Type | Constraints |
|--------|------|-------------|
| id | bigint unsigned | PK, auto-increment |
| question_group_id | bigint unsigned | FK to question_groups.id, required |
| text | text | required |
| has_yes | boolean | required, default false |
| has_no | boolean | required, default false |
| has_na | boolean | required, default false |
| details | varchar(50) | nullable, enum: `req_on_no`, `req_on_yes`, `optional`, `required` |
| sort_order | unsigned integer | required, default 0 |
| is_scored | boolean | required, default false |
| created_at | timestamp | nullable |
| updated_at | timestamp | nullable |

#### Question Configuration

Each question is configured through boolean flags and a details mode. No cross-question dependencies exist — every question is self-contained.

**Six configuration patterns cover all question types:**

| Pattern | has_yes | has_no | has_na | details | Example |
|---------|---------|--------|--------|---------|---------|
| Pure open text | false | false | false | `required` | "What sort of audit opportunity is it?" |
| Scored, no details | true | true | true | null | "Does the sector come with a reputation we are comfortable with?" |
| Scored + details on no | true | true | true | `req_on_no` | "There have been no significant changes in the client's business?" |
| Scored + details on yes | true | true | true | `req_on_yes` | "Are there any previous audit reports or findings that need to be considered?" |
| Scored + details always | true | true | true | `required` | "Have you completed a conflict check?" |
| Scored + details optional | true | true | true | `optional` | "Has the client provided financial statements?" |

- When `has_yes`, `has_no`, and `has_na` are all false → the question renders as open text only (textarea)
- When any are true → radio buttons render for the enabled options
- The `details` column controls when a details textarea appears and whether it is required

### sessions

One entry per user per questionnaire run. A session covers one category (not all categories).

| Column | Type | Constraints |
|--------|------|-------------|
| id | bigint unsigned | PK, auto-increment |
| user_id | bigint unsigned | FK to users.id, required |
| category_id | bigint unsigned | FK to categories.id, required |
| screening_id | bigint unsigned | FK to screenings.id, nullable |
| status | varchar(50) | required, enum: `in_progress`, `completed`, `abandoned` |
| score | integer | nullable, calculated from scored answers |
| result | varchar(50) | nullable, enum: `go`, `no_go`, `consult_leadership` |
| basic_info | json | stores: client_name, client_contact, lead_firm_name, lead_firm_contact |
| additional_comments | text | nullable, free text from final section |
| completed_at | timestamp | nullable |
| created_at | timestamp | nullable |
| updated_at | timestamp | nullable |

#### Basic Info JSON Structure

Every category starts with these fields, stored in `basic_info`:

```json
{
  "client_name": "Acme Corp",
  "client_contact": "John Doe",
  "lead_firm_name": "Baker Tilly NL",
  "lead_firm_contact": "Jane Smith"
}
```

### answers

Stores all user responses. One answer per question per session.

| Column | Type | Constraints |
|--------|------|-------------|
| id | bigint unsigned | PK, auto-increment |
| session_id | bigint unsigned | FK to sessions.id, required |
| question_id | bigint unsigned | FK to questions.id, required |
| value | varchar(255) | nullable, stores `"yes"` / `"no"` / `"not_applicable"` for scored questions |
| text_value | text | nullable, stores free text for open text questions and details fields |
| created_at | timestamp | nullable |
| updated_at | timestamp | nullable |

**Unique constraint:** `(session_id, question_id)` -- one answer per question per session.

### screenings

Pre-screening sessions. One per attempt. Tracks the 10 Yes/No pre-screening questions before category selection.

| Column | Type | Constraints |
|--------|------|-------------|
| id | bigint unsigned | PK, auto-increment |
| user_id | bigint unsigned | FK to users.id, required |
| score | integer | nullable, calculated from screening answers |
| passed | boolean | nullable, true if score >= 5 |
| created_at | timestamp | nullable |
| updated_at | timestamp | nullable |

### screening_answers

Stores answers to the 10 pre-screening questions.

| Column | Type | Constraints |
|--------|------|-------------|
| id | bigint unsigned | PK, auto-increment |
| screening_id | bigint unsigned | FK to screenings.id, required |
| question_number | unsigned integer | required, 1-10 |
| value | varchar(10) | required, `yes` or `no` |
| created_at | timestamp | nullable |
| updated_at | timestamp | nullable |

**Unique constraint:** `(screening_id, question_number)` -- one answer per question per screening.

### logs

Comprehensive activity log for usage analytics. Every meaningful user action is recorded to enable statistics on: who is using the application, which categories are used most, usage frequency, whether certain questions are always answered the same way, and whether specific firms or groups are more active than others.

| Column | Type | Constraints |
|--------|------|-------------|
| id | bigint unsigned | PK, auto-increment |
| user_id | bigint unsigned | FK to users.id, nullable (for unauthenticated events like login attempts) |
| session_id | bigint unsigned | FK to sessions.id, nullable |
| category_id | bigint unsigned | FK to categories.id, nullable |
| action | varchar(100) | required |
| metadata | json | nullable, additional context per action |
| created_at | timestamp | nullable |

The `logs` table is append-only — rows are never updated or deleted.

#### Logged Actions

| Action | When | Metadata |
|--------|------|----------|
| `login` | User completes SSO | `{email, firm_name}` |
| `logout` | User logs out | — |
| `screening_started` | New screening created | — |
| `screening_completed` | Screening submitted | `{score, passed}` |
| `session_started` | New session created | `{category_id}` |
| `session_completed` | Session submitted | `{category_id, score, result}` |
| `session_abandoned` | Session marked abandoned | `{category_id}` |
| `answer_saved` | User saves an answer | `{question_id, question_group_id, value}` |
| `step_viewed` | User navigates to a question group | `{question_group_id}` |

## 6. Frontend Routes & Controllers

All questionnaire routes are custom (defined in `routes/web.php`), handled by custom controllers, and render Vue page components via Inertia.

| Method | Route | Controller | Vue Page | Description |
|--------|-------|------------|----------|-------------|
| GET | `/` | LandingController@index | Landing | Landing page. Describes the application. Has a "Continue" button. |
| GET | `/login` | SocialiteController@redirect | — | Redirects to Azure AD for SSO |
| GET | `/auth/callback` | SocialiteController@callback | — | Azure AD callback, creates/finds user, logs in |
| POST | `/logout` | SocialiteController@logout | — | Logout |
| POST | `/screening` | ScreeningController@store | — | Create a new screening session |
| GET | `/screening/{screening}` | ScreeningController@show | Screening/Show | Pre-screening Yes/No questions (10 questions) |
| PUT | `/screening/{screening}` | ScreeningController@update | — | Save screening answers |
| GET | `/screening/{screening}/result` | ScreeningController@result | Screening/Result | Pre-screening result (pass/fail). If pass, shows category picker. |
| POST | `/sessions` | SessionController@store | — | Create a new session for a selected category (from screening result page) |
| GET | `/sessions/{session}` | SessionController@show | Session/Show | Basic info form + category questionnaire flow |
| PUT | `/sessions/{session}` | SessionController@update | — | Save answers, update basic info, submit session |
| GET | `/sessions/{session}/result` | SessionController@result | Session/Result | View final result after submission |

Nova is served separately at `/cp` with its own authentication. No custom routes needed for Nova.

## 7. Questionnaire Flow

1. User logs in via Azure AD SSO
2. `/` is the landing page — describes the application purpose, has a "Continue" button
3. User clicks Continue → creates a screening session, redirected to pre-screening questions
4. Pre-screening: 10 Yes/No scored questions (1 point per Yes)
5. Pre-screening result:
   - Score < 5 points → No Go result page (red). "Again" button returns to `/`.
   - Score >= 5 points → pass. Page shows category picker to continue.
6. User selects a category → creates a session, redirected to Session/Show
7. Basic Information form (client name, client contact, lead firm name, lead firm contact)
8. All questions are displayed on a single page (not paginated per question or per group). In phase 2, questions will be visually grouped by their question group.
9. Each question renders based on its field configuration:
   - If `has_yes`/`has_no`/`has_na` are all false → render as open text (textarea only)
   - If any are true → render radio buttons for the enabled options
   - If `details` is set → render a details textarea with the appropriate requirement behavior (`required`, `optional`, `req_on_yes`, `req_on_no`)
10. Running score displayed (for scored questions only)
11. Color-coded result indicator updates live (green/yellow/red)
12. Additional comments free text section at the bottom of the page
13. Submit and view result (GO / NO GO / Consult Leadership)
14. Session saved with score and result
15. All result pages have an "Again" button that returns to `/`

## 8. Scoring Logic

### Pre-Screening Threshold

The pre-screening uses a simple pass/fail:
- 10 Yes/No questions, each Yes = 1 point
- Score >= 5 → pass (proceed to category selection)
- Score < 5 → fail (No Go, return to start)

### Category Questionnaire Scoring

- Only questions with `is_scored = true` are scored
- **Yes** = 1 point
- **No** = 0 points
- **Not Applicable** = excluded from scoring (does not count toward total)
- Score calculated server-side and returned to frontend via Inertia props

### Thresholds (same across all categories)

| Score | Result | Color | Meaning |
|-------|--------|-------|---------|
| 10+ points | GO | Green | Pursue the opportunity |
| 5-9 points | Consult Leadership | Yellow | Speak to SL or SSL leadership |
| 1-4 points | NO GO | Red | Do not pursue |

## 9. Nova Resources & Policies

All Nova resources have an associated Laravel Policy that controls authorization.

| Resource | Model | Policy | Menu Item | Notes |
|----------|-------|--------|-----------|-------|
| CategoryResource | Category | CategoryPolicy | No | Lookup data only — no dedicated resource page. Exists as a Nova resource class so it can be referenced in relational fields (BelongsTo, HasMany). No CRUD operations exposed. |
| QuestionGroupResource | QuestionGroup | QuestionGroupPolicy | No | Read-only. Filterable by category. No create, update, or delete. Displayed inline via HasMany on other resources only. |
| QuestionResource | Question | QuestionPolicy | Yes | Only the `text` field is editable by administrators. All other fields (has_yes, has_no, has_na, details, is_scored, sort_order) are read-only after seeding. No create or delete. |
| ScreeningResource | Screening | ScreeningPolicy | Yes | Read-only. Filterable by user and result. View pre-screening scores. No create, update, or delete from Nova. |
| SessionResource | Session | SessionPolicy | Yes | Filterable by user, category, and status. View scores and results. No create, update, or delete from Nova. |
| AnswerResource | Answer | AnswerPolicy | No | Strictly read-only — no create, update, or delete by anyone (administrators included). Viewed within session context only. |
| LogResource | Log | LogPolicy | Yes | Read-only. Filterable by user, category, action, and date range. For usage analytics and statistics. No create, update, or delete. |

### Policy Summary

| Model | View | Create | Update | Delete |
|-------|------|--------|--------|--------|
| Category | Yes | No | No | No |
| QuestionGroup | Yes | No | No | No |
| Question | Yes | No | Yes (text only) | No |
| Screening | Yes | No | No | No |
| Session | Yes | No | No | No |
| Answer | Yes | No | No | No |
| Log | Yes | No | No | No |

### Nova Field Behavior (Global Rule)

Applies to **all** Nova resources without exception:

- **Filterable:** Every field that can be filtered, must be filterable.
- **Sortable:** Every field that can be sorted, must be sortable.
- **Copyable:** Every field that can be copied, must be copyable.

## 10. Nova Excel Export

- **Package:** `maatwebsite/laravel-nova-excel`
- Every Nova resource index gets a `DownloadExcel` action
- Exports all visible columns to `.xlsx`
- Added to every resource's `actions()` method
