# Testing & QA

# Test Plan

# Bilko — Test Plan

**Version:** 1.0
**Date:** 2026-02-23
**Project ID:** bbd77cc0
**Status:** Current — reflects actual codebase and target testing strategy as of 2026-02-23

---

## Table of Contents

1. [Test Philosophy](#1-test-philosophy)
2. [Unit Test Strategy](#2-unit-test-strategy)
3. [Integration Test Strategy](#3-integration-test-strategy)
4. [End-to-End Test Strategy](#4-end-to-end-test-strategy)
5. [Accounting Scenario Tests](#5-accounting-scenario-tests)
6. [Regulatory Compliance Tests](#6-regulatory-compliance-tests)
7. [Performance Benchmarks](#7-performance-benchmarks)
8. [Security Tests](#8-security-tests)
9. [Test Infrastructure](#9-test-infrastructure)
10. [Test Coverage Targets](#10-test-coverage-targets)

---

## 1. Test Philosophy

### 1.1 Existing Tests

The `@bilko/core` package has unit tests written with **Vitest**:

| Test File | Coverage |
|-----------|---------|
| `packages/core/tests/accounting.test.ts` | `validateDoubleEntry`, `createJournalEntry`, `calculateTrialBalance` |
| `packages/core/tests/tax.test.ts` | `calculateVAT`, `getDefaultVATRate`, `getVATRates`, `calculateNetFromGross`, `calculateCIT` |
| `packages/core/tests/multi-currency.test.ts` | `convertCurrency`, `lockExchangeRate`, `calculateForexGainLoss` |
| `packages/core/tests/invoicing.test.ts` | Invoice calculation helpers |
| `packages/core/tests/chart-of-accounts.test.ts` | Chart structure validation |

### 1.2 Test Pyramid

```
          ┌─────────┐
          │   E2E   │  ← Fewer, slower, critical user flows
          │  Tests  │
          └────┬────┘
          ┌────┴────┐
          │  Integ  │  ← API endpoints with real test DB
          │  Tests  │
          └────┬────┘
     ┌─────────┴──────────┐
     │     Unit Tests     │  ← Core engine, services, validators (fast, many)
     └────────────────────┘
```

```mermaid
graph TD
    subgraph PYRAMID["Bilko Test Pyramid"]
        E2E["E2E Tests — 10%<br/>Playwright<br/>5 critical flows<br/>Staging environment<br/>~60s per test"]
        INT["Integration Tests — 30%<br/>Supertest + Vitest<br/>Real PostgreSQL<br/>API endpoints<br/>~5s per test"]
        UNIT["Unit Tests — 60%<br/>Vitest<br/>@bilko/core engine<br/>Pure functions<br/>~50ms per test"]
    end

    E2E --> INT
    INT --> UNIT

    UNIT --> U1["accounting.test.ts"]
    UNIT --> U2["tax.test.ts"]
    UNIT --> U3["multi-currency.test.ts"]
    UNIT --> U4["bank-import.test.ts"]
    UNIT --> U5["chart-of-accounts.test.ts"]

    INT --> I1["auth.test.ts"]
    INT --> I2["invoices.test.ts"]
    INT --> I3["expenses.test.ts"]
    INT --> I4["reports.test.ts"]
    INT --> I5["isolation.test.ts"]

    E2E --> E1["invoice-lifecycle.spec.ts"]
    E2E --> E2["expense-flow.spec.ts"]
    E2E --> E3["bank-reconciliation.spec.ts"]
    E2E --> E4["reports.spec.ts"]
    E2E --> E5["auth.spec.ts"]

    style PYRAMID fill:#f8f9fa,stroke:#dee2e6
    style E2E fill:#dc3545,color:#fff,stroke:#c82333
    style INT fill:#fd7e14,color:#fff,stroke:#e8690b
    style UNIT fill:#198754,color:#fff,stroke:#157347
```

### 1.3 Non-Negotiable Rules

1. **Money is never JavaScript `number`** — all monetary tests use `Decimal.js` or string assertions
2. **Double-entry always balanced** — every test that creates a financial transaction verifies debit = credit
3. **Organization isolation** — cross-org data access must be impossible (tested explicitly)
4. **Immutability** — locked transactions cannot be modified (must throw/fail)
5. **Audit trail** — mutations must create `LoggedAction` entries (tested in integration)

---

## 2. Unit Test Strategy

**Framework:** Vitest (already configured in `packages/core/vitest.config.ts`)
**Run:** `cd packages/core && npx vitest`

### 2.1 Core Accounting Engine (`@bilko/core`)

#### `accounting/index.ts` — Double-Entry Engine

**File:** `packages/core/tests/accounting.test.ts` (EXISTS)

| Test Case | Assertion |
|-----------|-----------|
| Balanced entry: debit = credit | `validateDoubleEntry` returns `true` |
| Unbalanced entry: debit ≠ credit | Returns `false` |
| Less than 2 lines | Returns `false` |
| Negative amounts | Returns `false` |
| Zero amounts | Returns `false` |
| Multiple lines summing to balanced | Returns `true` |
| Decimal amounts with 4dp precision | Returns `true` |
| `createJournalEntry` with valid data | Returns entry unchanged |
| Missing description | Throws "must have a description" |
| Missing date | Throws "must have a date" |
| Unbalanced amounts in error message | Error shows actual debit/credit totals |
| `calculateTrialBalance` from balanced entries | `isBalanced = true`, sums correct |
| `calculateTrialBalance` groups by account number | Same account accumulated correctly |
| `calculateTrialBalance` empty input | `isBalanced = true`, empty rows |
| `calculateTrialBalance` sorts by account number | Rows sorted ascending |

**Additional tests needed:**
```typescript
describe('Immutable transaction locking', () => {
  it('locked transactions cannot have amount changed');
  it('locked transactions cannot change debit/credit accounts');
  it('locked = true after period close');
});
```

---

#### `tax/index.ts` — VAT/CIT Calculator

**File:** `packages/core/tests/tax.test.ts` (EXISTS)

| Test Case | Assertion |
|-----------|-----------|
| Serbia PDV 20% on 1000 | base=1000, tax=200, total=1200 |
| BiH PDV 17% on 1000 | base=1000, tax=170, total=1170 |
| Croatia PDV 25% on 1000 | base=1000, tax=250, total=1250 |
| Zero rate | tax=0, total=base |
| Decimal base amounts (123.45 at 20%) | tax=24.69, total=148.14 |
| Negative amount | Throws "non-negative" |
| Negative rate | Throws "non-negative" |
| Large amounts (999,999,999.9999) | No precision loss |
| `Decimal` input accepted | Same result as string |
| `getDefaultVATRate('RS')` | Returns 20 |
| `getDefaultVATRate('BA')` | Returns 17 |
| `getDefaultVATRate('HR')` | Returns 25 |
| Unsupported country | Throws "Unsupported country" |
| `getVATRates('RS')` | 3 rates: 20, 10, 0 |
| `getVATRates('BA')` | 2 rates: 17, 0 |
| `getVATRates('HR')` | 3 rates: 25, 13, 0 |
| Returns copies (immutable) | Mutation doesn't affect originals |
| Reverse VAT (BiH 1170 gross) | base≈1000, tax≈170 |
| CIT at 15% | 100000 → 15000 |

**Additional tests needed (country modules):**
```typescript
// packages/country-rs/src/tax/index.ts
describe('Serbian tax specifics', () => {
  it('calculateSerbianPDV standard 20%');
  it('calculateSerbianPDV reduced 10%');
  it('calculateSerbianPDV zero rate');
  it('calculateSerbianCIT 15% flat');
  it('qualifiesForPausalRegime: revenue < 6M RSD → true');
  it('qualifiesForPausalRegime: revenue >= 6M RSD → false');
  it('requiresVATRegistration: revenue >= 8M RSD → true');
  it('requiresVATRegistration: revenue < 8M RSD → false');
});

// packages/country-ba/src/tax/index.ts
describe('Bosnian tax specifics', () => {
  it('calculateBosnianPDV single 17% rate');
  it('calculateCITFBiH 10%');
  it('calculateCITRS 10%');
  it('calculateDividendWHT FBiH: dividends 5%');
  it('calculateDividendWHT RS: dividends 10%');
  it('requiresVATRegistration: >= 100000 BAM → true');
});

// packages/country-hr/src/tax/index.ts
describe('Croatian tax specifics', () => {
  it('calculateCroatianPDV standard 25%');
  it('calculateCroatianPDV reduced 13%');
  it('calculateCroatianPDV superReduced 5%');
  it('calculateCroatianCIT: revenue < 1M EUR → 10%');
  it('calculateCroatianCIT: revenue >= 1M EUR → 18%');
  it('qualifiesForPausalni: revenue < 60000 EUR → true');
  it('requiresVATRegistration: revenue >= 60000 EUR → true');
});
```

---

#### `multi-currency/index.ts` — Currency Conversion

**File:** `packages/core/tests/multi-currency.test.ts` (EXISTS)

| Test Case | Assertion |
|-----------|-----------|
| Same currency | Rate = 1, no conversion |
| RSD to EUR at rate 0.0086 | Correct base amount |
| `lockExchangeRate` returns ExchangeRate object | Correct fields |
| `lockExchangeRate` same currency | Throws error |
| `lockExchangeRate` rate ≤ 0 | Throws "must be positive" |
| `convertCurrency` with zero fromRate | Throws |
| `calculateForexGainLoss` gain scenario | `gain > 0`, `loss = 0` |
| `calculateForexGainLoss` loss scenario | `gain = 0`, `loss > 0` |
| `isSupportedCurrency('EUR')` | `true` |
| `isSupportedCurrency('XYZ')` | `false` |
| Precision: toFixed(4) on result | 4 decimal places |

---

#### `bank-import/index.ts` — CSV Parser

**File:** `packages/core/tests/bank-import.test.ts` (MISSING — needs creation)

```typescript
describe('parseCSV', () => {
  it('parses ISO date format YYYY-MM-DD');
  it('parses Balkan dot format DD.MM.YYYY');
  it('parses slash format DD/MM/YYYY');
  it('skips header line');
  it('skips empty lines');
  it('returns empty array for empty string');
  it('returns empty array for header-only CSV');
  it('sets direction: inbound by default');
  it('sets direction: outbound when field is "outbound"');
  it('handles quoted fields with commas');
  it('generates deterministic IDs for dedup');
});

describe('detectDuplicates', () => {
  it('detects exact duplicate by date+amount+currency+reference');
  it('returns empty array when no duplicates');
  it('returns empty array when either list is empty');
  it('does NOT flag as duplicate if amount differs');
  it('does NOT flag as duplicate if date differs');
  it('does NOT flag as duplicate if reference differs (but amount/date same)');
});
```

---

### 2.2 Validator Unit Tests

**Framework:** Vitest
**Location:** `apps/api/src/validators/*.ts`

```typescript
describe('Invoice validators (createInvoiceSchema)', () => {
  it('valid invoice passes');
  it('missing customerId fails');
  it('invalid date format fails');
  it('negative unitPrice fails');
  it('empty items array fails');
  it('taxRate > 100 fails');
  it('invalid currencyCode (5 chars) fails');
  it('invalid UUID for customerId fails');
});

describe('Auth validators (registerSchema)', () => {
  it('valid registration passes');
  it('invalid email fails');
  it('password too short fails (< 8 chars)');
  it('invalid country code (not RS/BA/HR) fails');
  it('missing organizationName fails');
});
```

---

## Integration Test Architecture

```mermaid
sequenceDiagram
    participant TC as Test Case
    participant ST as Supertest
    participant APP as Express App
    participant MID as Middleware<br/>(Auth + RBAC)
    participant SVC as Service Layer
    participant PRI as Prisma ORM
    participant DB as Test PostgreSQL

    TC->>ST: HTTP request + Bearer token
    ST->>APP: Forward request
    APP->>MID: Authenticate JWT
    MID->>MID: Verify organizationId scope
    MID->>SVC: Authorized request
    SVC->>PRI: DB query (org-scoped)
    PRI->>DB: Parameterized SQL
    DB-->>PRI: Result rows
    PRI-->>SVC: Typed objects
    SVC-->>APP: Response data
    APP-->>ST: HTTP response
    ST-->>TC: Assert status + body

    Note over DB: beforeEach: seed<br/>afterEach: truncate<br/>(reverse FK order)
```

---

## 3. Integration Test Strategy

**Framework:** Supertest + Vitest (or Jest)
**Database:** Test PostgreSQL instance (separate from dev/prod)
**Setup:** Prisma migrations applied before tests; data seeded per test suite; truncated after each test

### 3.1 Test Database Setup

```typescript
// test/setup.ts
import { prisma } from '../src/lib/prisma';
import { execSync } from 'child_process';

beforeAll(async () => {
  // Apply migrations to test DB
  execSync('npx prisma migrate deploy', { env: { DATABASE_URL: process.env.TEST_DATABASE_URL } });
});

beforeEach(async () => {
  // Seed minimal data: 1 org, 1 owner user, default accounts
  await seedTestOrg();
});

afterEach(async () => {
  // Clean up in reverse FK order
  await prisma.loggedAction.deleteMany();
  await prisma.bankTransaction.deleteMany();
  await prisma.bankAccount.deleteMany();
  await prisma.transaction.deleteMany();
  await prisma.invoiceItem.deleteMany();
  await prisma.invoice.deleteMany();
  await prisma.expense.deleteMany();
  await prisma.contact.deleteMany();
  await prisma.account.deleteMany();
  await prisma.user.deleteMany();
  await prisma.organization.deleteMany();
});
```

### 3.2 Auth Endpoints

```typescript
describe('POST /api/v1/auth/register', () => {
  it('creates org + owner user, returns tokens');
  it('returns 409 for duplicate email');
  it('returns 400 for missing required fields');
  it('password is hashed (not stored plain)');
  it('sets refreshToken httpOnly cookie');
  it('org baseCurrency defaults to EUR');
});

describe('POST /api/v1/auth/login', () => {
  it('returns accessToken + sets cookie on valid credentials');
  it('returns 401 for wrong password');
  it('returns 401 for non-existent email');
  it('updates lastLoginAt on success');
  it('rememberMe=true extends cookie to 30 days');
});

describe('POST /api/v1/auth/refresh', () => {
  it('returns new accessToken from valid refresh cookie');
  it('returns 401 when no cookie');
  it('returns 401 for expired refresh token');
});

describe('POST /api/v1/auth/logout', () => {
  it('clears refreshToken cookie');
  it('returns 204');
});

describe('GET /api/v1/auth/me', () => {
  it('returns user + org data for valid token');
  it('returns 401 for missing token');
  it('returns 401 for expired token');
});
```

### 3.3 Invoice Endpoints

```typescript
describe('GET /api/v1/invoices', () => {
  it('returns paginated invoices for organization');
  it('does NOT return invoices from other orgs');
  it('filters by status');
  it('filters by customerId');
  it('filters by date range');
  it('returns empty data array when no invoices');
  it('returns 401 without auth');
});

describe('POST /api/v1/invoices', () => {
  it('creates invoice in draft status');
  it('auto-generates invoice number INV-YYYY-001');
  it('increments invoice number sequentially');
  it('calculates subtotal correctly from line items');
  it('calculates taxAmount at specified rate');
  it('sets baseAmount = totalAmount when currency = baseCurrency');
  it('locks exchange rate from ExchangeRate table');
  it('returns 404 for non-existent customerId');
  it('returns 400 for contact that is vendor only (not customer)');
});

describe('PATCH /api/v1/invoices/:id/status → send', () => {
  it('changes status from draft to sent');
  it('creates Transaction: DR Receivable / CR Revenue');
  it('transaction.amount = invoice.totalAmount');
  it('transaction.referenceType = invoice, referenceId = invoice.id');
  it('returns 400 if invoice already sent');
  it('returns 400 if required accounts not in chart of accounts');
});

describe('PATCH /api/v1/invoices/:id/status → mark-paid', () => {
  it('changes status from sent to paid');
  it('creates Transaction: DR Bank / CR Receivable');
  it('sets paidAt to provided date');
  it('returns 400 if invoice is still draft');
});

describe('DELETE /api/v1/invoices/:id', () => {
  it('deletes draft invoice');
  it('returns 400 when trying to delete sent invoice');
  it('returns 404 for non-existent invoice');
  it('cannot delete invoice from another org');
});
```

### 3.4 Expense Endpoints

```typescript
describe('POST /api/v1/expenses', () => {
  it('creates expense in pending status');
  it('auto-generates expense number EXP-YYYY-001');
  it('stores taxAmount separately');
  it('locks exchange rate at expenseDate');
});

describe('PATCH /api/v1/expenses/:id/approve', () => {
  it('changes status from pending to approved');
  it('creates Transaction: DR Expense / CR Payable');
  it('returns 400 for non-pending expense');
});

describe('PATCH /api/v1/expenses/:id/pay', () => {
  it('changes status from approved to paid');
  it('creates Transaction: DR Payable / CR Bank');
  it('returns 400 for non-approved expense');
});
```

### 3.5 Transaction Endpoints

```typescript
describe('POST /api/v1/transactions (manual journal)', () => {
  it('accountant can create manual transaction');
  it('viewer cannot create manual transaction (403)');
  it('debit and credit account must be different (422)');
  it('debit account must belong to same org (404)');
  it('credit account must belong to same org (404)');
  it('creates transaction with correct amounts');
  it('referenceType = manual');
});

describe('GET /api/v1/transactions', () => {
  it('filters by accountId (both debit and credit sides)');
  it('filters by referenceType');
  it('filters by date range');
  it('does not return transactions from other orgs');
});
```

### 3.6 Report Endpoints

```typescript
describe('GET /api/v1/reports/trial-balance', () => {
  it('returns balanced trial balance (totalDebits = totalCredits)');
  it('returns balanced = true when no transactions');
  it('includes all accounts with transactions');
  it('debit-normal accounts: balance = debit - credit');
  it('credit-normal accounts: balance = credit - debit');
});

describe('GET /api/v1/reports/profit-loss', () => {
  it('revenue accounts (type=4) in revenue section');
  it('expense accounts (type=5) in expenses section');
  it('netProfit = revenue - expenses');
  it('respects date range filter');
});

describe('GET /api/v1/reports/vat', () => {
  it('outputVAT sum from invoice.taxAmount for sent/paid invoices');
  it('inputVAT sum from expense.taxAmount for approved/paid expenses');
  it('netVAT = outputVAT - inputVAT');
  it('draft invoices excluded from output VAT');
  it('pending expenses excluded from input VAT');
});
```

### 3.7 Multi-Tenancy Isolation Tests

```typescript
describe('Organization isolation', () => {
  let org1Token: string;
  let org2Token: string;
  let org1InvoiceId: string;

  beforeEach(async () => {
    // Create two separate organizations
    org1Token = await registerAndLogin('org1@test.rs');
    org2Token = await registerAndLogin('org2@test.rs');
    // Create invoice in org1
    const res = await createInvoice(org1Token, { ... });
    org1InvoiceId = res.body.id;
  });

  it('org2 cannot GET invoice from org1 (returns 404)');
  it('org2 cannot PUT invoice from org1 (returns 404)');
  it('org2 cannot DELETE invoice from org1 (returns 404 or 404)');
  it('org2 list invoices does not include org1 invoices');
  it('org2 cannot GET org1 contacts');
  it('org2 cannot GET org1 transactions');
  it('org2 cannot GET org1 bank accounts');
  it('org2 trial balance does not include org1 accounts');
});
```

---

## Invoice Lifecycle — Integration Test Flow

```mermaid
stateDiagram-v2
    [*] --> Draft: POST /api/v1/invoices<br/>(test: creates draft, generates INV-YYYY-NNN)

    Draft --> Sent: PATCH /status → send<br/>(test: DR Receivable / CR Revenue)
    Draft --> Deleted: DELETE /invoices/:id<br/>(test: draft can be deleted)
    Sent --> Paid: PATCH /status → mark-paid<br/>(test: DR Bank / CR Receivable)
    Sent --> Deleted_ERR: DELETE attempt<br/>(test: returns 400 — cannot delete sent)
    Paid --> [*]: Trial balance balanced<br/>(test: Receivable = 0, balanced=true)

    state "Sent → mark-paid" as Paid {
        [*] --> TX_Created: Transaction created
        TX_Created --> GL_Updated: General Ledger updated
        GL_Updated --> Reconciled: BankTransaction matched
    }

    note right of Draft
        Auto-generates invoice number
        Locks exchange rate if foreign currency
        Validates customerId belongs to org
    end note

    note right of Sent
        Creates accounting transaction
        referenceType = invoice
        referenceId = invoice.id
    end note
```

---

## 4. End-to-End Test Strategy

**Framework:** Playwright
**Target:** Critical business flows that span the full stack
**Environment:** Staging environment with seeded data

### 4.1 User Registration and Setup

```typescript
test('New user can register, set up org, and access dashboard', async ({ page }) => {
  // 1. Navigate to /register
  // 2. Fill in org name, country=RS, email, password
  // 3. Submit → redirected to dashboard
  // 4. Dashboard loads with zero-state (empty metrics)
  // 5. Logout → redirected to /login
  // 6. Login with same credentials → dashboard again
});
```

### 4.2 Complete Invoice Flow

```typescript
test('Create invoice → send → mark paid → check P&L', async ({ page }) => {
  // Step 1: Create contact (customer)
  await page.goto('/contacts/new');
  await fillContactForm({ name: 'Test Customer', type: 'customer' });
  await page.click('button[type=submit]');

  // Step 2: Create invoice
  await page.goto('/invoices/new');
  // Fill 6-step wizard: customer, date, items (1000 RSD + 20% PDV), review
  await completeInvoiceWizard({ customer: 'Test Customer', amount: 1000, taxRate: 20 });
  // Verify: status = draft, total = 1200 RSD

  // Step 3: Send invoice
  await page.click('button:text("Send Invoice")');
  // Verify: status = sent

  // Step 4: Mark paid
  await page.click('button:text("Mark as Paid")');
  await page.fill('[name=paidAt]', '2026-02-20');
  await page.click('button:text("Confirm")');
  // Verify: status = paid, paidAt set

  // Step 5: Check P&L report
  await page.goto('/reports?from=2026-01-01&to=2026-12-31');
  await expect(page.locator('[data-testid=revenue-total]')).toContainText('1,200.00');

  // Step 6: Check trial balance (balanced)
  await page.goto('/reports/trial-balance');
  await expect(page.locator('[data-testid=balanced-indicator]')).toBeVisible();
});
```

### 4.3 Expense Approval Flow

```typescript
test('Create expense → approve → pay → check trial balance', async ({ page }) => {
  // Step 1: Create expense (office supplies, 5000 RSD, 17% PDV)
  // Step 2: Approve expense → DR Office Expense / CR Accounts Payable
  // Step 3: Pay expense → DR Accounts Payable / CR Bank
  // Step 4: Verify trial balance is still balanced
  // Step 5: Verify P&L shows expense in correct category
});
```

### 4.4 Bank Reconciliation Flow

```typescript
test('Import bank statement → reconcile with invoice payment', async ({ page }) => {
  // Pre-condition: Paid invoice exists (DR Bank / CR Receivable transaction)
  // Step 1: Go to Banking page
  // Step 2: Import CSV with matching payment entry
  // Step 3: Verify imported: 1, duplicates: 0
  // Step 4: Match bank transaction to GL transaction
  // Step 5: Verify BankTransaction.reconciled = true
  // Step 6: Verify Transaction.reconciled = true
});
```

### 4.5 VAT Report Generation

```typescript
test('VAT report reflects invoices and expenses for period', async ({ page }) => {
  // Pre-condition: 3 sent invoices with 20% PDV, 2 approved expenses with PDV
  // Step 1: Navigate to Reports → VAT Report
  // Step 2: Set period to current month
  // Step 3: Verify: output VAT = sum of invoice tax amounts
  // Step 4: Verify: input VAT = sum of expense tax amounts
  // Step 5: Verify: net VAT = output - input
  // Step 6: Download/export VAT report (future feature)
});
```

---

## E2E Test Flow — Complete Invoice Lifecycle

```mermaid
flowchart TD
    START([Browser: /login]) --> LOGIN[Fill credentials<br/>demo@bilko.io]
    LOGIN --> DASH[Dashboard loaded<br/>assert: zero-state metrics]

    DASH --> NEW_CONTACT["/contacts/new<br/>Create: Test Customer"]
    NEW_CONTACT --> NEW_INV["/invoices/new<br/>6-step wizard"]

    NEW_INV --> W1["Step 1: Select Customer<br/>assert: customer appears in dropdown"]
    W1 --> W2["Step 2: Set dates<br/>invoiceDate, dueDate"]
    W2 --> W3["Step 3: Add line items<br/>1000 RSD + 20% PDV = 1200 RSD total"]
    W3 --> W4["Step 4: Currency & exchange rate"]
    W4 --> W5["Step 5: Notes / payment terms"]
    W5 --> W6["Step 6: Review & Create<br/>assert: subtotal=1000, tax=200, total=1200"]

    W6 --> INV_DETAIL["Invoice detail page<br/>assert: status=draft, number=INV-YYYY-NNN"]
    INV_DETAIL --> SEND["Click: Send Invoice<br/>assert: status=sent"]
    SEND --> PAY["Click: Mark as Paid<br/>Enter paidAt date"]
    PAY --> PAID["assert: status=paid, paidAt set"]

    PAID --> PL["/reports?from=...&to=...<br/>assert: revenue-total = 1,200.00"]
    PL --> TB["/reports/trial-balance<br/>assert: balanced-indicator visible<br/>assert: Receivable balance = 0"]

    style START fill:#198754,color:#fff
    style TB fill:#0d6efd,color:#fff
    style PAID fill:#198754,color:#fff
```

---

## 5. Accounting Scenario Tests

These tests verify correctness of the double-entry system under real-world accounting scenarios.

### 5.1 Invoice → Payment → Reconciliation

**Scenario:** Company issues invoice, receives payment, reconciles bank statement

```typescript
test('Full invoice lifecycle creates correct ledger entries', async () => {
  // 1. Create invoice: 100,000 RSD net + 20,000 RSD PDV = 120,000 RSD total
  // 2. Send invoice → Transaction: DR Receivable 120,000 / CR Revenue 120,000
  // 3. Mark paid → Transaction: DR Bank 120,000 / CR Receivable 120,000
  // 4. Trial balance: Bank +120,000 / Revenue +120,000 (balanced)
  // 5. Receivable account balance = 0 (opened and closed)
  // 6. General ledger shows both entries on Receivable account
  const trialBalance = await getTrialBalance();
  expect(trialBalance.balanced).toBe(true);
  const receivable = trialBalance.accounts.find(a => a.code.startsWith('12'));
  expect(receivable.balance).toBe('0.0000');
});
```

### 5.2 Multi-Currency Invoice

**Scenario:** RSD-based company invoices EUR customer

```typescript
test('EUR invoice stored and reported in RSD base currency', async () => {
  // Exchange rate: 1 EUR = 117.25 RSD (locked at invoice date)
  // Invoice: 1,000 EUR + 200 EUR PDV = 1,200 EUR
  // Expected baseAmount: 1,200 × 117.25 = 140,700 RSD

  const invoice = await createInvoice({
    currencyCode: 'EUR',
    items: [{ quantity: 1, unitPrice: 1000, taxRate: 20 }],
    invoiceDate: '2026-02-01' // rate exists for this date
  });

  expect(invoice.currencyCode).toBe('EUR');
  expect(invoice.totalAmount).toBe('1200.0000');
  expect(invoice.exchangeRate).toBe('117.250000');
  expect(invoice.baseAmount).toBe('140700.0000');

  // When paid: DR Bank 140,700 RSD / CR Receivable 140,700 RSD
  await markPaid(invoice.id, '2026-02-15');
  const transaction = await getTransactionForInvoice(invoice.id, 'payment');
  expect(transaction.baseAmount).toBe('140700.0000');
});
```

### 5.3 VAT Calculation Accuracy

```typescript
test('VAT calculated with Decimal precision, no float errors', async () => {
  // Known float trap: 0.1 + 0.2 ≠ 0.3 in JavaScript float
  // Test with amounts that expose float precision issues
  const result = calculateVAT('123.45', '20');
  // Expected: tax = 123.45 × 0.20 = 24.69 (not 24.690000000000003)
  expect(result.tax.toString()).toBe('24.6900');
  expect(result.total.toString()).toBe('148.1400');

  // Large amount
  const large = calculateVAT('999999.9999', '17');
  expect(large.tax.toString()).toBe('169999.9998');  // exact
});
```

### 5.4 Trial Balance After Multiple Transactions

```typescript
test('Trial balance remains balanced after 10 invoices and 5 expenses', async () => {
  // Create 10 invoices (all sent + paid)
  for (let i = 0; i < 10; i++) {
    const inv = await createAndSendInvoice(orgId, 10000 + i * 100);
    await markPaid(inv.id, today);
  }
  // Create 5 expenses (all approved + paid)
  for (let i = 0; i < 5; i++) {
    const exp = await createAndApproveExpense(orgId, 5000 + i * 50);
    await payExpense(exp.id);
  }

  const tb = await getTrialBalance(orgId);
  expect(tb.balanced).toBe(true);
  // Total debits must equal total credits
  expect(new Decimal(tb.totals.debit)).toEqual(new Decimal(tb.totals.credit));
});
```

### 5.5 Expense Approval Double-Entry

```typescript
test('Expense approval creates correct DR Expense / CR Payable entry', async () => {
  const expense = await createExpense({ amount: 5000, taxRate: 17 }); // 850 PDV
  await approveExpense(expense.id);

  const transactions = await getTransactionsForExpense(expense.id);
  expect(transactions).toHaveLength(1);
  const tx = transactions[0];

  // Verify debit is an expense account
  expect(tx.debitAccountCode).toMatch(/^5/);
  // Verify credit is payable
  expect(tx.creditAccountCode).toMatch(/^22/);
  // Amount matches expense amount
  expect(tx.amount).toBe('5000.0000');
});
```

---

## 6. Regulatory Compliance Tests

### 6.1 Serbia (RS)

```typescript
describe('Serbia regulatory compliance', () => {
  it('PDV 20% standard rate applied to default supplies', async () => {
    const result = calculateSerbianPDV('10000', 'standard');
    expect(result).toBe('2000.00');
  });

  it('PDV 10% reduced rate applied to food/medicine', async () => {
    const result = calculateSerbianPDV('10000', 'reduced');
    expect(result).toBe('1000.00');
  });

  it('Business below 8M RSD threshold does not require VAT registration', () => {
    expect(requiresVATRegistration('7999999')).toBe(false);
  });

  it('Business at 8M RSD threshold requires VAT registration', () => {
    expect(requiresVATRegistration('8000000')).toBe(true);
  });

  it('Business below 6M RSD qualifies for pausal regime', () => {
    expect(qualifiesForPausalRegime('5999999')).toBe(true);
  });

  it('CIT calculated at flat 15%', () => {
    const cit = calculateSerbianCIT('100000');
    expect(cit).toBe('15000.00');
  });

  it('Invoice number follows Serbian format requirements', () => {
    // INV-YYYY-NNN format with sequential numbering
    expect(invoiceNumber).toMatch(/^INV-\d{4}-\d{3,}$/);
  });

  it('VAT report groups output and input VAT separately', async () => {
    const report = await getVATReport(orgId, { from: '2026-01-01', to: '2026-01-31' });
    expect(report).toHaveProperty('outputVAT');
    expect(report).toHaveProperty('inputVAT');
    expect(report).toHaveProperty('netVAT');
    expect(new Decimal(report.netVAT)).toEqual(
      new Decimal(report.outputVAT.total).sub(new Decimal(report.inputVAT.total))
    );
  });
});
```

### 6.2 Bosnia & Herzegovina (BA)

```typescript
describe('BiH regulatory compliance', () => {
  it('Single PDV rate of 17% applied uniformly', () => {
    const result = calculateBosnianPDV('10000');
    expect(result).toBe('1700.00');
  });

  it('BiH has no reduced VAT rate (only standard and zero)', () => {
    const rates = Object.keys(bosnianVATRates);
    expect(rates).toEqual(['standard', 'zero']);
  });

  it('VAT registration required at 100,000 BAM', () => {
    expect(requiresVATRegistration('99999')).toBe(false);
    expect(requiresVATRegistration('100000')).toBe(true);
  });

  it('FBiH CIT at 10%', () => {
    expect(calculateCITFBiH('100000')).toBe('10000.00');
  });

  it('RS CIT at 10%', () => {
    expect(calculateCITRS('100000')).toBe('10000.00');
  });

  it('Dividend WHT: FBiH 5%, RS 10%', () => {
    expect(calculateDividendWHT('100000', 'fbih')).toBe('5000.00');
    expect(calculateDividendWHT('100000', 'rs')).toBe('10000.00');
  });
});
```

### 6.3 Croatia (HR)

```typescript
describe('Croatia regulatory compliance', () => {
  it('Standard PDV rate is 25%', () => {
    const result = calculateCroatianPDV('10000', 'standard');
    expect(result).toBe('2500.00');
  });

  it('Reduced PDV rate is 13% (food, accommodation)', () => {
    const result = calculateCroatianPDV('10000', 'reduced');
    expect(result).toBe('1300.00');
  });

  it('Super-reduced PDV rate is 5% (books, medicines)', () => {
    const result = calculateCroatianPDV('10000', 'superReduced');
    expect(result).toBe('500.00');
  });

  it('CIT 10% for small business (revenue < 1M EUR)', () => {
    const cit = calculateCroatianCIT('50000', '900000');
    expect(cit).toBe('5000.00');
  });

  it('CIT 18% for large business (revenue >= 1M EUR)', () => {
    const cit = calculateCroatianCIT('50000', '1000000');
    expect(cit).toBe('9000.00');
  });

  it('VAT registration threshold 60,000 EUR (EU 2025 aligned)', () => {
    expect(requiresVATRegistration('59999')).toBe(false);
    expect(requiresVATRegistration('60000')).toBe(true);
  });
});
```

### 6.4 Audit Trail Compliance

```typescript
describe('Immutable audit trail', () => {
  it('LoggedAction created on invoice create', async () => {
    await createInvoice(orgId, ...);
    const logs = await prisma.loggedAction.findMany({
      where: { tableName: 'invoices', action: 'INSERT' }
    });
    expect(logs).toHaveLength(1);
    expect(logs[0].rowData).toBeTruthy();
  });

  it('LoggedAction created on invoice status change', async () => {
    await sendInvoice(invoiceId);
    const logs = await prisma.loggedAction.findMany({
      where: { tableName: 'invoices', action: 'UPDATE' }
    });
    expect(logs.length).toBeGreaterThan(0);
    expect(logs[0].changedFields).toHaveProperty('status');
  });

  it('LoggedAction cannot be deleted', async () => {
    // Attempt to delete a log entry — should fail (policy enforced at app or DB level)
    await expect(prisma.loggedAction.delete({ where: { eventId: logs[0].eventId } }))
      .rejects.toThrow();
  });

  it('locked transaction cannot be updated', async () => {
    await prisma.transaction.update({
      where: { id: txId },
      data: { locked: true }
    });
    // Attempt to change amount of locked transaction via API
    const response = await request(app)
      .put(`/api/v1/transactions/${txId}`)
      .set('Authorization', `Bearer ${token}`)
      .send({ amount: 99999 });
    expect(response.status).toBe(400);
  });
});
```

### 6.5 Record Retention

```typescript
describe('Record retention requirements', () => {
  it('Deleted invoices remain in LoggedAction with full row data', async () => {
    const invoice = await createInvoice(orgId);
    const invoiceId = invoice.id;
    await deleteInvoice(invoiceId);
    // Invoice deleted from invoices table
    const inv = await prisma.invoice.findUnique({ where: { id: invoiceId } });
    expect(inv).toBeNull();
    // But audit log captures the full row
    const log = await prisma.loggedAction.findFirst({
      where: { tableName: 'invoices', action: 'DELETE' }
    });
    expect(log.rowData).toBeTruthy();
    expect(JSON.parse(log.rowData).id).toBe(invoiceId);
  });
});
```

---

## 7. Performance Benchmarks

**Tool:** k6 (load testing), Lighthouse (frontend)

### 7.1 API Response Time Targets

| Endpoint | Target (P95) | Max Acceptable |
|----------|-------------|----------------|
| `GET /api/v1/health` | < 10ms | < 50ms |
| `POST /api/v1/auth/login` | < 300ms | < 1s |
| `GET /api/v1/invoices` (20 items) | < 200ms | < 500ms |
| `POST /api/v1/invoices` | < 500ms | < 1s |
| `GET /api/v1/reports/profit-loss` | < 500ms | < 2s |
| `GET /api/v1/reports/trial-balance` | < 1s | < 3s |
| `GET /api/v1/reports/general-ledger` | < 2s | < 5s |
| `POST /api/v1/bank-accounts/:id/import` (100 rows) | < 1s | < 3s |

### 7.2 Load Test Scenarios

```javascript
// k6 scenario: Normal business day load
export const options = {
  scenarios: {
    normal_load: {
      executor: 'constant-vus',
      vus: 50,
      duration: '5m',
    },
    spike: {
      executor: 'ramping-vus',
      startVUs: 0,
      stages: [
        { duration: '30s', target: 200 },
        { duration: '1m', target: 200 },
        { duration: '30s', target: 0 },
      ],
    }
  },
  thresholds: {
    'http_req_duration{type:api}': ['p(95)<500'],
    'http_req_failed': ['rate<0.01'], // < 1% error rate
  }
};
```

### 7.3 Database Performance

| Operation | Target |
|-----------|--------|
| Invoice list query (org with 10K invoices) | < 100ms |
| Trial balance (org with 1K accounts, 100K transactions) | < 2s |
| Exchange rate lookup | < 10ms (covered by index) |
| Audit log insert | < 5ms |

### 7.4 Frontend Performance (Lighthouse)

| Metric | Target |
|--------|--------|
| First Contentful Paint (FCP) | < 1.5s |
| Largest Contentful Paint (LCP) | < 2.5s |
| Time to Interactive (TTI) | < 3.5s |
| Cumulative Layout Shift (CLS) | < 0.1 |
| Lighthouse Performance Score | > 90 |

---

## 8. Security Tests

### 8.1 Authentication Security

```typescript
describe('Authentication security', () => {
  it('rejected with 401 for missing Authorization header');
  it('rejected with 401 for malformed Bearer token');
  it('rejected with 401 for expired access token');
  it('rejected with 401 for tampered JWT signature');
  it('rejected with 401 for wrong JWT_SECRET');
  it('access token cannot be used as refresh token');
  it('refresh token cannot be used as access token');
  it('tokens have correct issuer and audience claims');
});
```

### 8.2 RBAC Authorization

```typescript
describe('Role-based access control', () => {
  it('viewer cannot create invoices (403)');
  it('viewer cannot create expenses (403)');
  it('viewer cannot create manual transactions (403)');
  it('accountant cannot change user roles (403)');
  it('accountant cannot invite users (403)');
  it('admin cannot change owner role (403)');
  it('owner can change any role');
  it('user cannot change their own role');
  it('user cannot delete themselves');
});
```

### 8.3 SQL Injection

```typescript
describe('SQL injection prevention', () => {
  it('invoice search with SQL payload returns 400 (Zod validation)', async () => {
    const res = await request(app)
      .get('/api/v1/invoices?customerId=\'; DROP TABLE invoices; --')
      .set('Authorization', `Bearer ${token}`);
    expect(res.status).toBe(400); // Zod rejects invalid UUID
  });

  it('Prisma parameterizes all queries (no raw SQL in services)');
});
```

### 8.4 Cross-Site Scripting (XSS)

```typescript
describe('XSS prevention', () => {
  it('contact name with script tag is stored as plain text', async () => {
    const name = '<script>alert("xss")</script>';
    const contact = await createContact({ name });
    expect(contact.name).toBe(name); // stored as-is
    // API response should not execute as HTML (verified by Content-Type: application/json)
  });

  it('Content-Security-Policy header blocks inline scripts', async () => {
    const res = await request(app).get('/api/v1/health');
    const csp = res.headers['content-security-policy'];
    expect(csp).toContain("script-src 'self'");
    expect(csp).not.toContain("'unsafe-eval'");
  });
});
```

### 8.5 Rate Limiting

```typescript
describe('Rate limiting', () => {
  it('general API limit: 100 requests per 15 min per IP', async () => {
    // Make 101 requests from same IP
    const responses = await makeRequests(101, '/api/v1/health');
    const lastResponse = responses[100];
    expect(lastResponse.status).toBe(429);
  });

  it('auth endpoints have stricter rate limit', async () => {
    // Make rapid login attempts — triggers auth rate limiter before general
    const responses = await makeLoginAttempts(20);
    expect(responses.some(r => r.status === 429)).toBe(true);
  });
});
```

### 8.6 Data Isolation / Multi-Tenant Security

```typescript
describe('Tenant isolation security', () => {
  it('cannot access another org invoice by ID (returns 404, not 403)');
  // Note: returning 404 instead of 403 prevents enumeration attacks
  it('cannot access another org transactions by reference ID');
  it('cannot access another org users via /api/v1/users');
  it('cannot access another org bank accounts');
  it('PATCH invoice from another org returns 404');
  it('DELETE invoice from another org returns 404');
});
```

### 8.7 CORS

```typescript
describe('CORS policy', () => {
  it('requests from bilko.io are allowed');
  it('requests from unknown origin are rejected with CORS error');
  it('OPTIONS preflight returns correct headers');
  it('credentials (cookies) allowed with CORS');
});
```

### 8.8 Security Headers

```typescript
describe('Security headers', () => {
  it('X-Frame-Options: deny (clickjacking protection)');
  it('X-Content-Type-Options: nosniff');
  it('Strict-Transport-Security: maxAge=31536000; includeSubDomains; preload');
  it('Content-Security-Policy present');
  it('X-Powered-By header removed (helmet default)');
});
```

---

## CI/CD Test Pipeline

```mermaid
flowchart TD
    PUSH["git push / PR opened"] --> CI["GitHub Actions triggered"]

    CI --> J1["Job: unit-tests<br/>ubuntu-latest<br/>No DB required"]
    CI --> J2["Job: integration-tests<br/>ubuntu-latest<br/>postgres:15 service"]
    CI --> J3["Job: e2e-tests<br/>ubuntu-latest<br/>Full stack startup"]

    J1 --> U1["npm ci"]
    U1 --> U2["cd packages/core && npx vitest run"]
    U2 --> U3{Coverage >= 80%?}
    U3 -->|Yes| U_OK["PASS"]
    U3 -->|No| U_FAIL["FAIL — block merge"]

    J2 --> I1["npm ci"]
    I1 --> I2["npx prisma migrate deploy<br/>(TEST_DATABASE_URL)"]
    I2 --> I3["npm run test:integration<br/>(apps/api)"]
    I3 --> I4{All assertions pass?}
    I4 -->|Yes| I_OK["PASS"]
    I4 -->|No| I_FAIL["FAIL — block merge"]

    J3 --> E1["npx playwright install --with-deps"]
    E1 --> E2["npm run dev (staging seed)"]
    E2 --> E3["npm run test:e2e"]
    E3 --> E4{All flows pass?}
    E4 -->|Yes| E_OK["PASS"]
    E4 -->|No| E_FAIL["FAIL — screenshot + video saved"]

    U_OK --> MERGE{All jobs passed?}
    I_OK --> MERGE
    E_OK --> MERGE
    MERGE -->|Yes| DEPLOY["Allow merge to main"]
    MERGE -->|No| BLOCK["Block PR merge"]

    style PUSH fill:#6c757d,color:#fff
    style DEPLOY fill:#198754,color:#fff
    style BLOCK fill:#dc3545,color:#fff
    style U_FAIL fill:#dc3545,color:#fff
    style I_FAIL fill:#dc3545,color:#fff
    style E_FAIL fill:#dc3545,color:#fff
```

---

## 9. Test Infrastructure

### 9.1 Directory Structure

```
Bilko/
├── packages/core/
│   ├── tests/                         ← Unit tests (EXISTS)
│   │   ├── accounting.test.ts
│   │   ├── tax.test.ts
│   │   ├── multi-currency.test.ts
│   │   ├── invoicing.test.ts
│   │   └── chart-of-accounts.test.ts
│   └── vitest.config.ts               ← Vitest config (EXISTS)
│
├── apps/api/
│   └── tests/                         ← To be created
│       ├── setup.ts                   ← DB setup/teardown
│       ├── helpers/
│       │   ├── auth.helper.ts         ← Login/register helpers
│       │   └── factory.ts             ← Test data factories
│       ├── integration/
│       │   ├── auth.test.ts
│       │   ├── invoices.test.ts
│       │   ├── expenses.test.ts
│       │   ├── contacts.test.ts
│       │   ├── accounts.test.ts
│       │   ├── transactions.test.ts
│       │   ├── reports.test.ts
│       │   ├── banking.test.ts
│       │   ├── settings.test.ts
│       │   └── isolation.test.ts
│       └── security/
│           ├── auth-security.test.ts
│           ├── rbac.test.ts
│           ├── injection.test.ts
│           └── headers.test.ts
│
└── e2e/                               ← To be created
    ├── playwright.config.ts
    ├── fixtures/
    │   └── test-data.ts
    └── tests/
        ├── auth.spec.ts
        ├── invoice-lifecycle.spec.ts
        ├── expense-flow.spec.ts
        ├── bank-reconciliation.spec.ts
        └── reports.spec.ts
```

### 9.2 Environment Variables for Testing

```bash
# Test environment
TEST_DATABASE_URL="postgresql://bilko_test:password@localhost:5432/bilko_test"
JWT_SECRET="test-jwt-secret-not-for-production"
JWT_REFRESH_SECRET="test-refresh-secret-not-for-production"
NODE_ENV="test"
```

### 9.3 CI Pipeline Integration

```yaml
# .github/workflows/test.yml (target)
jobs:
  unit-tests:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
      - run: npm ci
      - run: cd packages/core && npx vitest run

  integration-tests:
    runs-on: ubuntu-latest
    services:
      postgres:
        image: postgres:15
        env:
          POSTGRES_DB: bilko_test
          POSTGRES_PASSWORD: password
    steps:
      - uses: actions/checkout@v4
      - run: npm ci
      - run: npx prisma migrate deploy
        env:
          DATABASE_URL: ${{ env.TEST_DATABASE_URL }}
      - run: npm run test:integration
        working-directory: apps/api

  e2e-tests:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: npx playwright install --with-deps
      - run: npm run test:e2e
        env:
          PLAYWRIGHT_BASE_URL: http://localhost:3000
```

---

## 10. Test Coverage Targets

| Module | Unit Coverage | Integration Coverage |
|--------|-------------|-------------------|
| `@bilko/core` accounting | 95% (near complete) | N/A |
| `@bilko/core` tax | 95% (near complete) | N/A |
| `@bilko/core` multi-currency | 90% | N/A |
| `@bilko/core` bank-import | 80% (tests missing) | N/A |
| `@bilko/country-rs` tax | 0% (tests missing) | N/A |
| `@bilko/country-ba` tax | 0% (tests missing) | N/A |
| `@bilko/country-hr` tax | 0% (tests missing) | N/A |
| API auth routes | N/A | 90% |
| API invoice routes | N/A | 90% |
| API expense routes | N/A | 85% |
| API report routes | N/A | 80% |
| API banking routes | N/A | 75% |
| API settings routes | N/A | 80% |
| Multi-tenancy isolation | N/A | 100% |
| Security tests | N/A | 90% |

**Overall target:** 80% line coverage across the codebase before production launch.

# Testing Guide

# Bilko — Testing Guide

**Status:** NO TESTS EXIST YET — This document defines the testing strategy for implementation.

---

## Testing Philosophy

Financial software has a higher correctness bar than typical web apps. Bilko's testing strategy prioritizes:

1. **Financial Logic Accuracy** — VAT calculations, double-entry bookkeeping, currency conversion
2. **Data Integrity** — No lost transactions, no balance discrepancies
3. **Regression Prevention** — Once fixed, bugs stay fixed
4. **Fast Feedback** — Tests run in <5 minutes locally

---

## Testing Pyramid

```
         /\
        /E2E\        ← 10% (Critical user flows)
       /------\
      /  Integ \     ← 30% (API endpoints, DB queries)
     /----------\
    /    Unit    \   ← 60% (Business logic, utilities)
   /--------------\
```

**Distribution:**
- **60% Unit Tests** — Fast, isolated, test business logic
- **30% Integration Tests** — Test API + database together
- **10% E2E Tests** — Test full user flows (expensive, slow)

```mermaid
graph TD
    subgraph STACK["Bilko Testing Stack"]
        direction TB

        subgraph E2E_LAYER["E2E Layer — 10% — Playwright"]
            PW1["invoice-flow.spec.ts<br/>Create → Send → Paid"]
            PW2["expense-flow.spec.ts<br/>Add → Approve → Pay"]
            PW3["report-flow.spec.ts<br/>P&L → Export PDF"]
            PW4["auth-flow.spec.ts<br/>Register → Login → Logout"]
        end

        subgraph INT_LAYER["Integration Layer — 30% — Supertest"]
            ST1["auth.routes.test.ts<br/>register / login / refresh / logout"]
            ST2["invoices.routes.test.ts<br/>CRUD + status transitions"]
            ST3["expenses.routes.test.ts<br/>CRUD + approve/pay"]
            ST4["reports.routes.test.ts<br/>P&L / trial-balance / VAT"]
            ST5["isolation.test.ts<br/>Cross-org data access prevention"]
        end

        subgraph UNIT_LAYER["Unit Layer — 60% — Vitest"]
            VT1["accounting.test.ts<br/>validateDoubleEntry, createJournalEntry<br/>calculateTrialBalance"]
            VT2["tax.test.ts<br/>calculateVAT: RS 20% / BA 17% / HR 25%<br/>calculateCIT, getVATRates"]
            VT3["multi-currency.test.ts<br/>convertCurrency, lockExchangeRate<br/>calculateForexGainLoss"]
            VT4["bank-import.test.ts<br/>parseCSV, detectDuplicates"]
            VT5["chart-of-accounts.test.ts<br/>Structure validation"]
        end
    end

    E2E_LAYER --> INT_LAYER
    INT_LAYER --> UNIT_LAYER

    style E2E_LAYER fill:#ffc107,stroke:#e0a800
    style INT_LAYER fill:#fd7e14,color:#fff,stroke:#e8690b
    style UNIT_LAYER fill:#198754,color:#fff,stroke:#157347
```

---

## Tech Stack

| Test Type | Framework | Purpose |
|-----------|-----------|---------|
| **Unit** | Vitest | Business logic, utilities, components |
| **Integration** | Supertest | API endpoint testing |
| **E2E** | Playwright | Browser automation, user flows |
| **Coverage** | c8 (built into Vitest) | Code coverage reporting |

### Why These Tools?

#### Vitest (not Jest)
- ✅ Faster (ESM native, Vite-based)
- ✅ Compatible with Vite/Turborepo
- ✅ Watch mode with HMR
- ✅ Same API as Jest (easy migration if needed)

#### Supertest (not Postman)
- ✅ Programmatic API testing
- ✅ Works with Express
- ✅ Can test without starting server

#### Playwright (not Cypress)
- ✅ Multi-browser (Chromium, Firefox, WebKit)
- ✅ Auto-wait (no flaky tests from race conditions)
- ✅ Parallel execution
- ✅ Video recording on failure

---

## Unit Tests (Vitest)

### Scope
Test pure functions and business logic in isolation:
- Invoice calculations (subtotal, tax, discount, total)
- VAT calculations (Serbia 20%, BiH 17%, Croatia 25%)
- Currency conversion (exchange rate locking)
- Double-entry validation (debit = credit)
- Date utilities (fiscal year, due date calculation)
- Number formatting (currency display)

### File Structure
```
apps/api/src/
├── services/
│   ├── invoice.service.ts
│   └── invoice.service.test.ts  ← Unit test
├── utils/
│   ├── vat.ts
│   └── vat.test.ts  ← Unit test
```

### Example: VAT Calculation Test

```typescript
// apps/api/src/utils/vat.test.ts
import { describe, it, expect } from 'vitest';
import { calculateVAT } from './vat';

describe('calculateVAT', () => {
  it('calculates Serbia VAT (20%)', () => {
    const result = calculateVAT(100, 20);
    expect(result).toBe(20);
  });

  it('calculates BiH VAT (17%)', () => {
    const result = calculateVAT(100, 17);
    expect(result).toBe(17);
  });

  it('calculates Croatia VAT (25%)', () => {
    const result = calculateVAT(100, 25);
    expect(result).toBe(25);
  });

  it('handles zero VAT', () => {
    const result = calculateVAT(100, 0);
    expect(result).toBe(0);
  });

  it('handles decimal amounts', () => {
    const result = calculateVAT(123.45, 20);
    expect(result).toBe(24.69);
  });

  it('rounds to 2 decimal places', () => {
    const result = calculateVAT(10.01, 20);
    expect(result).toBe(2.00); // Not 2.002
  });
});
```

### Running Unit Tests

```bash
# Run all unit tests
npm run test:unit

# Watch mode (re-run on file change)
npm run test:unit -- --watch

# Coverage report
npm run test:unit -- --coverage

# Specific file
npm run test:unit -- vat.test.ts
```

### Coverage Requirements

| Category | Target | Rationale |
|----------|--------|-----------|
| **Financial logic** | >95% | Critical for correctness |
| **Utilities** | >90% | Reused across codebase |
| **Services** | >80% | Business logic layer |
| **Controllers** | >60% | Thin layer (tested via integration) |
| **Overall** | >80% | Industry standard |

---

## Integration Tests (Supertest)

### Scope
Test API endpoints with real database:
- Auth flow (register, login, refresh, logout)
- CRUD operations (invoices, expenses, contacts)
- Data validation (Zod schemas)
- Error handling (400, 401, 403, 404, 500)
- Database transactions
- Organization scoping (can't access other org's data)

### File Structure
```
apps/api/src/
├── routes/
│   ├── auth.routes.ts
│   └── auth.routes.test.ts  ← Integration test
├── routes/
│   ├── invoices.routes.ts
│   └── invoices.routes.test.ts  ← Integration test
```

### Test Database Setup

Use separate test database:

```bash
# .env.test
DATABASE_URL=postgresql://bilko_test:bilko_test@localhost:5432/bilko_test
```

Setup/teardown:

```typescript
// apps/api/src/test/setup.ts
import { PrismaClient } from '@prisma/client';
import { beforeAll, afterAll, beforeEach } from 'vitest';

const prisma = new PrismaClient();

beforeAll(async () => {
  // Run migrations on test DB
  await execSync('npx prisma migrate deploy');
});

beforeEach(async () => {
  // Clear all tables before each test
  await prisma.$transaction([
    prisma.invoice.deleteMany(),
    prisma.expense.deleteMany(),
    prisma.contact.deleteMany(),
    prisma.user.deleteMany(),
    prisma.organization.deleteMany(),
  ]);
});

afterAll(async () => {
  await prisma.$disconnect();
});
```

### Example: Invoice API Test

```typescript
// apps/api/src/routes/invoices.routes.test.ts
import { describe, it, expect, beforeEach } from 'vitest';
import request from 'supertest';
import { app } from '../app';
import { prisma } from '../lib/prisma';

describe('POST /api/v1/invoices', () => {
  let authToken: string;
  let organizationId: string;
  let customerId: string;

  beforeEach(async () => {
    // Create test organization
    const org = await prisma.organization.create({
      data: {
        name: 'Test Company',
        baseCurrency: 'RSD',
        country: 'RS',
      },
    });
    organizationId = org.id;

    // Create test user
    const user = await prisma.user.create({
      data: {
        organizationId,
        email: 'test@bilko.io',
        passwordHash: '$2b$12$...', // bcrypt hash
        fullName: 'Test User',
        role: 'admin',
      },
    });

    // Login to get token
    const loginRes = await request(app)
      .post('/api/v1/auth/login')
      .send({ email: 'test@bilko.io', password: 'test123' });
    authToken = loginRes.body.accessToken;

    // Create test customer
    const customer = await prisma.contact.create({
      data: {
        organizationId,
        type: 'customer',
        name: 'Test Customer',
        email: 'customer@example.com',
      },
    });
    customerId = customer.id;
  });

  it('creates invoice with valid data', async () => {
    const res = await request(app)
      .post('/api/v1/invoices')
      .set('Authorization', `Bearer ${authToken}`)
      .send({
        customerId,
        invoiceDate: '2026-02-20',
        dueDate: '2026-03-20',
        currencyCode: 'RSD',
        items: [
          {
            description: 'Web Development',
            quantity: 10,
            unitPrice: 5000,
            taxRate: 20,
          },
        ],
      });

    expect(res.status).toBe(201);
    expect(res.body.invoiceNumber).toMatch(/^INV-\d+$/);
    expect(res.body.subtotal).toBe(50000);
    expect(res.body.taxAmount).toBe(10000);
    expect(res.body.totalAmount).toBe(60000);
  });

  it('rejects invoice without auth', async () => {
    const res = await request(app)
      .post('/api/v1/invoices')
      .send({ customerId, items: [] });

    expect(res.status).toBe(401);
  });

  it('rejects invoice for customer in different org', async () => {
    // Create another org
    const otherOrg = await prisma.organization.create({
      data: { name: 'Other Company', baseCurrency: 'EUR', country: 'RS' },
    });

    // Create customer in other org
    const otherCustomer = await prisma.contact.create({
      data: {
        organizationId: otherOrg.id,
        type: 'customer',
        name: 'Other Customer',
      },
    });

    const res = await request(app)
      .post('/api/v1/invoices')
      .set('Authorization', `Bearer ${authToken}`)
      .send({
        customerId: otherCustomer.id,
        items: [],
      });

    expect(res.status).toBe(403); // Forbidden (can't access other org's data)
  });
});
```

### Running Integration Tests

```bash
# Run all integration tests
npm run test:integration

# Specific file
npm run test:integration -- invoices.routes.test.ts
```

---

## E2E Tests (Playwright)

### Scope
Test critical user flows from browser:
- **Invoice Flow:** Create → Preview → Send → Mark Paid
- **Expense Flow:** Add → Upload Receipt → Approve → Pay
- **Report Flow:** Generate P&L → Export PDF
- **Auth Flow:** Register → Login → 2FA → Logout

### File Structure
```
apps/e2e/
├── tests/
│   ├── invoice-flow.spec.ts
│   ├── expense-flow.spec.ts
│   ├── report-flow.spec.ts
│   └── auth-flow.spec.ts
├── fixtures/
│   └── test-data.ts
└── playwright.config.ts
```

### Configuration

```typescript
// apps/e2e/playwright.config.ts
import { defineConfig } from '@playwright/test';

export default defineConfig({
  testDir: './tests',
  timeout: 60000, // 60s per test
  retries: 1, // Retry flaky tests once
  workers: 4, // Run 4 tests in parallel
  use: {
    baseURL: 'http://localhost:3000',
    screenshot: 'only-on-failure',
    video: 'retain-on-failure',
  },
  projects: [
    { name: 'chromium', use: { browserName: 'chromium' } },
    { name: 'firefox', use: { browserName: 'firefox' } },
    { name: 'webkit', use: { browserName: 'webkit' } },
  ],
});
```

### Example: Invoice E2E Test

```typescript
// apps/e2e/tests/invoice-flow.spec.ts
import { test, expect } from '@playwright/test';

test.describe('Invoice Flow', () => {
  test.beforeEach(async ({ page }) => {
    // Login
    await page.goto('/login');
    await page.fill('input[name="email"]', 'demo@bilko.io');
    await page.fill('input[name="password"]', 'demo123');
    await page.click('button[type="submit"]');
    await expect(page).toHaveURL('/dashboard');
  });

  test('create invoice and mark as paid', async ({ page }) => {
    // Navigate to invoices
    await page.click('a[href="/invoices"]');
    await expect(page).toHaveURL('/invoices');

    // Click "New Invoice"
    await page.click('button:has-text("New Invoice")');
    await expect(page).toHaveURL('/invoices/new');

    // Fill invoice form (6-step wizard)
    // Step 1: Customer
    await page.selectOption('select[name="customerId"]', { label: 'Acme Corp' });
    await page.click('button:has-text("Next")');

    // Step 2: Details
    await page.fill('input[name="invoiceDate"]', '2026-02-20');
    await page.fill('input[name="dueDate"]', '2026-03-20');
    await page.click('button:has-text("Next")');

    // Step 3: Items
    await page.fill('input[name="items.0.description"]', 'Web Development');
    await page.fill('input[name="items.0.quantity"]', '10');
    await page.fill('input[name="items.0.unitPrice"]', '5000');
    await page.selectOption('select[name="items.0.taxRate"]', '20');
    await page.click('button:has-text("Next")');

    // Step 4: Review
    await expect(page.locator('text=Subtotal')).toContainText('50,000.00 RSD');
    await expect(page.locator('text=Tax')).toContainText('10,000.00 RSD');
    await expect(page.locator('text=Total')).toContainText('60,000.00 RSD');
    await page.click('button:has-text("Create Invoice")');

    // Verify redirect to invoice detail
    await expect(page).toHaveURL(/\/invoices\/[a-f0-9-]+$/);
    await expect(page.locator('h1')).toContainText('INV-');

    // Mark as paid
    await page.click('button:has-text("Mark as Paid")');
    await page.click('button:has-text("Confirm")');

    // Verify status changed
    await expect(page.locator('.status-badge')).toContainText('Paid');
  });

  test('validates required fields', async ({ page }) => {
    await page.goto('/invoices/new');

    // Try to submit without customer
    await page.click('button:has-text("Next")');

    // Verify error message
    await expect(page.locator('.error')).toContainText('Customer is required');
  });
});
```

### Running E2E Tests

```bash
# Start dev server first
npm run dev

# In another terminal:
npm run test:e2e

# Headless (CI mode)
npm run test:e2e -- --headed

# Debug mode (pause on failure)
npm run test:e2e -- --debug

# Specific browser
npm run test:e2e -- --project=firefox
```

---

## VAT Test Matrix — Country Coverage

```mermaid
graph TD
    VAT["calculateVAT Tests<br/>@bilko/core/tax"]

    VAT --> RS["Serbia RS<br/>Standard: 20%<br/>Reduced: 10%<br/>Zero: 0% exports<br/>CIT: 15% flat<br/>Pausal: &lt; 6M RSD<br/>VAT reg: &gt;= 8M RSD"]

    VAT --> BA["Bosnia BA<br/>Standard: 17%<br/>No reduced rate<br/>Zero: 0% exports<br/>FBiH CIT: 10%<br/>RS CIT: 10%<br/>WHT FBiH: 5% div<br/>VAT reg: &gt;= 100K BAM"]

    VAT --> HR["Croatia HR<br/>Standard: 25%<br/>Reduced: 13%<br/>Super-red: 5%<br/>CIT small: 10%<br/>CIT large: 18%<br/>VAT reg: &gt;= 60K EUR"]

    RS --> RS_T["Test: calculateSerbianPDV<br/>Test: qualifiesForPausalRegime<br/>Test: requiresVATRegistration RS"]
    BA --> BA_T["Test: calculateBosnianPDV<br/>Test: calculateCITFBiH / CITRS<br/>Test: calculateDividendWHT"]
    HR --> HR_T["Test: calculateCroatianPDV<br/>Test: calculateCroatianCIT<br/>Test: requiresVATRegistration HR"]

    style VAT fill:#0d6efd,color:#fff
    style RS fill:#c0392b,color:#fff
    style BA fill:#2c3e50,color:#fff
    style HR fill:#e74c3c,color:#fff
```

## Test Data Management

### Factories (Recommended)

Create reusable test data generators:

```typescript
// apps/api/src/test/factories/invoice.factory.ts
import { faker } from '@faker-js/faker';
import { prisma } from '../../lib/prisma';

export async function createInvoice(overrides = {}) {
  return prisma.invoice.create({
    data: {
      organizationId: faker.string.uuid(),
      customerId: faker.string.uuid(),
      invoiceNumber: `INV-${faker.number.int({ min: 1000, max: 9999 })}`,
      invoiceDate: faker.date.recent(),
      dueDate: faker.date.future(),
      currencyCode: 'RSD',
      subtotal: 50000,
      taxAmount: 10000,
      totalAmount: 60000,
      baseAmount: 60000,
      status: 'draft',
      ...overrides,
    },
  });
}
```

Usage:

```typescript
const invoice = await createInvoice({ status: 'paid' });
```

---

## Coverage Reporting

### Generate Coverage Report

```bash
npm run test:unit -- --coverage
```

Output:
```
File             | % Stmts | % Branch | % Funcs | % Lines
-----------------|---------|----------|---------|--------
All files        |   82.5  |   75.3   |   80.1  |  82.5
 vat.ts          |   95.0  |   90.0   |  100.0  |  95.0
 invoice.ts      |   88.2  |   80.5   |   85.0  |  88.2
 currency.ts     |   78.0  |   70.0   |   75.0  |  78.0
```

### Coverage Thresholds (CI)

Fail build if coverage drops below threshold:

```json
// vitest.config.ts
export default defineConfig({
  test: {
    coverage: {
      provider: 'c8',
      reporter: ['text', 'json', 'html'],
      statements: 80,
      branches: 75,
      functions: 80,
      lines: 80,
    },
  },
});
```

---

## Testing Best Practices

### 1. Test Behavior, Not Implementation

❌ **Bad:** Test internal state
```typescript
it('sets status to paid', () => {
  invoice.status = 'paid';
  expect(invoice.status).toBe('paid');
});
```

✅ **Good:** Test observable behavior
```typescript
it('marks invoice as paid', async () => {
  await invoiceService.markAsPaid(invoice.id);
  const updated = await prisma.invoice.findUnique({ where: { id: invoice.id } });
  expect(updated.status).toBe('paid');
  expect(updated.paidAt).toBeTruthy();
});
```

---

### 2. Use Descriptive Test Names

❌ **Bad:** Vague test name
```typescript
it('works', () => { /* ... */ });
```

✅ **Good:** Descriptive test name
```typescript
it('calculates Serbian VAT at 20% on €100 as €20', () => { /* ... */ });
```

---

### 3. Arrange-Act-Assert (AAA)

```typescript
it('creates invoice with correct totals', async () => {
  // ARRANGE — Set up test data
  const customer = await createCustomer();
  const invoiceData = { customerId: customer.id, items: [...] };

  // ACT — Perform action
  const invoice = await invoiceService.create(invoiceData);

  // ASSERT — Verify outcome
  expect(invoice.subtotal).toBe(50000);
  expect(invoice.taxAmount).toBe(10000);
  expect(invoice.totalAmount).toBe(60000);
});
```

---

### 4. Test Edge Cases

Always test:
- **Empty input** — `calculateVAT(0, 20)`
- **Null/undefined** — `formatCurrency(null)`
- **Negative numbers** — `calculateDiscount(100, -10)`
- **Large numbers** — `convertCurrency(999999999999.9999, 1.2)`
- **Boundary values** — Tax rate at 0%, 100%

---

### 5. Avoid Test Interdependence

❌ **Bad:** Tests depend on each other
```typescript
let invoiceId;

it('creates invoice', async () => {
  const invoice = await createInvoice();
  invoiceId = invoice.id; // Shared state
});

it('updates invoice', async () => {
  await updateInvoice(invoiceId); // Depends on previous test
});
```

✅ **Good:** Tests are independent
```typescript
it('creates invoice', async () => {
  const invoice = await createInvoice();
  expect(invoice.id).toBeTruthy();
});

it('updates invoice', async () => {
  const invoice = await createInvoice(); // Create fresh data
  await updateInvoice(invoice.id);
});
```

---

## CI/CD Integration

Tests run automatically on every push (GitHub Actions):

```yaml
# .github/workflows/main.yml
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - run: npm run test:unit -- --coverage
      - run: npm run test:integration
      - run: npm run test:e2e
```

```mermaid
flowchart LR
    GIT["git push"] --> GHA["GitHub Actions"]

    GHA --> PARALLEL["Parallel Jobs"]

    PARALLEL --> U["unit-tests<br/>vitest run<br/>@bilko/core<br/>~30s"]
    PARALLEL --> I["integration-tests<br/>supertest<br/>postgres:15<br/>~2min"]
    PARALLEL --> E["e2e-tests<br/>playwright<br/>all browsers<br/>~5min"]

    U --> COV{"Coverage<br/>>80%?"}
    I --> API{"All API<br/>tests pass?"}
    E --> E2E{"All flows<br/>pass?"}

    COV -->|Pass| GATE["Merge Gate"]
    API -->|Pass| GATE
    E2E -->|Pass| GATE

    COV -->|Fail| BLOCK1["Block PR"]
    API -->|Fail| BLOCK1
    E2E -->|Fail| BLOCK1

    GATE --> MAIN["Merge to main"]
    MAIN --> DEPLOY["Deploy to Staging"]

    style GIT fill:#6c757d,color:#fff
    style MAIN fill:#198754,color:#fff
    style BLOCK1 fill:#dc3545,color:#fff
    style DEPLOY fill:#0d6efd,color:#fff
```

See [CI-CD.md](/books/bilko-balkan-accounting-saas/page/cicd-pipeline) for full pipeline.

---

## Debugging Tests

### Unit/Integration Tests (Vitest)

```bash
# Debug mode (pause on debugger statement)
npm run test:unit -- --inspect-brk

# VS Code launch.json:
{
  "type": "node",
  "request": "launch",
  "name": "Debug Vitest",
  "runtimeExecutable": "npm",
  "runtimeArgs": ["run", "test:unit", "--", "--inspect-brk"],
  "console": "integratedTerminal"
}
```

### E2E Tests (Playwright)

```bash
# Debug mode (opens inspector)
npm run test:e2e -- --debug

# Headed mode (see browser)
npm run test:e2e -- --headed

# Trace viewer (after failure)
npx playwright show-trace trace.zip
```

---

## Performance Testing (Future)

### Load Testing (k6)

Test API under load:

```javascript
// apps/e2e/load/invoices.js
import http from 'k6/http';
import { check } from 'k6';

export let options = {
  vus: 100, // 100 virtual users
  duration: '30s',
};

export default function () {
  const res = http.get('http://localhost:4000/api/v1/invoices');
  check(res, { 'status is 200': (r) => r.status === 200 });
}
```

Run:
```bash
k6 run apps/e2e/load/invoices.js
```

**Target:** API handles 1,000 requests/second with <200ms p95 latency.

**Status:** PLANNED (Phase 2)

---

## Related Documents
- CI/CD Pipeline: [../infrastructure/CI-CD.md](/books/bilko-balkan-accounting-saas/page/cicd-pipeline)
- Test Inventory: [TEST-INVENTORY.md](/books/bilko-balkan-accounting-saas/page/test-inventory)
- Security Testing: [../security/SECURITY-ARCHITECTURE.md](/books/bilko-balkan-accounting-saas/page/security-architecture)

---

**Last Updated:** 2026-02-20
**Status:** NO TESTS EXIST YET — Implement tests during backend development
**Coverage Target:** >80% overall, >95% for financial logic

# Test Inventory

# Bilko — Test Inventory

**Status:** NO TESTS EXIST YET — This document tracks planned tests and implementation status.

This inventory catalogs all tests planned for Bilko, organized by category and priority.

---

## Test Coverage Summary

| Category | Total Planned | Implemented | Coverage Target |
|----------|---------------|-------------|-----------------|
| **Unit Tests** | 45 | 0 | >80% |
| **Integration Tests** | 35 | 0 | >80% |
| **E2E Tests** | 12 | 0 | Critical flows |
| **TOTAL** | **92** | **0** | — |

```mermaid
graph TD
    subgraph COVERAGE["Test Coverage Matrix — 92 Tests Planned"]
        subgraph UNIT["Unit Tests — 45"]
            FIN["Financial Calculations<br/>12 tests — P0<br/>VAT, invoice totals, precision"]
            DE["Double-Entry Validation<br/>6 tests — P0<br/>debit=credit enforcement"]
            CUR["Currency Conversion<br/>8 tests — P0<br/>exchange rate locking"]
            DATE["Date Utilities<br/>6 tests — P1<br/>due dates, fiscal year"]
            FMT["Number Formatting<br/>5 tests — P1<br/>RSD, EUR, BAM display"]
            AUTH_U["Authentication Utils<br/>8 tests — P0<br/>bcrypt, JWT, tokens"]
        end

        subgraph INTEG["Integration Tests — 35"]
            AUTH_API["Auth API<br/>10 tests — P0<br/>register/login/refresh/logout"]
            INV_API["Invoices API<br/>10 tests — P0<br/>CRUD + status + org-scope"]
            EXP_API["Expenses API<br/>8 tests — P1<br/>CRUD + approve/pay"]
            REP_API["Reports API<br/>7 tests — P1<br/>P&L / BS / VAT reports"]
        end

        subgraph E2E_["E2E Tests — 12"]
            INV_E2E["Invoice Flow<br/>4 tests — P0<br/>Create → Send → Paid"]
            EXP_E2E["Expense Flow<br/>3 tests — P1<br/>Add → Approve → Pay"]
            REP_E2E["Report Flow<br/>2 tests — P1<br/>Generate → Export PDF"]
            SET_E2E["Settings Flow<br/>2 tests — P2<br/>Org settings + Invite user"]
            AUTH_E2E["Auth Flow<br/>1 test — P1<br/>Register → Login → 2FA → Logout"]
        end
    end

    style FIN fill:#dc3545,color:#fff
    style DE fill:#dc3545,color:#fff
    style CUR fill:#dc3545,color:#fff
    style AUTH_U fill:#dc3545,color:#fff
    style AUTH_API fill:#dc3545,color:#fff
    style INV_API fill:#dc3545,color:#fff
    style INV_E2E fill:#dc3545,color:#fff
    style DATE fill:#fd7e14,color:#fff
    style FMT fill:#fd7e14,color:#fff
    style EXP_API fill:#fd7e14,color:#fff
    style REP_API fill:#fd7e14,color:#fff
    style EXP_E2E fill:#fd7e14,color:#fff
    style REP_E2E fill:#fd7e14,color:#fff
    style AUTH_E2E fill:#fd7e14,color:#fff
    style SET_E2E fill:#6c757d,color:#fff
```

---

## Priority Legend

- **P0** — Critical (MVP blocker, financial logic)
- **P1** — High (core features, security)
- **P2** — Medium (nice-to-have, edge cases)
- **P3** — Low (future enhancements)

---

## Unit Tests (45 total)

### Financial Calculations (12 tests)

| Test File | Test Name | What It Tests | Priority | Status |
|-----------|-----------|---------------|----------|--------|
| `vat.test.ts` | `calculateVAT - Serbia 20%` | VAT calculation for Serbia | P0 | ❌ Not implemented |
| `vat.test.ts` | `calculateVAT - BiH 17%` | VAT calculation for BiH | P0 | ❌ Not implemented |
| `vat.test.ts` | `calculateVAT - Croatia 25%` | VAT calculation for Croatia | P0 | ❌ Not implemented |
| `vat.test.ts` | `calculateVAT - zero rate` | VAT at 0% (exports) | P0 | ❌ Not implemented |
| `vat.test.ts` | `calculateVAT - decimal amounts` | VAT on €123.45 | P0 | ❌ Not implemented |
| `vat.test.ts` | `calculateVAT - rounding` | Rounds to 2 decimal places | P0 | ❌ Not implemented |
| `invoice-calc.test.ts` | `calculateInvoiceTotal - subtotal` | Sum of line items | P0 | ❌ Not implemented |
| `invoice-calc.test.ts` | `calculateInvoiceTotal - tax` | Sum of tax amounts | P0 | ❌ Not implemented |
| `invoice-calc.test.ts` | `calculateInvoiceTotal - discount` | Subtract discount from subtotal | P0 | ❌ Not implemented |
| `invoice-calc.test.ts` | `calculateInvoiceTotal - total` | Subtotal + tax - discount | P0 | ❌ Not implemented |
| `invoice-calc.test.ts` | `calculateInvoiceTotal - multi-item` | Multiple line items with different tax rates | P0 | ❌ Not implemented |
| `invoice-calc.test.ts` | `calculateInvoiceTotal - precision` | NUMERIC(19,4) precision maintained | P0 | ❌ Not implemented |

---

### Double-Entry Validation (6 tests)

| Test File | Test Name | What It Tests | Priority | Status |
|-----------|-----------|---------------|----------|--------|
| `double-entry.test.ts` | `validateTransaction - debit equals credit` | Debit amount = Credit amount | P0 | ❌ Not implemented |
| `double-entry.test.ts` | `validateTransaction - rejects unbalanced` | Throws error if debit ≠ credit | P0 | ❌ Not implemented |
| `double-entry.test.ts` | `validateTransaction - requires both accounts` | Throws if missing debit or credit account | P0 | ❌ Not implemented |
| `double-entry.test.ts` | `validateTransaction - multi-currency` | Validates amounts in base currency | P0 | ❌ Not implemented |
| `double-entry.test.ts` | `validateTransaction - precision` | NUMERIC precision preserved | P0 | ❌ Not implemented |
| `double-entry.test.ts` | `validateTransaction - zero amount` | Rejects zero-amount transactions | P1 | ❌ Not implemented |

---

### Currency Conversion (8 tests)

| Test File | Test Name | What It Tests | Priority | Status |
|-----------|-----------|---------------|----------|--------|
| `currency.test.ts` | `convertCurrency - EUR to RSD` | Convert at locked exchange rate | P0 | ❌ Not implemented |
| `currency.test.ts` | `convertCurrency - RSD to EUR` | Reverse conversion | P0 | ❌ Not implemented |
| `currency.test.ts` | `convertCurrency - same currency` | Rate = 1.0 when currency matches | P0 | ❌ Not implemented |
| `currency.test.ts` | `convertCurrency - precision` | NUMERIC(19,4) preserved | P0 | ❌ Not implemented |
| `currency.test.ts` | `convertCurrency - large amounts` | €999,999,999.9999 | P0 | ❌ Not implemented |
| `currency.test.ts` | `convertCurrency - rounding` | Rounds to 4 decimal places | P1 | ❌ Not implemented |
| `currency.test.ts` | `lockExchangeRate - historical rate` | Uses rate from transaction date, not today | P0 | ❌ Not implemented |
| `currency.test.ts` | `lockExchangeRate - missing rate` | Throws if no rate available for date | P1 | ❌ Not implemented |

---

### Date Utilities (6 tests)

| Test File | Test Name | What It Tests | Priority | Status |
|-----------|-----------|---------------|----------|--------|
| `date.test.ts` | `calculateDueDate - 30 days` | Invoice date + 30 days | P1 | ❌ Not implemented |
| `date.test.ts` | `calculateDueDate - custom terms` | Invoice date + custom days | P1 | ❌ Not implemented |
| `date.test.ts` | `isOverdue - past due date` | Returns true if today > due date | P1 | ❌ Not implemented |
| `date.test.ts` | `isOverdue - not overdue` | Returns false if today <= due date | P1 | ❌ Not implemented |
| `date.test.ts` | `getFiscalYear - starts Jan 1` | Fiscal year 2026 = Jan 1 - Dec 31 | P2 | ❌ Not implemented |
| `date.test.ts` | `getFiscalYear - custom start` | Fiscal year starts on custom date | P2 | ❌ Not implemented |

---

### Number Formatting (5 tests)

| Test File | Test Name | What It Tests | Priority | Status |
|-----------|-----------|---------------|----------|--------|
| `format.test.ts` | `formatCurrency - RSD` | "50,000.00 RSD" format | P1 | ❌ Not implemented |
| `format.test.ts` | `formatCurrency - EUR` | "€50,000.00" format | P1 | ❌ Not implemented |
| `format.test.ts` | `formatCurrency - BAM` | "50,000.00 BAM" format | P1 | ❌ Not implemented |
| `format.test.ts` | `formatCurrency - decimal places` | Respects currency decimal places (0-4) | P1 | ❌ Not implemented |
| `format.test.ts` | `formatCurrency - null` | Returns "-" for null/undefined | P2 | ❌ Not implemented |

---

### Authentication (8 tests)

| Test File | Test Name | What It Tests | Priority | Status |
|-----------|-----------|---------------|----------|--------|
| `auth.test.ts` | `hashPassword - bcrypt 12 rounds` | Password hashed with bcrypt | P0 | ❌ Not implemented |
| `auth.test.ts` | `hashPassword - unique salt` | Each hash is different | P0 | ❌ Not implemented |
| `auth.test.ts` | `verifyPassword - correct password` | Returns true for correct password | P0 | ❌ Not implemented |
| `auth.test.ts` | `verifyPassword - incorrect password` | Returns false for wrong password | P0 | ❌ Not implemented |
| `auth.test.ts` | `generateJWT - valid payload` | JWT contains user ID, org ID, role | P0 | ❌ Not implemented |
| `auth.test.ts` | `generateJWT - expiry 15 min` | Access token expires in 15 min | P0 | ❌ Not implemented |
| `auth.test.ts` | `generateRefreshToken - expiry 7 days` | Refresh token expires in 7 days | P0 | ❌ Not implemented |
| `auth.test.ts` | `verifyJWT - expired token` | Throws error if token expired | P1 | ❌ Not implemented |

---

## Integration Tests (35 total)

### Auth API (10 tests)

| Test File | Test Name | What It Tests | Priority | Status |
|-----------|-----------|---------------|----------|--------|
| `auth-api.test.ts` | `POST /auth/register - success` | Creates user, returns 201 | P0 | ❌ Not implemented |
| `auth-api.test.ts` | `POST /auth/register - duplicate email` | Returns 400 if email exists | P0 | ❌ Not implemented |
| `auth-api.test.ts` | `POST /auth/register - weak password` | Returns 400 if password < 8 chars | P0 | ❌ Not implemented |
| `auth-api.test.ts` | `POST /auth/login - success` | Returns access + refresh tokens | P0 | ❌ Not implemented |
| `auth-api.test.ts` | `POST /auth/login - wrong password` | Returns 401 | P0 | ❌ Not implemented |
| `auth-api.test.ts` | `POST /auth/login - non-existent user` | Returns 401 | P0 | ❌ Not implemented |
| `auth-api.test.ts` | `POST /auth/refresh - success` | Returns new access token | P0 | ❌ Not implemented |
| `auth-api.test.ts` | `POST /auth/refresh - expired token` | Returns 401 | P1 | ❌ Not implemented |
| `auth-api.test.ts` | `POST /auth/logout - success` | Deletes refresh token from DB | P1 | ❌ Not implemented |
| `auth-api.test.ts` | `POST /auth/logout - already logged out` | Returns 204 (idempotent) | P2 | ❌ Not implemented |

---

### Invoices API (10 tests)

| Test File | Test Name | What It Tests | Priority | Status |
|-----------|-----------|---------------|----------|--------|
| `invoices-api.test.ts` | `POST /invoices - success` | Creates invoice, returns 201 | P0 | ❌ Not implemented |
| `invoices-api.test.ts` | `POST /invoices - validates required fields` | Returns 400 if missing customer | P0 | ❌ Not implemented |
| `invoices-api.test.ts` | `POST /invoices - validates currency` | Returns 400 if invalid currency code | P0 | ❌ Not implemented |
| `invoices-api.test.ts` | `POST /invoices - org scoping` | Returns 403 if customer in different org | P0 | ❌ Not implemented |
| `invoices-api.test.ts` | `GET /invoices - list` | Returns paginated invoices | P0 | ❌ Not implemented |
| `invoices-api.test.ts` | `GET /invoices - filter by status` | Returns only "paid" invoices | P1 | ❌ Not implemented |
| `invoices-api.test.ts` | `GET /invoices/:id - success` | Returns invoice by ID | P0 | ❌ Not implemented |
| `invoices-api.test.ts` | `GET /invoices/:id - not found` | Returns 404 if ID doesn't exist | P1 | ❌ Not implemented |
| `invoices-api.test.ts` | `PATCH /invoices/:id - update status` | Changes status to "sent" | P0 | ❌ Not implemented |
| `invoices-api.test.ts` | `DELETE /invoices/:id - soft delete` | Marks as deleted (not hard delete) | P1 | ❌ Not implemented |

---

### Expenses API (8 tests)

| Test File | Test Name | What It Tests | Priority | Status |
|-----------|-----------|---------------|----------|--------|
| `expenses-api.test.ts` | `POST /expenses - success` | Creates expense, returns 201 | P0 | ❌ Not implemented |
| `expenses-api.test.ts` | `POST /expenses - validates required fields` | Returns 400 if missing amount | P0 | ❌ Not implemented |
| `expenses-api.test.ts` | `POST /expenses - org scoping` | Returns 403 if vendor in different org | P0 | ❌ Not implemented |
| `expenses-api.test.ts` | `GET /expenses - list` | Returns paginated expenses | P0 | ❌ Not implemented |
| `expenses-api.test.ts` | `PATCH /expenses/:id/approve - success` | Changes status to "approved" | P1 | ❌ Not implemented |
| `expenses-api.test.ts` | `PATCH /expenses/:id/approve - requires admin` | Returns 403 if user is viewer | P1 | ❌ Not implemented |
| `expenses-api.test.ts` | `PATCH /expenses/:id/reject - success` | Changes status to "rejected" | P1 | ❌ Not implemented |
| `expenses-api.test.ts` | `DELETE /expenses/:id - soft delete` | Marks as deleted | P1 | ❌ Not implemented |

---

### Reports API (7 tests)

| Test File | Test Name | What It Tests | Priority | Status |
|-----------|-----------|---------------|----------|--------|
| `reports-api.test.ts` | `GET /reports/profit-loss - success` | Returns P&L with revenue, expenses, net | P1 | ❌ Not implemented |
| `reports-api.test.ts` | `GET /reports/profit-loss - date range` | Filters by start/end date | P1 | ❌ Not implemented |
| `reports-api.test.ts` | `GET /reports/balance-sheet - success` | Returns assets, liabilities, equity | P1 | ❌ Not implemented |
| `reports-api.test.ts` | `GET /reports/cash-flow - success` | Returns operating, investing, financing | P1 | ❌ Not implemented |
| `reports-api.test.ts` | `GET /reports/vat - success` | Returns sales VAT, purchase VAT, net VAT | P1 | ❌ Not implemented |
| `reports-api.test.ts` | `GET /reports/vat - Serbia 20%` | Calculates Serbian VAT correctly | P1 | ❌ Not implemented |
| `reports-api.test.ts` | `GET /reports/vat - export PDF` | Returns PDF file | P2 | ❌ Not implemented |

---

## E2E Tests (12 total)

### Invoice Flow (4 tests)

| Test File | Test Name | What It Tests | Priority | Status |
|-----------|-----------|---------------|----------|--------|
| `invoice-flow.spec.ts` | `Create invoice via 6-step wizard` | Full invoice creation flow | P0 | ❌ Not implemented |
| `invoice-flow.spec.ts` | `Preview invoice before sending` | Preview modal shows correct totals | P0 | ❌ Not implemented |
| `invoice-flow.spec.ts` | `Send invoice to customer` | Email sent, status changed to "sent" | P0 | ❌ Not implemented |
| `invoice-flow.spec.ts` | `Mark invoice as paid` | Status changed to "paid", paidAt timestamp | P0 | ❌ Not implemented |

---

### Expense Flow (3 tests)

| Test File | Test Name | What It Tests | Priority | Status |
|-----------|-----------|---------------|----------|--------|
| `expense-flow.spec.ts` | `Add expense with receipt upload` | Create expense, upload JPG | P1 | ❌ Not implemented |
| `expense-flow.spec.ts` | `Approve expense` | Admin approves, status changed | P1 | ❌ Not implemented |
| `expense-flow.spec.ts` | `Mark expense as paid` | Status changed to "paid" | P1 | ❌ Not implemented |

---

### Report Flow (2 tests)

| Test File | Test Name | What It Tests | Priority | Status |
|-----------|-----------|---------------|----------|--------|
| `report-flow.spec.ts` | `Generate P&L report` | Select date range, view report | P1 | ❌ Not implemented |
| `report-flow.spec.ts` | `Export P&L to PDF` | Download PDF file | P1 | ❌ Not implemented |

---

### Settings Flow (2 tests)

| Test File | Test Name | What It Tests | Priority | Status |
|-----------|-----------|---------------|----------|--------|
| `settings-flow.spec.ts` | `Update organization settings` | Change org name, tax settings | P2 | ❌ Not implemented |
| `settings-flow.spec.ts` | `Invite user to organization` | Send invite email, user accepts | P2 | ❌ Not implemented |

---

### Auth Flow (1 test)

| Test File | Test Name | What It Tests | Priority | Status |
|-----------|-----------|---------------|----------|--------|
| `auth-flow.spec.ts` | `Register → Login → 2FA → Logout` | Full auth flow with 2FA | P1 | ❌ Not implemented |

---

## Critical Path Testing Flow

```mermaid
flowchart TD
    START(["Start: No Tests"]) --> P1

    subgraph P1["Phase 1 — MVP Critical (25 tests)"]
        P1_FIN["Financial Calculations<br/>12 unit tests<br/>VAT RS/BA/HR, invoice totals"]
        P1_DE["Double-Entry Validation<br/>6 unit tests<br/>debit=credit, balanced=true"]
        P1_AUTH["Auth API<br/>7 integration tests<br/>register, login, refresh"]
        P1_FIN --> P1_DE --> P1_AUTH
    end

    P1 --> P1_GATE{"Coverage<br/>>=50%?"}
    P1_GATE -->|Yes| P2
    P1_GATE -->|No| P1

    subgraph P2["Phase 2 — Core Features (35 tests)"]
        P2_CUR["Currency Conversion<br/>8 unit tests"]
        P2_INV["Invoices API<br/>10 integration tests"]
        P2_E2E["Invoice + Auth + Expense E2E<br/>8 E2E tests"]
        P2_REP["Reports API<br/>7 integration tests"]
        P2_CUR --> P2_INV --> P2_E2E --> P2_REP
    end

    P2 --> P2_GATE{"Coverage<br/>>=70%?"}
    P2_GATE -->|Yes| P3
    P2_GATE -->|No| P2

    subgraph P3["Phase 3 — Polish (32 tests)"]
        P3_DATE["Date + Formatting<br/>11 unit tests"]
        P3_EXP["Expenses API<br/>8 integration tests"]
        P3_EDGE["Edge cases<br/>8 tests"]
        P3_SET["Settings flow<br/>5 tests"]
        P3_DATE --> P3_EXP --> P3_EDGE --> P3_SET
    end

    P3 --> DONE(["Production Ready<br/>>80% coverage<br/>All critical paths tested"])

    style START fill:#6c757d,color:#fff
    style DONE fill:#198754,color:#fff
    style P1_GATE fill:#ffc107,stroke:#e0a800
    style P2_GATE fill:#ffc107,stroke:#e0a800
```

## Test Implementation Roadmap

### Phase 1 (MVP Critical) — 25 tests
- ✅ **Financial calculations** (12 unit tests)
- ✅ **Double-entry validation** (6 unit tests)
- ✅ **Auth API** (7 integration tests: register, login, refresh)

**Target:** Before backend MVP launch

---

### Phase 2 (Core Features) — 35 tests
- ✅ **Currency conversion** (8 unit tests)
- ✅ **Invoices API** (10 integration tests)
- ✅ **Invoice E2E flow** (4 E2E tests)
- ✅ **Auth E2E flow** (1 E2E test)
- ✅ **Expense flow** (3 E2E tests)
- ✅ **Reports API** (7 integration tests)
- ✅ **Report E2E flow** (2 E2E tests)

**Target:** 1 month after MVP launch

---

### Phase 3 (Polish) — 32 tests
- ✅ **Date utilities** (6 unit tests)
- ✅ **Number formatting** (5 unit tests)
- ✅ **Expenses API** (8 integration tests)
- ✅ **Settings flow** (2 E2E tests)
- ✅ **Remaining auth tests** (3 integration tests)
- ✅ **Edge cases** (8 tests across categories)

**Target:** 3 months after MVP launch

---

## Test Execution Commands

### Run All Tests
```bash
npm run test        # All tests (unit + integration + E2E)
```

### Run by Category
```bash
npm run test:unit           # Unit tests only
npm run test:integration    # Integration tests only
npm run test:e2e            # E2E tests only
```

### Run Specific Test File
```bash
npm run test:unit -- vat.test.ts
npm run test:integration -- invoices-api.test.ts
npm run test:e2e -- invoice-flow.spec.ts
```

### Run with Coverage
```bash
npm run test:unit -- --coverage
```

### Watch Mode
```bash
npm run test:unit -- --watch
```

---

## Coverage Tracking

### Current Coverage (as of 2026-02-20)

| Category | Coverage | Target | Status |
|----------|----------|--------|--------|
| **Financial Logic** | 0% | >95% | ❌ Not started |
| **API Endpoints** | 0% | >80% | ❌ Not started |
| **Utilities** | 0% | >90% | ❌ Not started |
| **Overall** | 0% | >80% | ❌ Not started |

**Next Milestone:** 50% coverage (25 critical tests)

---

## Related Documents
- Testing Guide: [TESTING-GUIDE.md](/books/bilko-balkan-accounting-saas/page/testing-guide)
- CI/CD Pipeline: [../infrastructure/CI-CD.md](/books/bilko-balkan-accounting-saas/page/cicd-pipeline)
- Security Testing: [../security/SECURITY-ARCHITECTURE.md](/books/bilko-balkan-accounting-saas/page/security-architecture)

---

**Last Updated:** 2026-02-20
**Status:** NO TESTS IMPLEMENTED YET
**Total Tests Planned:** 92 (45 unit + 35 integration + 12 E2E)
**Next Action:** Implement Phase 1 financial calculation tests (12 tests)