|
@@ -0,0 +1,161 @@
|
|
|
|
|
+# Torqvoice, for coding agents
|
|
|
|
|
+
|
|
|
|
|
+Torqvoice is workshop management for automotive (and marine) service businesses: customers,
|
|
|
|
|
+vehicles, work orders, quotes, invoices, inventory, inspections, messaging, bookkeeping sync.
|
|
|
|
|
+One codebase runs both the self-hosted install and Torqvoice Cloud, where each workshop is an
|
|
|
|
|
+organization. Next.js 16 App Router, TypeScript, PostgreSQL, Prisma 7, Better Auth, next-intl,
|
|
|
|
|
+Tailwind 4 with shadcn/ui, Biome, Vitest, Playwright. Licensed under the Elastic License 2.0.
|
|
|
|
|
+
|
|
|
|
|
+User docs live at https://torqvoice.com/docs and are written by the maintainer, not in this repo.
|
|
|
|
|
+`CONTRIBUTING.md` has setup, the CLA and the pull request rules; this file is what you need
|
|
|
|
|
+while writing code and cannot see from the code itself.
|
|
|
|
|
+
|
|
|
|
|
+## Commands
|
|
|
|
|
+
|
|
|
|
|
+Open the repo in the Dev Container (`.devcontainer/`) and everything below works as is: Node 22,
|
|
|
|
|
+Postgres 16 as the `db` service on 5432, pgAdmin on 8080, Biome formatting on save. Outside it
|
|
|
|
|
+you need Node 22 and your own Postgres (`docker-compose up -d db`), then `cp .env.example .env`.
|
|
|
|
|
+
|
|
|
|
|
+```bash
|
|
|
|
|
+npm run dev # http://localhost:3000, needs DATABASE_URL in .env
|
|
|
|
|
+npm run check # Biome lint + format check, what CI runs (check:fix writes)
|
|
|
|
|
+npm test # Vitest, jsdom, Prisma mocked
|
|
|
|
|
+npx vitest run src/__tests__/<path>.test.tsx # one test file
|
|
|
|
|
+NODE_OPTIONS=--max-old-space-size=6144 npx tsc --noEmit # typecheck; without the flag it runs out of memory
|
|
|
|
|
+npm run test:db # the few tests that need Postgres; E2E_DATABASE_URL must contain "test" or "e2e"
|
|
|
|
|
+npm run test:e2e # Playwright against a built app and a real database, see e2e/README.md
|
|
|
|
|
+npx prisma generate # after a fresh clone or a schema change; output is src/generated (gitignored)
|
|
|
|
|
+npx prisma migrate dev --name <change> # schema change; commit the folder it creates
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+Formatting is Biome only. There is no ESLint or Prettier config; running Prettier rewrites
|
|
|
|
|
+whole files. Schema changes go through `prisma migrate dev` and a reviewed migration folder,
|
|
|
|
|
+never `prisma migrate deploy` or `prisma db push`.
|
|
|
|
|
+
|
|
|
|
|
+## Gotchas
|
|
|
|
|
+
|
|
|
|
|
+- `npm test` can end with every test passing and exit code 1: `EnvironmentTeardownError:
|
|
|
|
|
+ Closing rpc while "onUserConsoleLog" was pending`. That is a Vitest worker race, not your
|
|
|
|
|
+ change; rerun it.
|
|
|
|
|
+- A git worktree of this repo needs `node_modules` and `src/generated` symlinked from the main
|
|
|
|
|
+ checkout before Vitest or `tsc` will run there.
|
|
|
|
|
+- `.env.example` sets `DATABASE_URL` to host `db`, which resolves inside the Dev Container.
|
|
|
|
|
+ Outside it, change the host to `localhost`.
|
|
|
|
|
+
|
|
|
|
|
+## Where things are
|
|
|
|
|
+
|
|
|
|
|
+| Path | What |
|
|
|
|
|
+| --- | --- |
|
|
|
|
|
+| `src/app/(authenticated)` | Signed-in pages. Server components fetch with Prisma, then hand data to a client component. |
|
|
|
|
|
+| `src/app/(public)`, `(designer)`, `(presenter)` | Auth, portal and share links; the invoice and email designers; the work board for a wall screen. |
|
|
|
|
|
+| `src/app/api` | Route handlers: webhooks, PDFs, uploads, the technician app API under `v1/tech`, WebSockets. |
|
|
|
|
|
+| `src/features/<domain>/` | `Actions/` server actions, `Components/` client components, `Schema/` zod, `Lib/` helpers. |
|
|
|
|
|
+| `src/components/ui` | shadcn primitives. Shared app components sit one level up. |
|
|
|
|
|
+| `src/lib` | Auth, database, permissions, money, dates, realtime, backup, cron. |
|
|
|
|
|
+| `src/integrations/<id>/` | One connector per folder: `manifest.ts` + `server.ts`, listed in `registry.ts`. A test fails if a folder is missing from the registry. |
|
|
|
|
|
+| `prisma/schema/*.prisma` | The schema, split by area. `prisma/migrations/` is history; never edit an applied one. |
|
|
|
|
|
+| `messages/<locale>/<namespace>.json` | Translations. English is the source; other locales may lag. |
|
|
|
|
|
+| `src/__tests__/` | Vitest, mirroring `src/`. `e2e/specs/` is Playwright. |
|
|
|
|
|
+
|
|
|
|
|
+### The app directory
|
|
|
|
|
+
|
|
|
|
|
+Every `page.tsx` and `layout.tsx` in `src/app` is a server component: it reads the session,
|
|
|
|
|
+fetches with Prisma or a feature's action, and renders. A page that needs interactivity hands
|
|
|
|
|
+its data to a client component. One that belongs only to that page may sit beside it as
|
|
|
|
|
+`<route>-client.tsx`; anything reused lives in `src/features`. Route groups set the shell:
|
|
|
|
|
+
|
|
|
|
|
+```
|
|
|
|
|
+src/app/
|
|
|
|
|
+├── (authenticated)/ # signed-in workshop, layout.tsx has the sidebar and session
|
|
|
|
|
+│ ├── page.tsx # dashboard
|
|
|
|
|
+│ ├── vehicles/ # list, [id], [id]/service/new and [serviceId]; a work order is a service record
|
|
|
|
|
+│ ├── sales/ # counter sales: a work order without a vehicle
|
|
|
|
|
+│ ├── settings/ # one folder per settings page, settings-nav.tsx lists them
|
|
|
|
|
+│ └── admin/ # platform admin, super admins only
|
|
|
|
|
+├── (public)/ # nobody signed in, every page is token or code gated
|
|
|
|
|
+│ ├── auth/ # sign-in, sign-up, 2FA, email verification, password reset
|
|
|
|
|
+│ ├── onboarding/ app-setup/
|
|
|
|
|
+│ ├── portal/[orgId]/ # the customer portal
|
|
|
|
|
+│ ├── share/ # invoice, quote, inspection, status-report, terms by link
|
|
|
|
|
+│ ├── p/[token] # a phone opened from a work order's "Add from phone" code
|
|
|
|
|
+│ └── b/[token] # an online booking
|
|
|
|
|
+├── (designer)/ # invoice-designer and email-designer, full-screen canvas
|
|
|
|
|
+├── (presenter)/work-board/ # the work board for a wall screen
|
|
|
|
|
+├── api/
|
|
|
|
|
+│ ├── protected/ # route handlers for the signed-in app: uploads, files, ws, AI, backup
|
|
|
|
|
+│ ├── public/ # share links, portal, tracking pixels, images
|
|
|
|
|
+│ ├── webhooks/ # stripe, paypal, vipps, email, sms, whatsapp, telegram
|
|
|
|
|
+│ ├── integrations/[connector]/ # OAuth callbacks for connectors
|
|
|
|
|
+│ ├── v1/ # tech (the technician app), cron, health
|
|
|
|
|
+│ └── system/ # backup-heartbeat
|
|
|
|
|
+├── layout.tsx # root: providers, theme, locale
|
|
|
|
|
+└── globals.css # theme tokens for light, dark and the colour themes
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+API routes are for what cannot be a server action: a browser upload, a webhook, a PDF, a
|
|
|
|
|
+WebSocket, the technician app. Everything else goes through an action.
|
|
|
|
|
+
|
|
|
|
|
+## How code is written here
|
|
|
|
|
+
|
|
|
|
|
+**Server actions** are the only write path. Each lives in its own file under
|
|
|
|
|
+`src/features/<domain>/Actions/`, is wrapped with `withAuth` from `src/lib/with-auth.ts`, and
|
|
|
|
|
+returns `ActionResult<T>` (`{ success, data?, error?, gated?, forbidden? }`). `withAuth` hands
|
|
|
|
|
+you `ctx.organizationId`; every Prisma query is scoped by it, including lookups by id, because
|
|
|
|
|
+ids from the browser can belong to another workshop. Pass `requiredPermissions` when the
|
|
|
|
|
+action is not for everyone; `src/lib/permissions.ts` lists them. Validate input with zod
|
|
|
|
|
+before touching the database.
|
|
|
|
|
+
|
|
|
|
|
+**Four things an action may also need.** An action that changes a document, money or access
|
|
|
|
|
+passes `audit` to `withAuth` (`src/lib/audit.ts` has the event shape; 75 actions do this).
|
|
|
|
|
+One that sends a message or email or calls a paid service starts with `demoGuard()` from
|
|
|
|
|
+`src/lib/demo.ts`, because the public demo runs with real provider credentials; a test lists
|
|
|
|
|
+every unguarded one and fails. One a cloud plan may refuse returns `gated`, and the screen
|
|
|
|
|
+passes the result to `handleGated` from `src/components/upgrade-gate` before showing an error.
|
|
|
|
|
+And wording that names the vehicle has two forms: `useServiceType()` from
|
|
|
|
|
+`src/components/service-type-context` says whether this workshop fixes cars or boats, and the
|
|
|
|
|
+strings for both live side by side (`title` and `titleMarine`).
|
|
|
|
|
+
|
|
|
|
|
+**Action names are an API.** A tab opened before a deploy calls the new build with what the
|
|
|
|
|
+old page sends. Changing what an action takes, or what its input means, means a new action
|
|
|
|
|
+name; the old tab is then asked to reload instead of writing bad data.
|
|
|
|
|
+
|
|
|
|
|
+**Reads** happen in server components with `db` from `src/lib/db.ts`. A client component that
|
|
|
|
|
+must fetch uses TanStack Query (`src/lib/query-provider.tsx`) and parses the answer with zod.
|
|
|
|
|
+
|
|
|
|
|
+**Every string the user sees goes through next-intl.** Add the English under `messages/en/`
|
|
|
|
|
+in the namespace that owns the screen and read it with `useTranslations`. Playwright specs
|
|
|
|
|
+and the docs find buttons by their English words, so changing copy is a visible change, not
|
|
|
|
|
+a cleanup.
|
|
|
|
|
+
|
|
|
|
|
+**Dates and money.** The database holds UTC. Render in the workshop's time zone through
|
|
|
|
|
+`src/lib/timezone.ts` and `src/lib/workshop-datetime.ts`; no `setHours` or `getDay` on a raw
|
|
|
|
|
+`Date` on the server. Amounts carry a currency code (`src/lib/money.ts`), countries are
|
|
|
|
|
+ISO 3166-1, languages ISO 639-1.
|
|
|
|
|
+
|
|
|
|
|
+**Nothing country-specific by default.** Registries, tax rules, payment providers and parts
|
|
|
|
|
+sources ship as connectors or settings a workshop turns on, never as behaviour everyone sees.
|
|
|
|
|
+
|
|
|
|
|
+**UI.** Use the shadcn components and the theme tokens (`bg-background`, `text-muted-foreground`,
|
|
|
|
|
+`border`): the app has dark mode and six colour themes defined in `src/app/globals.css`, so a
|
|
|
|
|
+hard-coded colour is wrong in most of them. A dialog with a long form uses `DialogHeader`,
|
|
|
|
|
+`DialogBody` and `DialogFooter` from `src/components/ui/dialog`, so the buttons stay pinned
|
|
|
|
|
+while the fields scroll. Client components are `"use client"` files in `src/features`;
|
|
|
|
|
+pages in `src/app` stay server components.
|
|
|
|
|
+
|
|
|
|
|
+**Naming.** `src/components` and `src/lib` are kebab-case files. Inside `src/features` the
|
|
|
|
|
+folders are `Actions`, `Components`, `Schema`, `Lib` and component files are PascalCase.
|
|
|
|
|
+Imports use `@/`.
|
|
|
|
|
+
|
|
|
|
|
+## Tests
|
|
|
|
|
+
|
|
|
|
|
+Vitest tests assert what a user would notice, not implementation details, and mock Prisma;
|
|
|
|
|
+`src/__tests__/setup.ts` wires next-intl. Anything that only breaks with the pieces assembled
|
|
|
|
|
+(migrations, cookies, forms wired to actions, invoice numbering) is a Playwright spec. CI runs
|
|
|
|
|
+`npm run check`, `npm test` and the e2e suite in eight shards on every pull request.
|
|
|
|
|
+
|
|
|
|
|
+## Pull requests
|
|
|
|
|
+
|
|
|
|
|
+One change per PR, imperative title (it becomes the release note), body from
|
|
|
|
|
+`.github/PULL_REQUEST_TEMPLATE.md`: Summary, Why, Testing. First-time contributors keep the
|
|
|
|
|
+CLA line; the maintainer deletes it. Screenshots for UI changes. Before opening:
|
|
|
|
|
+`npm run check && npm test`.
|