# Coding Standards: Drop — Fintech Payment App

# Coding Standards: Drop — Fintech Payment App

> **Project:** Drop — Remittance + QR Payments
> **Version:** 1.0
> **Date:** 2026-02-23
> **Author:** John (AI Director)
> **Status:** Approved
> **Reviewers:** Alem Bašić (CEO)

## Document History
| Version | Date | Author | Changes |
|---------|------|--------|---------|
| 0.1     | 2026-02-23 | John | Initial standards — TypeScript strict, Next.js App Router, fintech security rules |

---

## Purpose

These standards apply to all code in **Drop**. When automated tools enforce a rule, the tool wins. When in doubt, optimize for readability — the next AI agent reading your code is trying to understand intent, not guess it.

**Non-negotiable:** All new code must pass TypeScript strict check, ESLint, and Prettier formatting before merge. No exceptions.

**Fintech non-negotiable:** Parameterized SQL everywhere. No stored balances. No stored card numbers or CVV. These are encoded as automated tests — failing these means P0.

---

## 1. Language-Specific Conventions

### 1.1 Naming Conventions

**TypeScript (Drop's primary language):**

| Type | Convention | Example |
|------|------------|---------|
| Variables | `camelCase` | `userId`, `kycStatus` |
| Functions | `camelCase` | `hashPassword()`, `calculateFee()` |
| Classes | `PascalCase` | `TransactionService` |
| Interfaces | `PascalCase` (no `I` prefix) | `User`, `TransactionResult` |
| Types | `PascalCase` | `KycStatus`, `CurrencyCode` |
| Enums | `PascalCase` | `TransactionType`, `KycStatus` |
| Constants | `UPPER_SNAKE_CASE` | `REMITTANCE_FEE_RATE`, `MIN_AMOUNT_NOK` |
| Files — components | `PascalCase.tsx` | `SendMoneyForm.tsx` |
| Files — utilities | `camelCase.ts` | `calculateFee.ts`, `verifyJWT.ts` |
| Files — API routes | `route.ts` (Next.js convention) | `app/api/auth/login/route.ts` |
| Test files | `{name}.test.ts` | `auth.test.ts`, `db.test.ts` |
| E2E test files | `{name}.spec.ts` | `user-flows.spec.ts` |

**Drop-specific constants:**

```typescript
// Fee rates (business rules — change requires CEO approval + ADR)
const REMITTANCE_FEE_RATE = 0.005;  // 0.5%
const QR_MERCHANT_FEE_RATE = 0.01;  // 1%
const MIN_REMITTANCE_NOK = 100;
const MAX_REMITTANCE_NOK = 50_000;

// Supported corridors
const SUPPORTED_CURRENCIES = ['RSD', 'BAM', 'PKR', 'TRY', 'PLN', 'EUR'] as const;
type CurrencyCode = typeof SUPPORTED_CURRENCIES[number];
```

### 1.2 File Organization

```
src/drop-app/src/
├── app/
│   ├── api/                  # Next.js API Routes
│   │   ├── auth/
│   │   │   ├── login/route.ts
│   │   │   ├── register/route.ts
│   │   │   └── me/route.ts
│   │   ├── transactions/
│   │   │   ├── remittance/route.ts
│   │   │   └── qr-payment/route.ts
│   │   └── rates/
│   │       ├── route.ts           # GET /api/rates
│   │       └── [currency]/route.ts # GET /api/rates/RSD
│   └── (app)/               # Protected pages (dashboard, send-money, etc.)
├── lib/
│   ├── auth.ts              # JWT, bcrypt, session helpers
│   ├── db.ts                # SQLite/PostgreSQL client
│   ├── validate.ts          # Zod schemas for input validation
│   └── fee.ts               # Fee calculation functions
└── components/
    ├── drop-logo.tsx        # Brand logo component
    └── ui/                  # Shared UI components
```

**Rules:**
- One route handler per file (`route.ts` in Next.js App Router)
- Business logic extracted to `lib/` — never inline in route handlers
- Test files co-located with source files in `__tests__/`

### 1.3 Import Ordering

```typescript
// 1. Node built-ins
import { cookies } from 'next/headers';

// 2. External dependencies (node_modules)
import { z } from 'zod';
import * as bcrypt from 'bcrypt';
import { SignJWT, jwtVerify } from 'jose';

// 3. Internal — absolute paths (@/ alias)
import { db } from '@/lib/db';
import { verifyJWT } from '@/lib/auth';
import type { User } from '@/lib/types';

// 4. Internal — relative paths
import { validateRemittanceBody } from './validate';
```

**Enforced by:** ESLint `import/order` rule

### 1.4 Error Handling Patterns

```typescript
// PREFERRED — consistent HTTP error responses in Next.js API routes
export async function POST(request: Request): Promise<Response> {
  let body: unknown;
  try {
    body = await request.json();
  } catch {
    return Response.json({ error: 'Invalid JSON body' }, { status: 400 });
  }

  const validation = remittanceSchema.safeParse(body);
  if (!validation.success) {
    return Response.json(
      { error: 'Validation failed', details: validation.error.flatten() },
      { status: 422 }
    );
  }

  // Business logic in lib/ — throws typed errors
  try {
    const result = await processRemittance(validation.data);
    return Response.json(result, { status: 201 });
  } catch (error) {
    if (error instanceof InsufficientBalanceError) {
      return Response.json({ error: 'Insufficient balance' }, { status: 402 });
    }
    if (error instanceof KycRequiredError) {
      return Response.json({ error: 'KYC verification required' }, { status: 403 });
    }
    // Never expose internal errors
    console.error('Unhandled error in remittance:', error);
    return Response.json({ error: 'Internal server error' }, { status: 500 });
  }
}
```

**Rules:**
- NEVER return stack traces or DB error messages to the client
- NEVER use `any` type — TypeScript strict mode enforces this
- NEVER swallow errors silently
- Use 402 for Insufficient Balance (fintech convention), not 400
- Error messages in Norwegian for user-facing validation

---

## 2. Code Formatting

### 2.1 Formatter

| Language | Tool | Config File | Enforced In |
|----------|------|-------------|-------------|
| TypeScript / JavaScript | Prettier | `.prettierrc` | CI + pre-commit hook |
| JSON / YAML | Prettier | `.prettierrc` | CI + pre-commit hook |

**Key formatting rules:**
- Indentation: 2 spaces
- Max line length: 100 characters
- Semicolons: required
- Trailing commas: `all` (ES2020)
- Quotes: single quotes

**Auto-format on save:** Enabled via `.vscode/settings.json` (see [local-development-setup.md](./local-development-setup.md))

### 2.2 Linter

| Language | Tool | Config | Rules Severity |
|----------|------|--------|----------------|
| TypeScript | ESLint + TypeScript-ESLint | `.eslintrc.json` | Error = blocks CI |

**Linting rules that are errors (block CI):**
- `@typescript-eslint/no-explicit-any` — no `any` types
- `no-console` (except `console.error` in server code)
- `@typescript-eslint/no-unused-vars`
- SQL string concatenation (custom Drop rule)

**Disable linting inline (sparingly):**
```typescript
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- External library returns untyped response
const rawResponse: any = await externalLibrary.call();
```
Every inline disable must have a comment explaining why.

---

## 3. Git Conventions

### 3.1 Commit Message Format

**Standard:** [Conventional Commits](https://www.conventionalcommits.org/)

**Format:**
```
<type>(<scope>): <description>

[optional body]

[optional footer(s)]
```

**Types:**

| Type | When to Use |
|------|-------------|
| `feat` | New feature |
| `fix` | Bug fix |
| `docs` | Documentation only |
| `style` | Formatting changes (no logic change) |
| `refactor` | Code change that neither fixes a bug nor adds a feature |
| `test` | Adding or updating tests |
| `chore` | Build system, dependency updates, CI changes |
| `perf` | Performance improvements |
| `security` | Security fix or hardening |

**Drop examples:**
```
feat(auth): add bcrypt rounds configuration via env var
fix(transactions): prevent double-spend with per-user lock
test(db): add assertion that users table has no balance column
security(middleware): add CSRF protection to all mutating endpoints
chore: upgrade Next.js to 15.x patch
```

**Breaking changes:**
```
feat(api)!: rename /api/transactions/send to /api/transactions/remittance

BREAKING CHANGE: All clients must update API calls to new path
```

### 3.2 Branch Naming Convention

| Type | Pattern | Example |
|------|---------|---------|
| Feature | `feature/MC-{task-id}-description` | `feature/MC-042-rate-limiting` |
| Bug fix | `fix/MC-{task-id}-description` | `fix/MC-051-double-spend` |
| Hotfix | `hotfix/MC-{task-id}-description` | `hotfix/MC-060-jwt-bypass` |
| Security | `security/MC-{task-id}-description` | `security/MC-070-csrf-fix` |
| Chore | `chore/MC-{task-id}-description` | `chore/MC-080-upgrade-deps` |

**Rules:**
- Lowercase only; hyphens, not underscores
- Always include Mission Control task ID (`MC-{id}`)

### 3.3 PR Title & Description Format

**Title format:** Same as commit message: `type(scope): description`

**Required PR description sections:**
1. **What**: What does this PR do? (bullet points)
2. **Why**: Which Mission Control task / acceptance criterion?
3. **Pass-through model check**: Does this change touch DB schema? If yes, confirm no balance/CVV columns added
4. **Tests**: What tests were added/modified?
5. **Security**: Does this touch auth, payments, or input validation?

### 3.4 PR Size Guidelines

| Size | Lines Changed | Status |
|------|--------------|--------|
| Small | < 200 | Ideal — review same session |
| Medium | 200-400 | Acceptable |
| Large | 400-800 | Needs justification — split if possible |
| Extra Large | > 800 | Exceptional only — must be pre-approved by John |

---

## 4. Code Review Guidelines

### 4.1 What to Look For (Validator Agent Checklist)

**Reviewers must check:**
- [ ] Pass-through model invariant: no `balance` column; no `card_number` or `cvv`
- [ ] Parameterized SQL — no string interpolation in any DB query
- [ ] bcrypt used for passwords — SHA-256 explicitly forbidden
- [ ] JWT secret not hardcoded; not in response body
- [ ] Rate limiting applies to new auth/transaction endpoints
- [ ] Input validation (Zod schema) on all new request bodies
- [ ] Error messages don't leak internal state
- [ ] Fee calculations correct (0.5% remittance; 1% QR)
- [ ] Tests cover the change and edge cases

**Reviewers should NOT block on:**
- Personal style preferences covered by Prettier/ESLint (the linter decides)
- Minor refactors not related to the PR's scope

### 4.2 Review Turnaround

| PR Type | First review |
|---------|-------------|
| Security / Critical fix | Same session |
| Standard feature | Within 1 session |

### 4.3 Approval Requirements

| Branch | Required Approvals |
|--------|-------------------|
| `main` | Validator agent approval + John (AI Director) sign-off |
| Feature branches | Validator agent approval |
| Hotfix | John (AI Director) or Alem Bašić (CEO) approval — async OK |

### 4.4 Constructive Review Feedback

- `nit:` — Minor issue, not a blocker
- `question:` — Need clarification
- `suggestion:` — Improvement idea (not required)
- `blocker:` — Must be fixed before merge (e.g., SQL injection risk, bcrypt bypass)

---

## 5. Testing Standards

### 5.1 Test Naming Conventions

```typescript
// Vitest format: describe → it('should [behavior] when [condition]')
describe('hashPassword', () => {
  it('should return a bcrypt hash starting with $2', async () => { ... });
  it('should reject SHA-256 hashes at verify time', async () => { ... });
  it('should reject passwords longer than 1000 characters', async () => { ... });
});

describe('calculateRemittanceFee', () => {
  it('should return 5 NOK fee for 1000 NOK amount', () => { ... });
  it('should reject amounts below 100 NOK', () => { ... });
});
```

### 5.2 Test Organization

```
src/drop-app/__tests__/
├── auth.test.ts              # Unit: bcrypt, JWT, password validation
├── validation.test.ts        # Unit: input validation, XSS, injection
├── transactions.test.ts      # Unit: fee calculation, transaction logic
├── rates.test.ts             # Unit: exchange rates
├── api-endpoints.test.ts     # Integration: all 26 API routes
├── db.test.ts                # Integration: schema compliance, FK constraints
├── middleware.test.ts        # Integration: rate limiting, auth, CSRF
├── api-benchmarks.test.ts    # Performance: bcrypt timing, DB queries
├── feature-flags.test.ts     # Unit: feature flag behavior
├── regression-suite.test.ts  # Regression: critical path smoke
├── sumsub-integration.test.ts # Integration: KYC webhook mock
├── cards-integration.test.ts  # Integration: cards (feature-flagged)
└── e2e/
    ├── user-flows.spec.ts    # E2E: registration, login
    ├── full-flows.spec.ts    # E2E: remittance, QR payment
    └── input-chaos.spec.ts   # E2E: 20+ validation edge cases
```

### 5.3 Mocking Guidelines

```typescript
// PREFERRED: Mock BaaS at the module level (NEXT_PUBLIC_SERVICE_MODE=mock)
// The mock mode is configured via env var — no vi.mock() needed for BaaS

// PREFERRED for unit tests: vi.mock for db queries
vi.mock('@/lib/db', () => ({
  db: {
    query: vi.fn(),
    run: vi.fn(),
  }
}));

// For integration tests: use real SQLite in-memory DB
// (configured in vitest.config.ts — no mock needed)

// NEVER mock the pass-through model assertions in db.test.ts
// NEVER mock the bcrypt rejection tests in auth.test.ts
```

### 5.4 Coverage Requirements

| Layer | Lines | Branches | Notes |
|-------|-------|----------|-------|
| Auth module | 100% | 100% | Fintech security — strictly enforced |
| Transaction logic | 100% | 100% | Fee calculation — strictly enforced |
| API handlers | ≥ 80% | ≥ 70% | |
| Input validation | ≥ 90% | ≥ 85% | Security-critical |
| DB layer | ≥ 90% | — | Compliance assertions required |
| Overall minimum | ≥ 80% | — | CI gate |

---

## 6. Documentation Standards

### 6.1 When to Add Comments

```typescript
// GOOD — explains WHY (not obvious from code)
// Bcrypt rounds set to 12 (not 10) per NFR-SEC02 fintech standard.
// Using 10 rounds would be faster but falls below security threshold.
const BCRYPT_ROUNDS = parseInt(process.env.BCRYPT_ROUNDS ?? '12', 10);

// GOOD — documents non-obvious business rule
// Fee is calculated on gross amount, not total debit (gross + fee)
// This is required by Drop's pass-through model (ADR-003)
const fee = amount * REMITTANCE_FEE_RATE;

// BAD — restates what code says
// Multiply amount by fee rate
const fee = amount * REMITTANCE_FEE_RATE;
```

**Comment when:**
- A fintech business rule is implemented (always cite the rule)
- A security decision was made (e.g., why bcrypt rounds = 12)
- A workaround exists for an external system quirk

### 6.2 ADR Format

Write an Architecture Decision Record when:
- Changing the database (SQLite → PostgreSQL)
- Changing the authentication mechanism (DOB → BankID)
- Modifying the fee model (new corridor, new fee rate)
- Adding a new BaaS partner

**ADR location:** `comms/decisions/YYYY-MM-DD-decision-title.md`

### 6.3 API Documentation

When adding a new API endpoint, update `docs/backend/API-REFERENCE.md`:
- Method + path
- Authentication required (Yes/No)
- Request body schema (Zod type → JSON example)
- Response schema (success + all error codes)

---

## 7. Security Coding Practices — Drop Fintech Standards

| Practice | Rule |
|----------|------|
| SQL queries | NEVER string interpolation. Always parameterized queries (better-sqlite3 `?` placeholders) |
| Password hashing | bcrypt ONLY with 12 rounds in production. SHA-256 hashes are REJECTED at login |
| JWT | `jose` library only. Secret via `JWT_SECRET` env var. Fail fast if missing |
| Input validation | Zod schemas on ALL API route request bodies. Server-side only — never trust client |
| Pass-through model | NEVER add `balance` column to users. NEVER add `card_number` or `cvv` to cards |
| Rate limiting | DB-backed rate limiter (not in-memory). Auth: 10/min; General: 60/min |
| CSRF | CSRF token required on all POST/PATCH/DELETE endpoints |
| Cookie settings | JWT: httpOnly, SameSite=Strict, Secure in production |
| Logging | NEVER log passwords, JWT tokens, card numbers, or full phone numbers |
| Error messages | NEVER expose DB errors, stack traces, or internal state to users |
| Dependencies | Run `npm audit` before adding any dependency. No HIGH/CRITICAL CVEs |

**Security review trigger:** Any PR touching auth, payments, DB schema, or input validation must be reviewed by Validator agent AND flagged to John (AI Director).

---

## 8. Performance Coding Practices

| Practice | Rule |
|----------|------|
| Database queries | One bounded query per API call where possible. Use JOIN instead of N+1 |
| Pagination | Never load all transactions in history — always paginate (default: 20 per page) |
| bcrypt | Password max length = 1,000 chars (validation before bcrypt to prevent DoS) |
| SQLite | Use WAL mode for concurrent reads. Serialize writes (one at a time) |
| Next.js | Use Server Components for static/cached data; Client Components for interactive UI |
| Bundle size | Run `npm run build` and check bundle analyzer before UI changes |

---

## Related Documents

- [Developer Onboarding Guide](./developer-onboarding-guide.md)
- [Local Development Setup](./local-development-setup.md)
- [Test Strategy](../templates-testing/test-strategy.md)
- [Definition of Done](../templates-testing/definition-of-done.md)

---

## Approval
| Role | Name | Date | Signature |
|------|------|------|-----------|
| Author | John (AI Director) | 2026-02-23 | Approved (AI) |
| QA Lead | Validator Agent | 2026-02-23 | Approved (AI) |
| Tech Lead | John | 2026-02-23 | Approved |
| CEO (Alem) | Alem Bašić | TBD | |