diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..eb68407 --- /dev/null +++ b/Makefile @@ -0,0 +1,60 @@ +# Familiarise local development. +# +# make up everything (Postgres + API + Flutter web) +# make db Postgres only — the fast path if containers feel slow +# make down stop, keeping data and build caches +# +# `make help` lists everything. + +.DEFAULT_GOAL := help +.PHONY: help up db api down reset logs shell psql test regen regen-force seed-sql doctor + +help: ## Show this help + @grep -hE '^[a-z-]+:.*?## ' $(MAKEFILE_LIST) \ + | awk 'BEGIN{FS=":.*?## "}{printf " \033[36m%-12s\033[0m %s\n", $$1, $$2}' + +up: ## Start the full stack (Postgres + API + Flutter web) + docker compose up + +db: ## Start Postgres only, provisioned — use with a native `dart_frog dev` + docker compose up db-init + +api: ## Start Postgres + API, no Flutter web + docker compose up db-init api + +down: ## Stop everything, KEEPING the database and build caches + docker compose down + +reset: ## Destroy the database and all build caches (next start is a cold rebuild) + docker compose down -v + +logs: ## Tail logs from all services + docker compose logs -f + +shell: ## Open a shell in the API container + docker compose exec api bash + +psql: ## Open psql against the local database + docker compose exec db psql -U familiarise -d familiarise + +test: ## Run the backend test suite inside the API container + docker compose exec api dart test + +regen: ## Backend codegen, skipped if schema/versions/build.yaml are unchanged + ./scripts/regenerate-build.sh --prisma + +regen-force: ## Backend codegen, wiping lib/generated first + ./scripts/regenerate-build.sh --prisma --force + +seed-sql: ## Rebuild backend/prisma/sql/seed-dev.sql from prompts/testing/unit/*.md + ./scripts/gen-dev-seed.sh + +doctor: ## Check that the local toolchain can run the stack + @printf 'docker : '; docker --version 2>/dev/null || echo 'MISSING' + @printf 'compose : '; docker compose version --short 2>/dev/null || echo 'MISSING' + @printf 'daemon : '; v=$$(docker info --format '{{.ServerVersion}} ({{.Architecture}})' 2>/dev/null); \ + [ -n "$$v" ] && [ "$$v" != " ()" ] && echo "$$v" || echo 'NOT RUNNING — start Docker Desktop' + @printf 'flutter : '; flutter --version 2>/dev/null | head -1 || echo 'MISSING' + @printf 'port 5433 : '; lsof -ti:5433 >/dev/null 2>&1 && echo 'IN USE — set FAM_PG_PORT' || echo 'free' + @printf 'port 8080 : '; lsof -ti:8080 >/dev/null 2>&1 && echo 'IN USE' || echo 'free' + @printf 'port 3000 : '; lsof -ti:3000 >/dev/null 2>&1 && echo 'IN USE' || echo 'free' diff --git a/backend/.dockerignore b/backend/.dockerignore new file mode 100644 index 0000000..84c08df --- /dev/null +++ b/backend/.dockerignore @@ -0,0 +1,11 @@ +.env +.env.* +!.env.example +.dart_tool/ +.packages +build/ +.git/ +.vscode/ +.serena/ +*.md +test/ diff --git a/backend/.env.example b/backend/.env.example index e7bd30e..9531f6b 100644 --- a/backend/.env.example +++ b/backend/.env.example @@ -1,9 +1,27 @@ # Familiarise Backend Environment Configuration -# Copy this file to .env and fill in the values - -# Database (Supabase PostgreSQL) -DATABASE_URL=postgresql://user:password@host:6543/postgres?pgbouncer=true -DIRECT_URL=postgresql://user:password@host:5432/postgres +# Copy this file to .env and fill in the values. +# +# ── Precedence ─────────────────────────────────────────────────────────────── +# main.dart resolves configuration in this order, last one winning: +# .env → .env.local → the process environment +# So an exported variable (or a docker compose `environment:` entry) always +# beats this file. That is what makes the database switchable without editing +# anything. +# +# ── Database ───────────────────────────────────────────────────────────────── +# You normally do NOT need to set the two URLs below. +# +# docker compose up → containers get the local Postgres +# source scripts/use-db.sh local → native dart_frog dev, local Postgres +# source scripts/use-db.sh supabase → native, cloud DB (see .env.supabase.example) +# +# Set them here only if you want a fixed default for every native run. Leaving +# a stale cloud URL here is the classic way to run destructive tests against +# shared data by accident — the startup log prints the resolved host on every +# boot so you can catch it. +# +# DATABASE_URL=postgresql://user:password@host:6543/postgres?pgbouncer=true +# DIRECT_URL=postgresql://user:password@host:5432/postgres # JWT Secret (generate a secure random string) JWT_SECRET=your-jwt-secret-here @@ -36,5 +54,9 @@ STRIPE_SECRET_KEY=sk_test_xxxxx # Webhook signing secret from Stripe Dashboard -> Webhooks STRIPE_WEBHOOK_SECRET=whsec_xxxxx +# PAN encryption key (required by the consultant tax-info routes). +# 32 bytes, hex-encoded — generate with: openssl rand -hex 32 +PAN_ENCRYPTION_KEY=0000000000000000000000000000000000000000000000000000000000000000 + # Server PORT=8080 diff --git a/backend/.env.supabase.example b/backend/.env.supabase.example new file mode 100644 index 0000000..2699813 --- /dev/null +++ b/backend/.env.supabase.example @@ -0,0 +1,25 @@ +# Template for backend/.env.supabase — the shared cloud database. +# +# Copy to backend/.env.supabase (gitignored) and fill in the real values, then: +# +# source scripts/use-db.sh supabase # native dart_frog dev +# FAM_DIRECT_URL="$DIRECT_URL" docker compose up # containers +# +# Local development does NOT need this file. The default everywhere is the +# docker compose Postgres; this exists only for the times you must reproduce +# something against real data. +# +# ⚠️ This is a shared database. Anything you write is visible to everyone and +# anything you delete is gone. Prefer `source scripts/use-db.sh local`. +# backend/prisma/sql/seed-dev.sql and docker/db-init refuse to touch any +# host that is not local, precisely so this cannot be provisioned by +# accident. + +# Session-mode pooler (port 5432). The backend uses DIRECT_URL because +# prisma_flutter_connector relies on prepared statements, which PgBouncer's +# transaction mode does not support. +DIRECT_URL=postgresql://USER:PASSWORD@aws-0-REGION.pooler.supabase.com:5432/postgres + +# Transaction-mode pooler (port 6543). Kept for parity with the deployed +# configuration; the backend prefers DIRECT_URL when both are set. +DATABASE_URL=postgresql://USER:PASSWORD@aws-0-REGION.pooler.supabase.com:6543/postgres?pgbouncer=true diff --git a/backend/.gitignore b/backend/.gitignore index 5adbfeb..8fbc280 100644 --- a/backend/.gitignore +++ b/backend/.gitignore @@ -6,7 +6,13 @@ # Files and directories created by pub .dart_tool/ .packages -pubspec.lock +# NOTE: pubspec.lock is deliberately NOT ignored. This is an application +# package (publish_to: none), so the lockfile belongs in version control — +# without it Docker builds and CI resolve fresh every time, which makes them +# non-reproducible and lets a caret-range bump (e.g. freezed 2.x -> 3.x, a +# breaking generator change) break the build with no code change. It is also +# an input to backend/scripts/ensure-generated.sh's codegen hash. +# The root pubspec.lock has always been tracked; this makes the two consistent. # Files and directories created by dart_frog build/ @@ -16,7 +22,12 @@ build/ coverage/ # Environment files +# .env.supabase holds real cloud credentials; .env.local is the per-developer +# override layer. Only the *.example templates are tracked. .env +.env.* +!.env.example +!.env.*.example # ============================================ # Code Generation (regenerate with commands below) diff --git a/backend/Dockerfile b/backend/Dockerfile new file mode 100644 index 0000000..97c2ee5 --- /dev/null +++ b/backend/Dockerfile @@ -0,0 +1,82 @@ +# Familiarise Mobile API — Production Dockerfile +# +# IMPORTANT BUILD NOTES (learned from failed deployments): +# +# 1. BASE IMAGE: Must use Flutter image, NOT dart:stable. +# prisma_flutter_connector depends on the Flutter SDK for dependency +# resolution. Using dart:stable causes "flutter from sdk doesn't exist" +# during `dart pub get`. +# +# 2. PRISMA CODEGEN: lib/generated/ is gitignored (PR #108), so Railway +# never receives the Prisma-generated types. The Dockerfile must regenerate +# them via `dart run prisma_flutter_connector:generate`. +# +# 3. BUILD_RUNNER: After Prisma generation, freezed/json_serializable must +# also run. Without this, the AOT compile fails with "Not a constant +# expression" and undefined type errors in repositories. +# +# 4. PRISMA SCHEMA: backend/prisma/schema.prisma was originally a symlink +# to ../../familiarise_web/prisma/schema.prisma. Docker cannot follow +# symlinks that point outside the build context. The symlink was replaced +# with a real file copy. If the web schema changes, re-copy it: +# cp ~/Desktop/familiarise_web/prisma/schema.prisma backend/prisma/schema.prisma +# +# 5. RUNTIME IMAGE: Cannot use `FROM scratch` with the Flutter build image. +# The official dart:stable image provides /runtime/ (libc + friends) for +# scratch-based final stages, but the Flutter image does not. AOT-compiled +# Dart binaries need libc, so we use debian:bookworm-slim instead. +# +# 6. LOCAL TESTING: Always test locally before deploying to Railway: +# docker build -t familiarise-mobile-api . +# docker run -p 8080:8080 --env-file .env familiarise-mobile-api +# curl http://localhost:8080/api/health +# +# Build steps mirror: scripts/regenerate-build.sh --backend +# ───────────────────────────────────────────────────────────── + +# ── Stage 1: Build ────────────────────────────────────────── +FROM ghcr.io/cirruslabs/flutter:stable AS build + +WORKDIR /app + +# Install dart_frog_cli for the build step +RUN dart pub global activate dart_frog_cli +ENV PATH="/root/.pub-cache/bin:${PATH}" + +# Copy and resolve dependencies first (Docker layer caching) +COPY pubspec.* ./ +RUN flutter pub get + +# Copy full source +COPY . . +RUN flutter pub get --offline + +# 1. Generate Prisma client (lib/generated/ is gitignored, must regenerate) +RUN dart run prisma_flutter_connector:generate \ + --schema prisma/schema.prisma \ + --output lib/generated \ + --server + +# 2. Run build_runner for freezed/json codegen on generated models +RUN dart run build_runner build --delete-conflicting-outputs + +# 3. Generate Dart Frog build output (creates build/bin/server.dart) +RUN dart_frog build + +# 4. AOT compile the generated server to a native binary +RUN dart compile exe build/bin/server.dart -o build/bin/server + +# ── Stage 2: Runtime ──────────────────────────────────────── +# Use debian:bookworm-slim (NOT scratch) — the Flutter build image does not +# provide /runtime/ like dart:stable does. AOT binaries need libc + friends. +FROM debian:bookworm-slim + +RUN apt-get update && apt-get install -y --no-install-recommends \ + ca-certificates \ + && rm -rf /var/lib/apt/lists/* + +COPY --from=build /app/build/bin/server /app/bin/server + +EXPOSE 8080 + +CMD ["/app/bin/server"] diff --git a/backend/Dockerfile.dev b/backend/Dockerfile.dev new file mode 100644 index 0000000..b995561 --- /dev/null +++ b/backend/Dockerfile.dev @@ -0,0 +1,43 @@ +# Development image for the Dart Frog API — hot reload, used by docker-compose. +# +# This is NOT the deployment image. See ./Dockerfile for that; its header +# comments document the constraints both images share, in particular: +# +# The base MUST be a Flutter image, not dart:stable. prisma_flutter_connector +# declares `flutter: sdk: flutter`, so `pub get` fails on a Dart-only SDK with +# "flutter from sdk doesn't exist". +# +# Deliberately does NO code generation at build time. lib/generated is ~735k +# lines rebuilt from prisma/schema.prisma; baking it into a layer would mean +# re-running it on every image rebuild. Instead docker-compose keeps +# lib/generated and .dart_tool in named volumes and the entrypoint regenerates +# only when the inputs actually change (see backend/scripts/ensure-generated.sh). + +FROM ghcr.io/cirruslabs/flutter:stable + +WORKDIR /app + +# curl: used by the compose healthcheck. +# postgresql-client: lets you psql the database from inside the container. +RUN apt-get update && apt-get install -y --no-install-recommends \ + curl \ + ca-certificates \ + postgresql-client \ + && rm -rf /var/lib/apt/lists/* + +RUN dart pub global activate dart_frog_cli +ENV PATH="/root/.pub-cache/bin:${PATH}" + +# Copied to /usr/local/bin rather than left in /app, because compose +# bind-mounts ./backend over /app and would shadow anything placed there. +COPY docker/dev-entrypoint.sh /usr/local/bin/dev-entrypoint +RUN chmod +x /usr/local/bin/dev-entrypoint + +EXPOSE 8080 + +ENTRYPOINT ["/usr/local/bin/dev-entrypoint"] + +# --hostname 0.0.0.0 is required for the published port to be reachable from +# the host; dart_frog dev binds localhost otherwise. Verified supported in +# dart_frog_cli 1.2.14 (it must be a literal IP, not a hostname). +CMD ["dart_frog", "dev", "--hostname", "0.0.0.0", "--port", "8080"] diff --git a/backend/build.yaml b/backend/build.yaml new file mode 100644 index 0000000..1194be2 --- /dev/null +++ b/backend/build.yaml @@ -0,0 +1,40 @@ +# Backend code-generation scope. +# +# ── Why this file exists ───────────────────────────────────────────────────── +# +# json_serializable was running as a build phase over every Dart file in this +# package and producing NOTHING. There is not a single `part '*.g.dart'` +# directive under backend/lib or backend/routes, and no generated .freezed.dart +# references _$…FromJson — the Prisma connector emits hand-written +# fromJson/toJson bodies instead (see lib/generated/models/*.dart). +# +# The one .g.dart in the package, lib/generated/schema_registry.g.dart, is +# emitted by `prisma_flutter_connector:generate`, NOT by build_runner, so +# disabling json_serializable cannot affect it. +# +# Disabling it also removes two more phases for free: json_serializable +# `applies_builders: [source_gen|combining_builder]`, and combining_builder is +# `auto_apply: none` (so nothing else pulls it in) and itself applies +# source_gen|part_cleanup. It additionally drops freezed's +# `runs_before: [json_serializable]` ordering barrier, allowing more parallelism. +# +# If you ever add an @JsonSerializable class to the backend, set `enabled: true` +# below and delete this comment. + +targets: + $default: + builders: + json_serializable:json_serializable: + enabled: false + + freezed:freezed: + enabled: true + # Every @freezed class in this package is Prisma-generated. Scoping + # freezed to lib/generated stops it walking routes/ (132 files) and the + # hand-written parts of lib/ looking for annotations that cannot be + # there. + generate_for: + include: + - lib/generated/**.dart + exclude: + - lib/generated/**.freezed.dart diff --git a/backend/docker/dev-entrypoint.sh b/backend/docker/dev-entrypoint.sh new file mode 100755 index 0000000..50b2c5e --- /dev/null +++ b/backend/docker/dev-entrypoint.sh @@ -0,0 +1,25 @@ +#!/usr/bin/env bash +# Entrypoint for the API dev container. +# +# Runs before `dart_frog dev`. Everything here is designed to be a no-op on a +# warm container so `docker compose restart api` comes back in seconds rather +# than minutes. + +set -euo pipefail +cd /app +export PATH="/root/.pub-cache/bin:$PATH" + +# .dart_tool is a named volume, so it starts empty on a fresh volume even +# though pubspec.yaml is bind-mounted from the host. Resolve if needed. +if [ ! -f .dart_tool/package_config.json ]; then + echo "==> flutter pub get (cold volume)" + flutter pub get +fi + +# Hash-guarded: regenerates lib/generated only when schema.prisma, the +# connector/freezed versions, or build.yaml actually changed. On a warm +# container this prints "up to date" and exits immediately. +./scripts/ensure-generated.sh + +echo "==> starting: $*" +exec "$@" diff --git a/backend/main.dart b/backend/main.dart index d1632b1..5e17c91 100644 --- a/backend/main.dart +++ b/backend/main.dart @@ -1,4 +1,9 @@ -import 'dart:io'; +import 'dart:io' show File, HttpServer, InternetAddress; +// Prefixed deliberately: lib/generated/index.dart exports a `Platform` enum +// from the Prisma schema, and database_client.dart re-exports it, so an +// unprefixed `Platform` here resolves to that enum instead of dart:io's. +// Same gotcha documented in lib/database/database_client.dart. +import 'dart:io' as io show Platform; import 'package:backend/database/database_client.dart'; import 'package:backend/services/auth/auth_service.dart'; @@ -14,8 +19,23 @@ import 'package:dotenv/dotenv.dart'; /// Server entry point /// Initializes database, services, and starts the HTTP server Future run(Handler handler, InternetAddress ip, int port) async { - // Load environment variables - final env = DotEnv()..load(['.env']); + // Environment resolution, lowest precedence first: + // 1. .env — shared local config (gitignored) + // 2. .env.local — per-developer override layer (gitignored), + // e.g. pointing DIRECT_URL at local Postgres + // 3. Platform.environment — ALWAYS WINS. This is what docker compose, + // Railway and CI inject. + // + // Note the deliberate ordering. DotEnv's own `includePlatformEnvironment` + // flag injects the environment in the *constructor*, and `load()` then does + // `_map.addAll(file)` on top — so the file would override the environment. + // That is backwards for containers: a stale DIRECT_URL in a bind-mounted + // .env would silently hijack a "local" run and point it at production. + // Applying Platform.environment last inverts that. + final env = DotEnv(); + if (File('.env').existsSync()) env.load(['.env']); + if (File('.env.local').existsSync()) env.load(['.env.local']); + env.addAll(io.Platform.environment); // Initialize Sentry for error tracking (optional) await SentryLogger.init(env['SENTRY_DSN']); @@ -25,12 +45,15 @@ Future run(Handler handler, InternetAddress ip, int port) async { // which the prisma_flutter_connector uses internally final databaseUrl = env['DIRECT_URL'] ?? env['DATABASE_URL']; if (databaseUrl == null) { - throw Exception('DIRECT_URL or DATABASE_URL must be set in .env'); + throw Exception( + 'DIRECT_URL or DATABASE_URL must be set in the environment or .env. ' + 'For local development run: source scripts/use-db.sh local', + ); } final jwtSecret = env['JWT_SECRET']; if (jwtSecret == null) { - throw Exception('JWT_SECRET must be set in .env'); + throw Exception('JWT_SECRET must be set in the environment or .env'); } // GitHub OAuth credentials (optional - only needed if using GitHub auth) @@ -49,7 +72,10 @@ Future run(Handler handler, InternetAddress ip, int port) async { } // Initialize database - SentryLogger.info('Connecting to database...', context: 'Startup'); + SentryLogger.info( + 'Connecting to database... (${_describeTarget(databaseUrl)})', + context: 'Startup', + ); final db = await DatabaseClient.initialize(databaseUrl); SentryLogger.info('Database connected successfully!', context: 'Startup'); @@ -116,3 +142,20 @@ Future run(Handler handler, InternetAddress ip, int port) async { ); return serve(handlerWithProviders, ip, port); } + +/// Renders the connection target of [databaseUrl] as `host:port/database`. +/// +/// Credentials are never included — this string is logged on every boot (and +/// forwarded to Sentry), so it must stay safe to read in a shared console. +/// +/// Its purpose is to make "am I about to run destructive tests against +/// production?" answerable at a glance: a local run must print `db:5432` (in +/// Docker) or `localhost:5433` (native), never a `*.supabase.com` host. +String _describeTarget(String databaseUrl) { + final uri = Uri.tryParse(databaseUrl); + if (uri == null || uri.host.isEmpty) return 'target='; + + final database = uri.pathSegments.isEmpty ? '?' : uri.pathSegments.first; + final port = uri.hasPort ? uri.port : 5432; + return 'host=${uri.host}:$port, db=$database'; +} diff --git a/backend/prisma/sql/check-constraints.sql b/backend/prisma/sql/check-constraints.sql new file mode 100644 index 0000000..d8f26c9 --- /dev/null +++ b/backend/prisma/sql/check-constraints.sql @@ -0,0 +1,186 @@ +-- ───────────────────────────────────────────────────────────── +-- VENDORED from familiarise_web (which owns the Prisma schema). +-- Do not edit here. To refresh after an upstream change: +-- cp ~/Desktop/familiarise_web/prisma/sql/check-constraints.sql \ +-- backend/prisma/sql/check-constraints.sql +-- +-- Applied by docker/db-init against the local Postgres. The upstream +-- `-- SPLIT` markers are plain comments here: psql runs the whole file +-- in one go, so no statement splitting is needed (they exist only +-- because Prisma $executeRawUnsafe takes one statement per call). +-- ───────────────────────────────────────────────────────────── + +-- #676 A1–A4 — data-integrity CHECK constraints for booking/payment tables. +-- +-- Prisma 7 has no @@check in PSL, so these ride the same sidecar pattern as +-- ledger-triggers.sql: idempotent statements split on `-- SPLIT`, applied via +-- `npm run db:constraints` after every push/reset (NOT against the shared dev +-- DB mid-cycle — the pre-MVP reset applies them to a clean schema). +-- +-- Payment amounts use >= 0, not > 0: credit-covered checkouts and +-- org-sponsored bookings legitimately write amount = 0 (free_/org_ synthetic +-- payment intents in lib/payments/operations/checkout.ts). + +ALTER TABLE "SlotOfAppointment" DROP CONSTRAINT IF EXISTS "slot_time_order"; +-- SPLIT +ALTER TABLE "SlotOfAppointment" ADD CONSTRAINT "slot_time_order" CHECK ("endsAt" > "startsAt"); +-- SPLIT +ALTER TABLE "Payment" DROP CONSTRAINT IF EXISTS "payment_amounts_nonnegative"; +-- SPLIT +ALTER TABLE "Payment" ADD CONSTRAINT "payment_amounts_nonnegative" CHECK ("amount" >= 0 AND "originalAmount" >= 0 AND "taxAmount" >= 0); +-- SPLIT +ALTER TABLE "ConsultationPlan" DROP CONSTRAINT IF EXISTS "consultation_plan_price_nonnegative"; +-- SPLIT +ALTER TABLE "ConsultationPlan" ADD CONSTRAINT "consultation_plan_price_nonnegative" CHECK ("price" >= 0); +-- SPLIT +ALTER TABLE "SubscriptionPlan" DROP CONSTRAINT IF EXISTS "subscription_plan_price_nonnegative"; +-- SPLIT +ALTER TABLE "SubscriptionPlan" ADD CONSTRAINT "subscription_plan_price_nonnegative" CHECK ("price" >= 0); +-- SPLIT +ALTER TABLE "WebinarPlan" DROP CONSTRAINT IF EXISTS "webinar_plan_price_nonnegative"; +-- SPLIT +ALTER TABLE "WebinarPlan" ADD CONSTRAINT "webinar_plan_price_nonnegative" CHECK ("price" >= 0); +-- SPLIT +ALTER TABLE "ClassPlan" DROP CONSTRAINT IF EXISTS "class_plan_price_nonnegative"; +-- SPLIT +ALTER TABLE "ClassPlan" ADD CONSTRAINT "class_plan_price_nonnegative" CHECK ("price" >= 0); +-- SPLIT +ALTER TABLE "WebinarPlan" DROP CONSTRAINT IF EXISTS "webinar_plan_max_participants_min"; +-- SPLIT +ALTER TABLE "WebinarPlan" ADD CONSTRAINT "webinar_plan_max_participants_min" CHECK ("maxParticipants" >= 1); +-- SPLIT +ALTER TABLE "ClassPlan" DROP CONSTRAINT IF EXISTS "class_plan_max_participants_min"; +-- SPLIT +ALTER TABLE "ClassPlan" ADD CONSTRAINT "class_plan_max_participants_min" CHECK ("maxParticipants" >= 1); + +-- SPLIT +-- #440 — DB-level double-booking backstop for 1:1 bookings. The application +-- guards (consultant allocation lock, #827 confirm-time recheck) are the +-- first line; this exclusion constraint is the last line: two CONFIRMED +-- slots for the same consultant may never overlap in time. Scoped to rows +-- carrying the denormalized consultantProfileId — consultation/subscription +-- slot creates set it; webinar/class attendee slots deliberately leave it +-- NULL (many same-window rows per event are legitimate there) and legacy +-- pre-#440 rows are NULL. tstzrange is '[)' so back-to-back slots don't +-- conflict. +CREATE EXTENSION IF NOT EXISTS btree_gist; +-- SPLIT +ALTER TABLE "SlotOfAppointment" DROP CONSTRAINT IF EXISTS "slot_no_confirmed_overlap"; +-- SPLIT +ALTER TABLE "SlotOfAppointment" ADD CONSTRAINT "slot_no_confirmed_overlap" + EXCLUDE USING gist ( + "consultantProfileId" WITH =, + tstzrange("startsAt", "endsAt") WITH && + ) + WHERE ("consultantProfileId" IS NOT NULL AND NOT "isTentative"); + +-- SPLIT +-- #747 / #685 — DB-enforced "at most one pending invite per (org, email)". +-- Prisma's `partialIndexes` is still preview at 7.7.0 (drift bugs +-- prisma/prisma#29263 / #29415), so the partial unique index ships via this +-- sidecar instead; the Serializable tx in invitations/route.ts stays as the +-- first line. lower(email): the accept flow compares case-insensitively and +-- the POST handler normalizes, so the index must not admit a mixed-case +-- duplicate from any other writer. Applied to a clean schema (pre-MVP reset) +-- — CREATE fails loudly if duplicate pending invites already exist, which is +-- the correct outcome. +DROP INDEX IF EXISTS "invitations_org_email_pending_key"; +-- SPLIT +CREATE UNIQUE INDEX "invitations_org_email_pending_key" + ON "invitations" ("organizationId", lower("email")) + WHERE "status" = 'pending'; + +-- SPLIT +-- #676 PM-17 — extend the payment_amounts_nonnegative pattern to every other +-- money-bearing table. Every paise column is non-negative (>= 0, matching +-- Payment). We deliberately do NOT use > 0: zero is legitimate across the +-- board — fully-refunded or credit/org-sponsored rows, LICENSE-funded +-- bookings (amount = 0), and refund/earnings reversal counter-entries that +-- net a row back to zero. NULLable columns (e.g. tdsAmountPaise, netAmount) +-- are exempted automatically: a CHECK passes when its operand is NULL. +ALTER TABLE "Refund" DROP CONSTRAINT IF EXISTS "refund_amount_nonnegative"; +-- SPLIT +ALTER TABLE "Refund" ADD CONSTRAINT "refund_amount_nonnegative" CHECK ("amountPaise" >= 0); +-- SPLIT +ALTER TABLE "Dispute" DROP CONSTRAINT IF EXISTS "dispute_amount_nonnegative"; +-- SPLIT +ALTER TABLE "Dispute" ADD CONSTRAINT "dispute_amount_nonnegative" CHECK ("amountPaise" >= 0); +-- SPLIT +ALTER TABLE "ConsultantPayout" DROP CONSTRAINT IF EXISTS "consultant_payout_amounts_nonnegative"; +-- SPLIT +ALTER TABLE "ConsultantPayout" ADD CONSTRAINT "consultant_payout_amounts_nonnegative" + CHECK ("amount" >= 0 AND "tdsDeducted" >= 0 AND ("netAmount" IS NULL OR "netAmount" >= 0)); +-- SPLIT +ALTER TABLE "OrganizationPayout" DROP CONSTRAINT IF EXISTS "org_payout_amounts_nonnegative"; +-- SPLIT +ALTER TABLE "OrganizationPayout" ADD CONSTRAINT "org_payout_amounts_nonnegative" + CHECK ( + "amountPaise" >= 0 + AND "grossRevenuePaise" >= 0 + AND "platformFeePaise" >= 0 + AND "refundsPaise" >= 0 + AND "netPayoutPaise" >= 0 + AND "clawbackAmountPaise" >= 0 + AND ("tdsAmountPaise" IS NULL OR "tdsAmountPaise" >= 0) + ); +-- SPLIT +ALTER TABLE "ConsultantEarnings" DROP CONSTRAINT IF EXISTS "consultant_earnings_amounts_nonnegative"; +-- SPLIT +ALTER TABLE "ConsultantEarnings" ADD CONSTRAINT "consultant_earnings_amounts_nonnegative" + CHECK ( + "grossAmount" >= 0 + AND "platformFeePaise" >= 0 + AND "consultantSharePaise" >= 0 + AND "refundedShareAmount" >= 0 + AND ("gstTcsAccruedPaise" IS NULL OR "gstTcsAccruedPaise" >= 0) + ); +-- SPLIT +ALTER TABLE "OrganizationEarnings" DROP CONSTRAINT IF EXISTS "org_earnings_amounts_nonnegative"; +-- SPLIT +ALTER TABLE "OrganizationEarnings" ADD CONSTRAINT "org_earnings_amounts_nonnegative" + CHECK ( + "grossAmountPaise" >= 0 + AND "platformFeePaise" >= 0 + AND "orgSharePaise" >= 0 + AND "consultantSharePaise" >= 0 + AND "refundedAmountPaise" >= 0 + ); + +-- SPLIT +-- #676 PM-18 — the 3-way split must never distribute more than it took in: +-- platform fee + org share + consultant share <= gross. earnings-service.ts +-- computes orgShare as the residual (gross - platformFee - consultantShare, +-- clamped to >= 0), so equality is the norm and this catches a future writer +-- that mis-derives the split. Applied to OrganizationEarnings only: the +-- ConsultantEarnings COLLABORATOR rows deliberately carry grossAmount = 0 +-- while consultantSharePaise > 0 (the booking gross lives once on the OWNER +-- row), so the same invariant does not hold there and must not be enforced. +ALTER TABLE "OrganizationEarnings" DROP CONSTRAINT IF EXISTS "org_earnings_split_within_gross"; +-- SPLIT +ALTER TABLE "OrganizationEarnings" ADD CONSTRAINT "org_earnings_split_within_gross" + CHECK ("platformFeePaise" + "orgSharePaise" + "consultantSharePaise" <= "grossAmountPaise"); + +-- SPLIT +-- #676 PM-22 — financial-year strings are always "YYYY-YY" (e.g. 2026-27). +-- Both writers persist the FY to avoid Apr-Mar boundary drift; this rejects a +-- malformed value at write time. ConsultantPayout.tdsFinancialYear is nullable +-- (payouts without a TDS deduction), so NULL is admitted. +ALTER TABLE "TDSRecord" DROP CONSTRAINT IF EXISTS "tds_record_financial_year_format"; +-- SPLIT +ALTER TABLE "TDSRecord" ADD CONSTRAINT "tds_record_financial_year_format" + CHECK ("financialYear" ~ '^[0-9]{4}-[0-9]{2}$'); +-- SPLIT +ALTER TABLE "ConsultantPayout" DROP CONSTRAINT IF EXISTS "consultant_payout_tds_fy_format"; +-- SPLIT +ALTER TABLE "ConsultantPayout" ADD CONSTRAINT "consultant_payout_tds_fy_format" + CHECK ("tdsFinancialYear" IS NULL OR "tdsFinancialYear" ~ '^[0-9]{4}-[0-9]{2}$'); + +-- SPLIT +-- #784 — a Collaborator references exactly one plan: a webinar XOR a class. +-- The app-level backstop is assertCollaboratorPlanXor in +-- lib/collaborators/service.ts; this DB CHECK is the last line. Exactly one of +-- the two FKs is non-NULL <=> exactly one IS NULL, which `<>` expresses. +ALTER TABLE "Collaborator" DROP CONSTRAINT IF EXISTS "collaborator_plan_xor"; +-- SPLIT +ALTER TABLE "Collaborator" ADD CONSTRAINT "collaborator_plan_xor" + CHECK (("webinarPlanId" IS NULL) <> ("classPlanId" IS NULL)); diff --git a/backend/prisma/sql/ledger-triggers.sql b/backend/prisma/sql/ledger-triggers.sql new file mode 100644 index 0000000..d5759be --- /dev/null +++ b/backend/prisma/sql/ledger-triggers.sql @@ -0,0 +1,56 @@ +-- ───────────────────────────────────────────────────────────── +-- VENDORED from familiarise_web (which owns the Prisma schema). +-- Do not edit here. To refresh after an upstream change: +-- cp ~/Desktop/familiarise_web/prisma/sql/ledger-triggers.sql \ +-- backend/prisma/sql/ledger-triggers.sql +-- +-- Applied by docker/db-init against the local Postgres. The upstream +-- `-- SPLIT` markers are plain comments here: psql runs the whole file +-- in one go, so no statement splitting is needed (they exist only +-- because Prisma $executeRawUnsafe takes one statement per call). +-- ───────────────────────────────────────────────────────────── + +-- #776 — DB-level enforcement of the double-entry invariant Σ(DEBIT) == Σ(CREDIT) +-- per LedgerTransaction. The app-level guard in postLedgerTxn() is the first line; +-- this CONSTRAINT TRIGGER is the backstop that makes an unbalanced transaction +-- impossible to COMMIT even via raw SQL, a future code path, or a partial migration. +-- +-- DEFERRABLE INITIALLY DEFERRED so the check runs once at COMMIT — after all of a +-- transaction's entries are inserted — not after each row (a balanced txn is +-- transiently unbalanced mid-insert). +-- +-- prisma db push / migrate do NOT manage triggers, so this file is applied +-- separately: `npm run db:triggers` (idempotent) after every push/reset. See +-- docs/enterprise/10-money-and-ledger/13-ledger-integrity.md + docs/enterprise/90-audits/03-verification-guide.md. +-- +-- Statements are separated by `-- SPLIT` because Prisma's $executeRawUnsafe runs a +-- single statement per call (extended protocol); the apply script splits on it. + +DROP TRIGGER IF EXISTS ledger_txn_balanced ON "LedgerEntry"; +-- SPLIT +CREATE OR REPLACE FUNCTION assert_ledger_txn_balanced() RETURNS trigger AS $$ +DECLARE + txn_id text; + imbalance bigint; +BEGIN + txn_id := COALESCE(NEW."transactionId", OLD."transactionId"); + SELECT COALESCE( + SUM(CASE WHEN "direction" = 'DEBIT' THEN "amountPaise" ELSE -"amountPaise" END), + 0) + INTO imbalance + FROM "LedgerEntry" + WHERE "transactionId" = txn_id; + IF imbalance <> 0 THEN + RAISE EXCEPTION + 'Ledger transaction % is unbalanced by % paise (Sum(DEBIT) - Sum(CREDIT) != 0)', + txn_id, imbalance; + END IF; + RETURN NULL; +END; +$$ LANGUAGE plpgsql; +-- SPLIT +CREATE CONSTRAINT TRIGGER ledger_txn_balanced + AFTER INSERT OR UPDATE OR DELETE ON "LedgerEntry" + DEFERRABLE INITIALLY DEFERRED + FOR EACH ROW + EXECUTE FUNCTION assert_ledger_txn_balanced(); diff --git a/backend/prisma/sql/seed.d/01-auth.sql b/backend/prisma/sql/seed.d/01-auth.sql new file mode 100644 index 0000000..7522efc --- /dev/null +++ b/backend/prisma/sql/seed.d/01-auth.sql @@ -0,0 +1,21 @@ +-- Generated from prompts/testing/unit/01-auth.md by scripts/gen-dev-seed.sh. +-- Do not edit directly; edit the prompt and regenerate. + +BEGIN; + +INSERT INTO "users" (id, name, email, "emailVerified", role, "onboardingCompleted", "createdAt", "updatedAt") +VALUES ('test_unit_auth_u1', 'Auth Unit User', 'test_unit_auth@test.com', true, 'CONSULTEE', true, NOW(), NOW()); + +INSERT INTO "accounts" (id, "userId", "accountId", "providerId", password, "createdAt", "updatedAt") +VALUES ('test_unit_auth_a1', 'test_unit_auth_u1', 'test_unit_auth_u1', 'credential', + '$2a$12$LJ3m4ys3Lf.GEHPmwH8Xh.q5Y6oN5K6YKD3lVz8mG0V5Z8Z8Z8Z', NOW(), NOW()); + +INSERT INTO "ConsulteeProfile" (id, "userId", "createdAt", "updatedAt") +VALUES ('test_unit_auth_cp1', 'test_unit_auth_u1', NOW(), NOW()); +UPDATE "users" SET "consulteeProfileId" = 'test_unit_auth_cp1' WHERE id = 'test_unit_auth_u1'; + +INSERT INTO "cookie_preferences" (id, "userId", essential, "consentGivenAt", "consentUpdatedAt") +VALUES ('test_unit_auth_ck1', 'test_unit_auth_u1', true, NOW(), NOW()); +INSERT INTO "notification_preferences" (id, "userId") VALUES ('test_unit_auth_np1', 'test_unit_auth_u1'); + +COMMIT; diff --git a/backend/prisma/sql/seed.d/02-onboarding.sql b/backend/prisma/sql/seed.d/02-onboarding.sql new file mode 100644 index 0000000..6bc6510 --- /dev/null +++ b/backend/prisma/sql/seed.d/02-onboarding.sql @@ -0,0 +1,28 @@ +-- Generated from prompts/testing/unit/02-onboarding.md by scripts/gen-dev-seed.sh. +-- Do not edit directly; edit the prompt and regenerate. + +BEGIN; + +-- Fresh users with onboardingCompleted = false +INSERT INTO "users" (id, name, email, "emailVerified", role, "onboardingCompleted", "createdAt", "updatedAt") +VALUES + ('test_unit_onb_cee', 'Onb Consultee', 'test_unit_onb_cee@test.com', true, 'CONSULTEE', false, NOW(), NOW()), + ('test_unit_onb_cnt', 'Onb Consultant', 'test_unit_onb_cnt@test.com', true, 'CONSULTEE', false, NOW(), NOW()); + +INSERT INTO "accounts" (id, "userId", "accountId", "providerId", password, "createdAt", "updatedAt") +VALUES + ('test_unit_onb_a1', 'test_unit_onb_cee', 'test_unit_onb_cee', 'credential', '$2a$12$LJ3m4ys3Lf.GEHPmwH8Xh.q5Y6oN5K6YKD3lVz8mG0V5Z8Z8Z8Z', NOW(), NOW()), + ('test_unit_onb_a2', 'test_unit_onb_cnt', 'test_unit_onb_cnt', 'credential', '$2a$12$LJ3m4ys3Lf.GEHPmwH8Xh.q5Y6oN5K6YKD3lVz8mG0V5Z8Z8Z8Z', NOW(), NOW()); + +INSERT INTO "cookie_preferences" (id, "userId", essential, "consentGivenAt", "consentUpdatedAt") +VALUES ('test_unit_onb_ck1', 'test_unit_onb_cee', true, NOW(), NOW()), + ('test_unit_onb_ck2', 'test_unit_onb_cnt', true, NOW(), NOW()); +INSERT INTO "notification_preferences" (id, "userId") +VALUES ('test_unit_onb_np1', 'test_unit_onb_cee'), ('test_unit_onb_np2', 'test_unit_onb_cnt'); + +INSERT INTO "Domain" (id, name, "createdAt", "updatedAt") +VALUES ('test_unit_onb_dom', 'Technology [test_unit_onb_dom]', NOW(), NOW()) ON CONFLICT (id) DO NOTHING; +INSERT INTO "SubDomain" (id, name, "domainId", "createdAt", "updatedAt") +VALUES ('test_unit_onb_sub', 'Flutter Dev', 'test_unit_onb_dom', NOW(), NOW()) ON CONFLICT (id) DO NOTHING; + +COMMIT; diff --git a/backend/prisma/sql/seed.d/03-profile.sql b/backend/prisma/sql/seed.d/03-profile.sql new file mode 100644 index 0000000..4c4e07d --- /dev/null +++ b/backend/prisma/sql/seed.d/03-profile.sql @@ -0,0 +1,25 @@ +-- Generated from prompts/testing/unit/03-profile.md by scripts/gen-dev-seed.sh. +-- Do not edit directly; edit the prompt and regenerate. + +BEGIN; + +-- Verified consultant user +INSERT INTO "users" (id, name, email, "emailVerified", role, "onboardingCompleted", bio, city, phone, "createdAt", "updatedAt") +VALUES ('test_unit_profile_u1', 'Profile Test User', 'test_unit_profile@test.com', true, 'CONSULTANT', true, 'Original bio', 'Mumbai', '+919000000001', NOW(), NOW()); + +INSERT INTO "accounts" (id, "userId", "accountId", "providerId", password, "createdAt", "updatedAt") +VALUES ('test_unit_profile_a1', 'test_unit_profile_u1', 'test_unit_profile_u1', 'credential', + '$2a$12$LJ3m4ys3Lf.GEHPmwH8Xh.q5Y6oN5K6YKD3lVz8mG0V5Z8Z8Z8Z', NOW(), NOW()); + +INSERT INTO "Domain" (id, name, "createdAt", "updatedAt") +VALUES ('test_unit_profile_dom', 'Technology [test_unit_profile_dom]', NOW(), NOW()) ON CONFLICT (id) DO NOTHING; + +INSERT INTO "ConsultantProfile" (id, "userId", "domainId", "scheduleType", "isVerified", "verificationStatus", "createdAt", "updatedAt") +VALUES ('test_unit_profile_cp1', 'test_unit_profile_u1', 'test_unit_profile_dom', 'WEEKLY', true, 'VERIFIED', NOW(), NOW()); +UPDATE "users" SET "consultantProfileId" = 'test_unit_profile_cp1' WHERE id = 'test_unit_profile_u1'; + +INSERT INTO "cookie_preferences" (id, "userId", essential, "consentGivenAt", "consentUpdatedAt") +VALUES ('test_unit_profile_ck1', 'test_unit_profile_u1', true, NOW(), NOW()); +INSERT INTO "notification_preferences" (id, "userId") VALUES ('test_unit_profile_np1', 'test_unit_profile_u1'); + +COMMIT; diff --git a/backend/prisma/sql/seed.d/04-verification.sql b/backend/prisma/sql/seed.d/04-verification.sql new file mode 100644 index 0000000..3fbbc95 --- /dev/null +++ b/backend/prisma/sql/seed.d/04-verification.sql @@ -0,0 +1,25 @@ +-- Generated from prompts/testing/unit/04-verification.md by scripts/gen-dev-seed.sh. +-- Do not edit directly; edit the prompt and regenerate. + +BEGIN; + +-- Consultant with PENDING_VERIFICATION status +INSERT INTO "users" (id, name, email, "emailVerified", role, "onboardingCompleted", "createdAt", "updatedAt") +VALUES ('test_unit_verif_u1', 'Verif Test User', 'test_unit_verif@test.com', true, 'CONSULTANT', true, NOW(), NOW()); + +INSERT INTO "accounts" (id, "userId", "accountId", "providerId", password, "createdAt", "updatedAt") +VALUES ('test_unit_verif_a1', 'test_unit_verif_u1', 'test_unit_verif_u1', 'credential', + '$2a$12$LJ3m4ys3Lf.GEHPmwH8Xh.q5Y6oN5K6YKD3lVz8mG0V5Z8Z8Z8Z', NOW(), NOW()); + +INSERT INTO "Domain" (id, name, "createdAt", "updatedAt") +VALUES ('test_unit_verif_dom', 'Technology [test_unit_verif_dom]', NOW(), NOW()) ON CONFLICT (id) DO NOTHING; + +INSERT INTO "ConsultantProfile" (id, "userId", "domainId", "scheduleType", "isVerified", "verificationStatus", "createdAt", "updatedAt") +VALUES ('test_unit_verif_cp1', 'test_unit_verif_u1', 'test_unit_verif_dom', 'WEEKLY', false, 'PENDING_VERIFICATION', NOW(), NOW()); +UPDATE "users" SET "consultantProfileId" = 'test_unit_verif_cp1' WHERE id = 'test_unit_verif_u1'; + +INSERT INTO "cookie_preferences" (id, "userId", essential, "consentGivenAt", "consentUpdatedAt") +VALUES ('test_unit_verif_ck1', 'test_unit_verif_u1', true, NOW(), NOW()); +INSERT INTO "notification_preferences" (id, "userId") VALUES ('test_unit_verif_np1', 'test_unit_verif_u1'); + +COMMIT; diff --git a/backend/prisma/sql/seed.d/05-plans.sql b/backend/prisma/sql/seed.d/05-plans.sql new file mode 100644 index 0000000..d83ff3d --- /dev/null +++ b/backend/prisma/sql/seed.d/05-plans.sql @@ -0,0 +1,25 @@ +-- Generated from prompts/testing/unit/05-plans.md by scripts/gen-dev-seed.sh. +-- Do not edit directly; edit the prompt and regenerate. + +BEGIN; + +-- Verified consultant +INSERT INTO "users" (id, name, email, "emailVerified", role, "onboardingCompleted", "createdAt", "updatedAt") +VALUES ('test_unit_plans_u1', 'Plans Test Consultant', 'test_unit_plans@test.com', true, 'CONSULTANT', true, NOW(), NOW()); + +INSERT INTO "accounts" (id, "userId", "accountId", "providerId", password, "createdAt", "updatedAt") +VALUES ('test_unit_plans_a1', 'test_unit_plans_u1', 'test_unit_plans_u1', 'credential', + '$2a$12$LJ3m4ys3Lf.GEHPmwH8Xh.q5Y6oN5K6YKD3lVz8mG0V5Z8Z8Z8Z', NOW(), NOW()); + +INSERT INTO "Domain" (id, name, "createdAt", "updatedAt") +VALUES ('test_unit_plans_dom', 'Technology [test_unit_plans_dom]', NOW(), NOW()) ON CONFLICT (id) DO NOTHING; + +INSERT INTO "ConsultantProfile" (id, "userId", "domainId", "scheduleType", "isVerified", "verificationStatus", "createdAt", "updatedAt") +VALUES ('test_unit_plans_cp1', 'test_unit_plans_u1', 'test_unit_plans_dom', 'WEEKLY', true, 'VERIFIED', NOW(), NOW()); +UPDATE "users" SET "consultantProfileId" = 'test_unit_plans_cp1' WHERE id = 'test_unit_plans_u1'; + +INSERT INTO "cookie_preferences" (id, "userId", essential, "consentGivenAt", "consentUpdatedAt") +VALUES ('test_unit_plans_ck1', 'test_unit_plans_u1', true, NOW(), NOW()); +INSERT INTO "notification_preferences" (id, "userId") VALUES ('test_unit_plans_np1', 'test_unit_plans_u1'); + +COMMIT; diff --git a/backend/prisma/sql/seed.d/06-slots.sql b/backend/prisma/sql/seed.d/06-slots.sql new file mode 100644 index 0000000..9c257c4 --- /dev/null +++ b/backend/prisma/sql/seed.d/06-slots.sql @@ -0,0 +1,25 @@ +-- Generated from prompts/testing/unit/06-slots.md by scripts/gen-dev-seed.sh. +-- Do not edit directly; edit the prompt and regenerate. + +BEGIN; + +-- Verified consultant +INSERT INTO "users" (id, name, email, "emailVerified", role, "onboardingCompleted", "createdAt", "updatedAt") +VALUES ('test_unit_slots_u1', 'Slots Test Consultant', 'test_unit_slots@test.com', true, 'CONSULTANT', true, NOW(), NOW()); + +INSERT INTO "accounts" (id, "userId", "accountId", "providerId", password, "createdAt", "updatedAt") +VALUES ('test_unit_slots_a1', 'test_unit_slots_u1', 'test_unit_slots_u1', 'credential', + '$2a$12$LJ3m4ys3Lf.GEHPmwH8Xh.q5Y6oN5K6YKD3lVz8mG0V5Z8Z8Z8Z', NOW(), NOW()); + +INSERT INTO "Domain" (id, name, "createdAt", "updatedAt") +VALUES ('test_unit_slots_dom', 'Technology [test_unit_slots_dom]', NOW(), NOW()) ON CONFLICT (id) DO NOTHING; + +INSERT INTO "ConsultantProfile" (id, "userId", "domainId", "scheduleType", "isVerified", "verificationStatus", "createdAt", "updatedAt") +VALUES ('test_unit_slots_cp1', 'test_unit_slots_u1', 'test_unit_slots_dom', 'WEEKLY', true, 'VERIFIED', NOW(), NOW()); +UPDATE "users" SET "consultantProfileId" = 'test_unit_slots_cp1' WHERE id = 'test_unit_slots_u1'; + +INSERT INTO "cookie_preferences" (id, "userId", essential, "consentGivenAt", "consentUpdatedAt") +VALUES ('test_unit_slots_ck1', 'test_unit_slots_u1', true, NOW(), NOW()); +INSERT INTO "notification_preferences" (id, "userId") VALUES ('test_unit_slots_np1', 'test_unit_slots_u1'); + +COMMIT; diff --git a/backend/prisma/sql/seed.d/07-explore.sql b/backend/prisma/sql/seed.d/07-explore.sql new file mode 100644 index 0000000..35ed43a --- /dev/null +++ b/backend/prisma/sql/seed.d/07-explore.sql @@ -0,0 +1,44 @@ +-- Generated from prompts/testing/unit/07-explore.md by scripts/gen-dev-seed.sh. +-- Do not edit directly; edit the prompt and regenerate. + +BEGIN; + +-- Consultant with plan + reviews +INSERT INTO "users" (id, name, email, "emailVerified", role, "onboardingCompleted", bio, city, "createdAt", "updatedAt") +VALUES + ('test_unit_explore_u1', 'Explore Consultant', 'test_unit_explore_cnt@test.com', true, 'CONSULTANT', true, 'Expert Flutter developer', 'Bangalore', NOW(), NOW()), + ('test_unit_explore_u2', 'Explore Consultee', 'test_unit_explore_cee@test.com', true, 'CONSULTEE', true, NULL, NULL, NOW(), NOW()); + +INSERT INTO "accounts" (id, "userId", "accountId", "providerId", password, "createdAt", "updatedAt") +VALUES + ('test_unit_explore_a1', 'test_unit_explore_u1', 'test_unit_explore_u1', 'credential', '$2a$12$LJ3m4ys3Lf.GEHPmwH8Xh.q5Y6oN5K6YKD3lVz8mG0V5Z8Z8Z8Z', NOW(), NOW()), + ('test_unit_explore_a2', 'test_unit_explore_u2', 'test_unit_explore_u2', 'credential', '$2a$12$LJ3m4ys3Lf.GEHPmwH8Xh.q5Y6oN5K6YKD3lVz8mG0V5Z8Z8Z8Z', NOW(), NOW()); + +INSERT INTO "Domain" (id, name, "createdAt", "updatedAt") +VALUES ('test_unit_explore_dom', 'Technology [test_unit_explore_dom]', NOW(), NOW()) ON CONFLICT (id) DO NOTHING; + +INSERT INTO "ConsultantProfile" (id, "userId", "domainId", "scheduleType", "isVerified", "verificationStatus", headline, rating, "createdAt", "updatedAt") +VALUES ('test_unit_explore_cp1', 'test_unit_explore_u1', 'test_unit_explore_dom', 'WEEKLY', true, 'VERIFIED', 'Flutter Expert', 4.5, NOW(), NOW()); +UPDATE "users" SET "consultantProfileId" = 'test_unit_explore_cp1' WHERE id = 'test_unit_explore_u1'; + +INSERT INTO "ConsulteeProfile" (id, "userId", "createdAt", "updatedAt") +VALUES ('test_unit_explore_cep1', 'test_unit_explore_u2', NOW(), NOW()); +UPDATE "users" SET "consulteeProfileId" = 'test_unit_explore_cep1' WHERE id = 'test_unit_explore_u2'; + +-- Consultation plan +INSERT INTO "ConsultationPlan" (id, title, description, "durationInHours", price, "consultantProfileId", "createdAt", "updatedAt") +VALUES ('test_unit_explore_plan1', 'Flutter Consultation', 'Learn Flutter basics', 1, 50000, 'test_unit_explore_cp1', NOW(), NOW()); + +-- Weekly availability slot +INSERT INTO "SlotOfAvailabilityWeekly" (id, "startDay", "startTimeUtc", "endDay", "endTimeUtc", "utcOffsetMinutes", "consultantProfileId", "createdAt", "updatedAt") +VALUES ('test_unit_explore_slot1', 'MONDAY', 600, 'MONDAY', 660, 330, 'test_unit_explore_cp1', NOW(), NOW()); + +-- Review +INSERT INTO "ConsultantReview" (id, rating, "reviewDescription", "consultantProfileId", "consulteeProfileId", "createdAt", "updatedAt") +VALUES ('test_unit_explore_rev1', 5, 'Excellent mentor, very helpful!', 'test_unit_explore_cp1', 'test_unit_explore_cep1', NOW(), NOW()); + +INSERT INTO "cookie_preferences" (id, "userId", essential, "consentGivenAt", "consentUpdatedAt") +VALUES ('test_unit_explore_ck1', 'test_unit_explore_u2', true, NOW(), NOW()); +INSERT INTO "notification_preferences" (id, "userId") VALUES ('test_unit_explore_np1', 'test_unit_explore_u2'); + +COMMIT; diff --git a/backend/prisma/sql/seed.d/08-booking.sql b/backend/prisma/sql/seed.d/08-booking.sql new file mode 100644 index 0000000..a1c9fcd --- /dev/null +++ b/backend/prisma/sql/seed.d/08-booking.sql @@ -0,0 +1,43 @@ +-- Generated from prompts/testing/unit/08-booking.md by scripts/gen-dev-seed.sh. +-- Do not edit directly; edit the prompt and regenerate. + +BEGIN; + +-- Consultant with plan + slots +INSERT INTO "users" (id, name, email, "emailVerified", role, "onboardingCompleted", "createdAt", "updatedAt") +VALUES + ('test_unit_booking_u1', 'Booking Consultant', 'test_unit_booking_cnt@test.com', true, 'CONSULTANT', true, NOW(), NOW()), + ('test_unit_booking_u2', 'Booking Consultee', 'test_unit_booking_cee@test.com', true, 'CONSULTEE', true, NOW(), NOW()); + +INSERT INTO "accounts" (id, "userId", "accountId", "providerId", password, "createdAt", "updatedAt") +VALUES + ('test_unit_booking_a1', 'test_unit_booking_u1', 'test_unit_booking_u1', 'credential', '$2a$12$LJ3m4ys3Lf.GEHPmwH8Xh.q5Y6oN5K6YKD3lVz8mG0V5Z8Z8Z8Z', NOW(), NOW()), + ('test_unit_booking_a2', 'test_unit_booking_u2', 'test_unit_booking_u2', 'credential', '$2a$12$LJ3m4ys3Lf.GEHPmwH8Xh.q5Y6oN5K6YKD3lVz8mG0V5Z8Z8Z8Z', NOW(), NOW()); + +INSERT INTO "Domain" (id, name, "createdAt", "updatedAt") +VALUES ('test_unit_booking_dom', 'Technology [test_unit_booking_dom]', NOW(), NOW()) ON CONFLICT (id) DO NOTHING; + +INSERT INTO "ConsultantProfile" (id, "userId", "domainId", "scheduleType", "isVerified", "verificationStatus", "createdAt", "updatedAt") +VALUES ('test_unit_booking_cp1', 'test_unit_booking_u1', 'test_unit_booking_dom', 'WEEKLY', true, 'VERIFIED', NOW(), NOW()); +UPDATE "users" SET "consultantProfileId" = 'test_unit_booking_cp1' WHERE id = 'test_unit_booking_u1'; + +INSERT INTO "ConsulteeProfile" (id, "userId", "createdAt", "updatedAt") +VALUES ('test_unit_booking_cep1', 'test_unit_booking_u2', NOW(), NOW()); +UPDATE "users" SET "consulteeProfileId" = 'test_unit_booking_cep1' WHERE id = 'test_unit_booking_u2'; + +-- Consultation plan +INSERT INTO "ConsultationPlan" (id, title, description, "durationInHours", price, "consultantProfileId", "createdAt", "updatedAt") +VALUES ('test_unit_booking_plan1', 'Booking Test Plan', '1-hour consultation', 1, 50000, 'test_unit_booking_cp1', NOW(), NOW()); + +-- Weekly availability slot (Monday 10:00-11:00 IST) +INSERT INTO "SlotOfAvailabilityWeekly" (id, "startDay", "startTimeUtc", "endDay", "endTimeUtc", "utcOffsetMinutes", "consultantProfileId", "createdAt", "updatedAt") +VALUES ('test_unit_booking_slot1', 'MONDAY', 270, 'MONDAY', 330, 330, 'test_unit_booking_cp1', NOW(), NOW()); + +INSERT INTO "cookie_preferences" (id, "userId", essential, "consentGivenAt", "consentUpdatedAt") +VALUES + ('test_unit_booking_ck1', 'test_unit_booking_u1', true, NOW(), NOW()), + ('test_unit_booking_ck2', 'test_unit_booking_u2', true, NOW(), NOW()); +INSERT INTO "notification_preferences" (id, "userId") +VALUES ('test_unit_booking_np1', 'test_unit_booking_u1'), ('test_unit_booking_np2', 'test_unit_booking_u2'); + +COMMIT; diff --git a/backend/prisma/sql/seed.d/09-checkout.sql b/backend/prisma/sql/seed.d/09-checkout.sql new file mode 100644 index 0000000..47d4965 --- /dev/null +++ b/backend/prisma/sql/seed.d/09-checkout.sql @@ -0,0 +1,47 @@ +-- Generated from prompts/testing/unit/09-checkout.md by scripts/gen-dev-seed.sh. +-- Do not edit directly; edit the prompt and regenerate. + +BEGIN; + +-- Consultant + consultee + booking in APPROVED_PENDING_PAYMENT state +INSERT INTO "users" (id, name, email, "emailVerified", role, "onboardingCompleted", "createdAt", "updatedAt") +VALUES + ('test_unit_checkout_u1', 'Checkout Consultant', 'test_unit_checkout_cnt@test.com', true, 'CONSULTANT', true, NOW(), NOW()), + ('test_unit_checkout_u2', 'Checkout Consultee', 'test_unit_checkout_cee@test.com', true, 'CONSULTEE', true, NOW(), NOW()); + +INSERT INTO "accounts" (id, "userId", "accountId", "providerId", password, "createdAt", "updatedAt") +VALUES + ('test_unit_checkout_a1', 'test_unit_checkout_u1', 'test_unit_checkout_u1', 'credential', '$2a$12$LJ3m4ys3Lf.GEHPmwH8Xh.q5Y6oN5K6YKD3lVz8mG0V5Z8Z8Z8Z', NOW(), NOW()), + ('test_unit_checkout_a2', 'test_unit_checkout_u2', 'test_unit_checkout_u2', 'credential', '$2a$12$LJ3m4ys3Lf.GEHPmwH8Xh.q5Y6oN5K6YKD3lVz8mG0V5Z8Z8Z8Z', NOW(), NOW()); + +INSERT INTO "Domain" (id, name, "createdAt", "updatedAt") +VALUES ('test_unit_checkout_dom', 'Technology [test_unit_checkout_dom]', NOW(), NOW()) ON CONFLICT (id) DO NOTHING; + +INSERT INTO "ConsultantProfile" (id, "userId", "domainId", "scheduleType", "isVerified", "verificationStatus", "createdAt", "updatedAt") +VALUES ('test_unit_checkout_cp1', 'test_unit_checkout_u1', 'test_unit_checkout_dom', 'WEEKLY', true, 'VERIFIED', NOW(), NOW()); +UPDATE "users" SET "consultantProfileId" = 'test_unit_checkout_cp1' WHERE id = 'test_unit_checkout_u1'; + +INSERT INTO "ConsulteeProfile" (id, "userId", "createdAt", "updatedAt") +VALUES ('test_unit_checkout_cep1', 'test_unit_checkout_u2', NOW(), NOW()); +UPDATE "users" SET "consulteeProfileId" = 'test_unit_checkout_cep1' WHERE id = 'test_unit_checkout_u2'; + +-- Consultation plan +INSERT INTO "ConsultationPlan" (id, title, description, "durationInHours", price, "consultantProfileId", "createdAt", "updatedAt") +VALUES ('test_unit_checkout_plan1', 'Checkout Test Plan', '1-hour session', 1, 75000, 'test_unit_checkout_cp1', NOW(), NOW()); + +-- Consultation in APPROVED_PENDING_PAYMENT status +INSERT INTO "Consultation" (id, "consultationPlanId", "requestStatus", "requestedById", "requestedAt", "createdAt", "updatedAt") +VALUES ('test_unit_checkout_con1', 'test_unit_checkout_plan1', 'APPROVED_PENDING_PAYMENT', 'test_unit_checkout_cep1', NOW(), NOW(), NOW()); + +-- Appointment for the consultation +INSERT INTO "Appointment" (id, "appointmentType", "consultationId", "createdAt", "updatedAt") +VALUES ('test_unit_checkout_apt1', 'CONSULTATION', 'test_unit_checkout_con1', NOW(), NOW()); + +INSERT INTO "cookie_preferences" (id, "userId", essential, "consentGivenAt", "consentUpdatedAt") +VALUES + ('test_unit_checkout_ck1', 'test_unit_checkout_u1', true, NOW(), NOW()), + ('test_unit_checkout_ck2', 'test_unit_checkout_u2', true, NOW(), NOW()); +INSERT INTO "notification_preferences" (id, "userId") +VALUES ('test_unit_checkout_np1', 'test_unit_checkout_u1'), ('test_unit_checkout_np2', 'test_unit_checkout_u2'); + +COMMIT; diff --git a/backend/prisma/sql/seed.d/10-trials.sql b/backend/prisma/sql/seed.d/10-trials.sql new file mode 100644 index 0000000..a1ecf92 --- /dev/null +++ b/backend/prisma/sql/seed.d/10-trials.sql @@ -0,0 +1,39 @@ +-- Generated from prompts/testing/unit/10-trials.md by scripts/gen-dev-seed.sh. +-- Do not edit directly; edit the prompt and regenerate. + +BEGIN; + +-- Consultant with subscription plan (freeTrialEnabled) + consultee +INSERT INTO "users" (id, name, email, "emailVerified", role, "onboardingCompleted", "createdAt", "updatedAt") +VALUES + ('test_unit_trials_u1', 'Trials Consultant', 'test_unit_trials_cnt@test.com', true, 'CONSULTANT', true, NOW(), NOW()), + ('test_unit_trials_u2', 'Trials Consultee', 'test_unit_trials_cee@test.com', true, 'CONSULTEE', true, NOW(), NOW()); + +INSERT INTO "accounts" (id, "userId", "accountId", "providerId", password, "createdAt", "updatedAt") +VALUES + ('test_unit_trials_a1', 'test_unit_trials_u1', 'test_unit_trials_u1', 'credential', '$2a$12$LJ3m4ys3Lf.GEHPmwH8Xh.q5Y6oN5K6YKD3lVz8mG0V5Z8Z8Z8Z', NOW(), NOW()), + ('test_unit_trials_a2', 'test_unit_trials_u2', 'test_unit_trials_u2', 'credential', '$2a$12$LJ3m4ys3Lf.GEHPmwH8Xh.q5Y6oN5K6YKD3lVz8mG0V5Z8Z8Z8Z', NOW(), NOW()); + +INSERT INTO "Domain" (id, name, "createdAt", "updatedAt") +VALUES ('test_unit_trials_dom', 'Technology [test_unit_trials_dom]', NOW(), NOW()) ON CONFLICT (id) DO NOTHING; + +INSERT INTO "ConsultantProfile" (id, "userId", "domainId", "scheduleType", "isVerified", "verificationStatus", "createdAt", "updatedAt") +VALUES ('test_unit_trials_cp1', 'test_unit_trials_u1', 'test_unit_trials_dom', 'WEEKLY', true, 'VERIFIED', NOW(), NOW()); +UPDATE "users" SET "consultantProfileId" = 'test_unit_trials_cp1' WHERE id = 'test_unit_trials_u1'; + +INSERT INTO "ConsulteeProfile" (id, "userId", "createdAt", "updatedAt") +VALUES ('test_unit_trials_cep1', 'test_unit_trials_u2', NOW(), NOW()); +UPDATE "users" SET "consulteeProfileId" = 'test_unit_trials_cep1' WHERE id = 'test_unit_trials_u2'; + +-- Subscription plan with free trial enabled +INSERT INTO "SubscriptionPlan" (id, title, description, "durationInMonths", price, "callsPerWeek", "sessionDurationInHours", "freeTrialEnabled", "freeTrialDurationMinutes", "consultantProfileId", "createdAt", "updatedAt") +VALUES ('test_unit_trials_sp1', 'Trial Subscription Plan', 'Monthly mentoring with free trial', 1, 200000, 1, 1, true, 30, 'test_unit_trials_cp1', NOW(), NOW()); + +INSERT INTO "cookie_preferences" (id, "userId", essential, "consentGivenAt", "consentUpdatedAt") +VALUES + ('test_unit_trials_ck1', 'test_unit_trials_u1', true, NOW(), NOW()), + ('test_unit_trials_ck2', 'test_unit_trials_u2', true, NOW(), NOW()); +INSERT INTO "notification_preferences" (id, "userId") +VALUES ('test_unit_trials_np1', 'test_unit_trials_u1'), ('test_unit_trials_np2', 'test_unit_trials_u2'); + +COMMIT; diff --git a/backend/prisma/sql/seed.d/11-waitlist.sql b/backend/prisma/sql/seed.d/11-waitlist.sql new file mode 100644 index 0000000..f087176 --- /dev/null +++ b/backend/prisma/sql/seed.d/11-waitlist.sql @@ -0,0 +1,44 @@ +-- Generated from prompts/testing/unit/11-waitlist.md by scripts/gen-dev-seed.sh. +-- Do not edit directly; edit the prompt and regenerate. + +BEGIN; + +-- Consultant + consultee + full webinar (maxParticipants=1, 1 enrolled) +INSERT INTO "users" (id, name, email, "emailVerified", role, "onboardingCompleted", "createdAt", "updatedAt") +VALUES + ('test_unit_waitlist_u1', 'Waitlist Consultant', 'test_unit_waitlist_cnt@test.com', true, 'CONSULTANT', true, NOW(), NOW()), + ('test_unit_waitlist_u2', 'Waitlist Consultee', 'test_unit_waitlist_cee@test.com', true, 'CONSULTEE', true, NOW(), NOW()), + ('test_unit_waitlist_u3', 'Waitlist Enrolled', 'test_unit_waitlist_enr@test.com', true, 'CONSULTEE', true, NOW(), NOW()); + +INSERT INTO "accounts" (id, "userId", "accountId", "providerId", password, "createdAt", "updatedAt") +VALUES + ('test_unit_waitlist_a1', 'test_unit_waitlist_u1', 'test_unit_waitlist_u1', 'credential', '$2a$12$LJ3m4ys3Lf.GEHPmwH8Xh.q5Y6oN5K6YKD3lVz8mG0V5Z8Z8Z8Z', NOW(), NOW()), + ('test_unit_waitlist_a2', 'test_unit_waitlist_u2', 'test_unit_waitlist_u2', 'credential', '$2a$12$LJ3m4ys3Lf.GEHPmwH8Xh.q5Y6oN5K6YKD3lVz8mG0V5Z8Z8Z8Z', NOW(), NOW()); + +INSERT INTO "Domain" (id, name, "createdAt", "updatedAt") +VALUES ('test_unit_waitlist_dom', 'Technology [test_unit_waitlist_dom]', NOW(), NOW()) ON CONFLICT (id) DO NOTHING; + +INSERT INTO "ConsultantProfile" (id, "userId", "domainId", "scheduleType", "isVerified", "verificationStatus", "createdAt", "updatedAt") +VALUES ('test_unit_waitlist_cp1', 'test_unit_waitlist_u1', 'test_unit_waitlist_dom', 'WEEKLY', true, 'VERIFIED', NOW(), NOW()); +UPDATE "users" SET "consultantProfileId" = 'test_unit_waitlist_cp1' WHERE id = 'test_unit_waitlist_u1'; + +INSERT INTO "ConsulteeProfile" (id, "userId", "createdAt", "updatedAt") +VALUES ('test_unit_waitlist_cep1', 'test_unit_waitlist_u2', NOW(), NOW()); +UPDATE "users" SET "consulteeProfileId" = 'test_unit_waitlist_cep1' WHERE id = 'test_unit_waitlist_u2'; + +-- Webinar plan with maxParticipants=1 (full capacity) +INSERT INTO "WebinarPlan" (id, title, description, price, "durationInHours", "maxParticipants", "consultantProfileId", "createdAt", "updatedAt") +VALUES ('test_unit_waitlist_wp1', 'Full Webinar', 'A sold-out webinar', 100000, 2, 1, 'test_unit_waitlist_cp1', NOW(), NOW()); + +-- Webinar instance +INSERT INTO "Webinar" (id, status, "webinarPlanId", "createdAt", "updatedAt") +VALUES ('test_unit_waitlist_w1', 'SCHEDULED', 'test_unit_waitlist_wp1', NOW(), NOW()); + +INSERT INTO "cookie_preferences" (id, "userId", essential, "consentGivenAt", "consentUpdatedAt") +VALUES + ('test_unit_waitlist_ck1', 'test_unit_waitlist_u1', true, NOW(), NOW()), + ('test_unit_waitlist_ck2', 'test_unit_waitlist_u2', true, NOW(), NOW()); +INSERT INTO "notification_preferences" (id, "userId") +VALUES ('test_unit_waitlist_np1', 'test_unit_waitlist_u1'), ('test_unit_waitlist_np2', 'test_unit_waitlist_u2'); + +COMMIT; diff --git a/backend/prisma/sql/seed.d/12-documents.sql b/backend/prisma/sql/seed.d/12-documents.sql new file mode 100644 index 0000000..456a906 --- /dev/null +++ b/backend/prisma/sql/seed.d/12-documents.sql @@ -0,0 +1,49 @@ +-- Generated from prompts/testing/unit/12-documents.md by scripts/gen-dev-seed.sh. +-- Do not edit directly; edit the prompt and regenerate. + +BEGIN; + +-- Consultant + consultee with an appointment +INSERT INTO "users" (id, name, email, "emailVerified", role, "onboardingCompleted", "createdAt", "updatedAt") +VALUES + ('test_unit_docs_u1', 'Docs Consultant', 'test_unit_docs_cnt@test.com', true, 'CONSULTANT', true, NOW(), NOW()), + ('test_unit_docs_u2', 'Docs Consultee', 'test_unit_docs_cee@test.com', true, 'CONSULTEE', true, NOW(), NOW()); + +INSERT INTO "accounts" (id, "userId", "accountId", "providerId", password, "createdAt", "updatedAt") +VALUES + ('test_unit_docs_a1', 'test_unit_docs_u1', 'test_unit_docs_u1', 'credential', '$2a$12$LJ3m4ys3Lf.GEHPmwH8Xh.q5Y6oN5K6YKD3lVz8mG0V5Z8Z8Z8Z', NOW(), NOW()), + ('test_unit_docs_a2', 'test_unit_docs_u2', 'test_unit_docs_u2', 'credential', '$2a$12$LJ3m4ys3Lf.GEHPmwH8Xh.q5Y6oN5K6YKD3lVz8mG0V5Z8Z8Z8Z', NOW(), NOW()); + +INSERT INTO "Domain" (id, name, "createdAt", "updatedAt") +VALUES ('test_unit_docs_dom', 'Technology [test_unit_docs_dom]', NOW(), NOW()) ON CONFLICT (id) DO NOTHING; + +INSERT INTO "ConsultantProfile" (id, "userId", "domainId", "scheduleType", "isVerified", "verificationStatus", "createdAt", "updatedAt") +VALUES ('test_unit_docs_cp1', 'test_unit_docs_u1', 'test_unit_docs_dom', 'WEEKLY', true, 'VERIFIED', NOW(), NOW()); +UPDATE "users" SET "consultantProfileId" = 'test_unit_docs_cp1' WHERE id = 'test_unit_docs_u1'; + +INSERT INTO "ConsulteeProfile" (id, "userId", "createdAt", "updatedAt") +VALUES ('test_unit_docs_cep1', 'test_unit_docs_u2', NOW(), NOW()); +UPDATE "users" SET "consulteeProfileId" = 'test_unit_docs_cep1' WHERE id = 'test_unit_docs_u2'; + +-- Consultation plan + consultation + appointment +INSERT INTO "ConsultationPlan" (id, title, "durationInHours", price, "consultantProfileId", "createdAt", "updatedAt") +VALUES ('test_unit_docs_plan1', 'Docs Test Plan', 1, 50000, 'test_unit_docs_cp1', NOW(), NOW()); + +INSERT INTO "Consultation" (id, "consultationPlanId", "requestStatus", "requestedById", "requestedAt", "createdAt", "updatedAt") +VALUES ('test_unit_docs_con1', 'test_unit_docs_plan1', 'SCHEDULED', 'test_unit_docs_cep1', NOW(), NOW(), NOW()); + +INSERT INTO "Appointment" (id, "appointmentType", "consultationId", "createdAt", "updatedAt") +VALUES ('test_unit_docs_apt1', 'CONSULTATION', 'test_unit_docs_con1', NOW(), NOW()); + +-- Seed an existing document +INSERT INTO "AppointmentDocument" (id, "fileName", "originalName", "fileSize", "mimeType", "fileUrl", "storagePath", description, "reviewStatus", "uploadedByRole", "appointmentId", "uploadedAt", "updatedAt") +VALUES ('test_unit_docs_doc1', 'resume.pdf', 'resume.pdf', 102400, 'application/pdf', 'https://example.com/resume.pdf', 'documents/test/resume.pdf', 'My resume for review', 'PENDING', 'CONSULTEE', 'test_unit_docs_apt1', NOW(), NOW()); + +INSERT INTO "cookie_preferences" (id, "userId", essential, "consentGivenAt", "consentUpdatedAt") +VALUES + ('test_unit_docs_ck1', 'test_unit_docs_u1', true, NOW(), NOW()), + ('test_unit_docs_ck2', 'test_unit_docs_u2', true, NOW(), NOW()); +INSERT INTO "notification_preferences" (id, "userId") +VALUES ('test_unit_docs_np1', 'test_unit_docs_u1'), ('test_unit_docs_np2', 'test_unit_docs_u2'); + +COMMIT; diff --git a/backend/prisma/sql/seed.d/13-chat.sql b/backend/prisma/sql/seed.d/13-chat.sql new file mode 100644 index 0000000..c662c1d --- /dev/null +++ b/backend/prisma/sql/seed.d/13-chat.sql @@ -0,0 +1,22 @@ +-- Generated from prompts/testing/unit/13-chat.md by scripts/gen-dev-seed.sh. +-- Do not edit directly; edit the prompt and regenerate. + +BEGIN; + +-- Simple user for chat UI test +INSERT INTO "users" (id, name, email, "emailVerified", role, "onboardingCompleted", "createdAt", "updatedAt") +VALUES ('test_unit_chat_u1', 'Chat Test User', 'test_unit_chat@test.com', true, 'CONSULTEE', true, NOW(), NOW()); + +INSERT INTO "accounts" (id, "userId", "accountId", "providerId", password, "createdAt", "updatedAt") +VALUES ('test_unit_chat_a1', 'test_unit_chat_u1', 'test_unit_chat_u1', 'credential', + '$2a$12$LJ3m4ys3Lf.GEHPmwH8Xh.q5Y6oN5K6YKD3lVz8mG0V5Z8Z8Z8Z', NOW(), NOW()); + +INSERT INTO "ConsulteeProfile" (id, "userId", "createdAt", "updatedAt") +VALUES ('test_unit_chat_cep1', 'test_unit_chat_u1', NOW(), NOW()); +UPDATE "users" SET "consulteeProfileId" = 'test_unit_chat_cep1' WHERE id = 'test_unit_chat_u1'; + +INSERT INTO "cookie_preferences" (id, "userId", essential, "consentGivenAt", "consentUpdatedAt") +VALUES ('test_unit_chat_ck1', 'test_unit_chat_u1', true, NOW(), NOW()); +INSERT INTO "notification_preferences" (id, "userId") VALUES ('test_unit_chat_np1', 'test_unit_chat_u1'); + +COMMIT; diff --git a/backend/prisma/sql/seed.d/14-referrals.sql b/backend/prisma/sql/seed.d/14-referrals.sql new file mode 100644 index 0000000..ef7186d --- /dev/null +++ b/backend/prisma/sql/seed.d/14-referrals.sql @@ -0,0 +1,26 @@ +-- Generated from prompts/testing/unit/14-referrals.md by scripts/gen-dev-seed.sh. +-- Do not edit directly; edit the prompt and regenerate. + +BEGIN; + +-- User with referral code +INSERT INTO "users" (id, name, email, "emailVerified", role, "onboardingCompleted", "createdAt", "updatedAt") +VALUES ('test_unit_referrals_u1', 'Referrals User', 'test_unit_referrals@test.com', true, 'CONSULTEE', true, NOW(), NOW()); + +INSERT INTO "accounts" (id, "userId", "accountId", "providerId", password, "createdAt", "updatedAt") +VALUES ('test_unit_referrals_a1', 'test_unit_referrals_u1', 'test_unit_referrals_u1', 'credential', + '$2a$12$LJ3m4ys3Lf.GEHPmwH8Xh.q5Y6oN5K6YKD3lVz8mG0V5Z8Z8Z8Z', NOW(), NOW()); + +INSERT INTO "ConsulteeProfile" (id, "userId", "createdAt", "updatedAt") +VALUES ('test_unit_referrals_cep1', 'test_unit_referrals_u1', NOW(), NOW()); +UPDATE "users" SET "consulteeProfileId" = 'test_unit_referrals_cep1' WHERE id = 'test_unit_referrals_u1'; + +-- Pre-existing referral code +INSERT INTO "ReferralCode" (id, "userId", code, "referrerReward", "refereeReward", "isActive", "createdAt", "updatedAt") +VALUES ('test_unit_referrals_rc1', 'test_unit_referrals_u1', 'TESTREF123', 10000, 5000, true, NOW(), NOW()); + +INSERT INTO "cookie_preferences" (id, "userId", essential, "consentGivenAt", "consentUpdatedAt") +VALUES ('test_unit_referrals_ck1', 'test_unit_referrals_u1', true, NOW(), NOW()); +INSERT INTO "notification_preferences" (id, "userId") VALUES ('test_unit_referrals_np1', 'test_unit_referrals_u1'); + +COMMIT; diff --git a/backend/prisma/sql/seed.d/15-reviews.sql b/backend/prisma/sql/seed.d/15-reviews.sql new file mode 100644 index 0000000..4acf03e --- /dev/null +++ b/backend/prisma/sql/seed.d/15-reviews.sql @@ -0,0 +1,45 @@ +-- Generated from prompts/testing/unit/15-reviews.md by scripts/gen-dev-seed.sh. +-- Do not edit directly; edit the prompt and regenerate. + +BEGIN; + +-- Consultant + consultee with completed appointment +INSERT INTO "users" (id, name, email, "emailVerified", role, "onboardingCompleted", "createdAt", "updatedAt") +VALUES + ('test_unit_reviews_u1', 'Reviews Consultant', 'test_unit_reviews_cnt@test.com', true, 'CONSULTANT', true, NOW(), NOW()), + ('test_unit_reviews_u2', 'Reviews Consultee', 'test_unit_reviews_cee@test.com', true, 'CONSULTEE', true, NOW(), NOW()); + +INSERT INTO "accounts" (id, "userId", "accountId", "providerId", password, "createdAt", "updatedAt") +VALUES + ('test_unit_reviews_a1', 'test_unit_reviews_u1', 'test_unit_reviews_u1', 'credential', '$2a$12$LJ3m4ys3Lf.GEHPmwH8Xh.q5Y6oN5K6YKD3lVz8mG0V5Z8Z8Z8Z', NOW(), NOW()), + ('test_unit_reviews_a2', 'test_unit_reviews_u2', 'test_unit_reviews_u2', 'credential', '$2a$12$LJ3m4ys3Lf.GEHPmwH8Xh.q5Y6oN5K6YKD3lVz8mG0V5Z8Z8Z8Z', NOW(), NOW()); + +INSERT INTO "Domain" (id, name, "createdAt", "updatedAt") +VALUES ('test_unit_reviews_dom', 'Technology [test_unit_reviews_dom]', NOW(), NOW()) ON CONFLICT (id) DO NOTHING; + +INSERT INTO "ConsultantProfile" (id, "userId", "domainId", "scheduleType", "isVerified", "verificationStatus", rating, "createdAt", "updatedAt") +VALUES ('test_unit_reviews_cp1', 'test_unit_reviews_u1', 'test_unit_reviews_dom', 'WEEKLY', true, 'VERIFIED', 0, NOW(), NOW()); +UPDATE "users" SET "consultantProfileId" = 'test_unit_reviews_cp1' WHERE id = 'test_unit_reviews_u1'; + +INSERT INTO "ConsulteeProfile" (id, "userId", "createdAt", "updatedAt") +VALUES ('test_unit_reviews_cep1', 'test_unit_reviews_u2', NOW(), NOW()); +UPDATE "users" SET "consulteeProfileId" = 'test_unit_reviews_cep1' WHERE id = 'test_unit_reviews_u2'; + +-- Completed consultation + appointment +INSERT INTO "ConsultationPlan" (id, title, "durationInHours", price, "consultantProfileId", "createdAt", "updatedAt") +VALUES ('test_unit_reviews_plan1', 'Review Test Plan', 1, 50000, 'test_unit_reviews_cp1', NOW(), NOW()); + +INSERT INTO "Consultation" (id, "consultationPlanId", "requestStatus", "requestedById", "requestedAt", "createdAt", "updatedAt") +VALUES ('test_unit_reviews_con1', 'test_unit_reviews_plan1', 'COMPLETED', 'test_unit_reviews_cep1', NOW(), NOW(), NOW()); + +INSERT INTO "Appointment" (id, "appointmentType", "consultationId", "createdAt", "updatedAt") +VALUES ('test_unit_reviews_apt1', 'CONSULTATION', 'test_unit_reviews_con1', NOW(), NOW()); + +INSERT INTO "cookie_preferences" (id, "userId", essential, "consentGivenAt", "consentUpdatedAt") +VALUES + ('test_unit_reviews_ck1', 'test_unit_reviews_u1', true, NOW(), NOW()), + ('test_unit_reviews_ck2', 'test_unit_reviews_u2', true, NOW(), NOW()); +INSERT INTO "notification_preferences" (id, "userId") +VALUES ('test_unit_reviews_np1', 'test_unit_reviews_u1'), ('test_unit_reviews_np2', 'test_unit_reviews_u2'); + +COMMIT; diff --git a/backend/prisma/sql/seed.d/16-support.sql b/backend/prisma/sql/seed.d/16-support.sql new file mode 100644 index 0000000..904be1f --- /dev/null +++ b/backend/prisma/sql/seed.d/16-support.sql @@ -0,0 +1,26 @@ +-- Generated from prompts/testing/unit/16-support.md by scripts/gen-dev-seed.sh. +-- Do not edit directly; edit the prompt and regenerate. + +BEGIN; + +-- User with existing ticket +INSERT INTO "users" (id, name, email, "emailVerified", role, "onboardingCompleted", "createdAt", "updatedAt") +VALUES ('test_unit_support_u1', 'Support User', 'test_unit_support@test.com', true, 'CONSULTEE', true, NOW(), NOW()); + +INSERT INTO "accounts" (id, "userId", "accountId", "providerId", password, "createdAt", "updatedAt") +VALUES ('test_unit_support_a1', 'test_unit_support_u1', 'test_unit_support_u1', 'credential', + '$2a$12$LJ3m4ys3Lf.GEHPmwH8Xh.q5Y6oN5K6YKD3lVz8mG0V5Z8Z8Z8Z', NOW(), NOW()); + +INSERT INTO "ConsulteeProfile" (id, "userId", "createdAt", "updatedAt") +VALUES ('test_unit_support_cep1', 'test_unit_support_u1', NOW(), NOW()); +UPDATE "users" SET "consulteeProfileId" = 'test_unit_support_cep1' WHERE id = 'test_unit_support_u1'; + +-- Existing support ticket +INSERT INTO "support_tickets" (id, title, description, priority, status, category, "userId", "createdAt", "updatedAt") +VALUES ('test_unit_support_t1', 'Payment not received', 'I completed payment but status still shows pending', 'HIGH', 'OPEN', 'PAYMENT_FAILED', 'test_unit_support_u1', NOW(), NOW()); + +INSERT INTO "cookie_preferences" (id, "userId", essential, "consentGivenAt", "consentUpdatedAt") +VALUES ('test_unit_support_ck1', 'test_unit_support_u1', true, NOW(), NOW()); +INSERT INTO "notification_preferences" (id, "userId") VALUES ('test_unit_support_np1', 'test_unit_support_u1'); + +COMMIT; diff --git a/backend/prisma/sql/seed.d/17-feedback.sql b/backend/prisma/sql/seed.d/17-feedback.sql new file mode 100644 index 0000000..f38aa16 --- /dev/null +++ b/backend/prisma/sql/seed.d/17-feedback.sql @@ -0,0 +1,21 @@ +-- Generated from prompts/testing/unit/17-feedback.md by scripts/gen-dev-seed.sh. +-- Do not edit directly; edit the prompt and regenerate. + +BEGIN; + +INSERT INTO "users" (id, name, email, "emailVerified", role, "onboardingCompleted", "createdAt", "updatedAt") +VALUES ('test_unit_feedback_u1', 'Feedback User', 'test_unit_feedback@test.com', true, 'CONSULTEE', true, NOW(), NOW()); + +INSERT INTO "accounts" (id, "userId", "accountId", "providerId", password, "createdAt", "updatedAt") +VALUES ('test_unit_feedback_a1', 'test_unit_feedback_u1', 'test_unit_feedback_u1', 'credential', + '$2a$12$LJ3m4ys3Lf.GEHPmwH8Xh.q5Y6oN5K6YKD3lVz8mG0V5Z8Z8Z8Z', NOW(), NOW()); + +INSERT INTO "ConsulteeProfile" (id, "userId", "createdAt", "updatedAt") +VALUES ('test_unit_feedback_cep1', 'test_unit_feedback_u1', NOW(), NOW()); +UPDATE "users" SET "consulteeProfileId" = 'test_unit_feedback_cep1' WHERE id = 'test_unit_feedback_u1'; + +INSERT INTO "cookie_preferences" (id, "userId", essential, "consentGivenAt", "consentUpdatedAt") +VALUES ('test_unit_feedback_ck1', 'test_unit_feedback_u1', true, NOW(), NOW()); +INSERT INTO "notification_preferences" (id, "userId") VALUES ('test_unit_feedback_np1', 'test_unit_feedback_u1'); + +COMMIT; diff --git a/backend/prisma/sql/seed.d/18-payout.sql b/backend/prisma/sql/seed.d/18-payout.sql new file mode 100644 index 0000000..8626c3c --- /dev/null +++ b/backend/prisma/sql/seed.d/18-payout.sql @@ -0,0 +1,25 @@ +-- Generated from prompts/testing/unit/18-payout.md by scripts/gen-dev-seed.sh. +-- Do not edit directly; edit the prompt and regenerate. + +BEGIN; + +-- Verified consultant +INSERT INTO "users" (id, name, email, "emailVerified", role, "onboardingCompleted", "createdAt", "updatedAt") +VALUES ('test_unit_payout_u1', 'Payout Consultant', 'test_unit_payout@test.com', true, 'CONSULTANT', true, NOW(), NOW()); + +INSERT INTO "accounts" (id, "userId", "accountId", "providerId", password, "createdAt", "updatedAt") +VALUES ('test_unit_payout_a1', 'test_unit_payout_u1', 'test_unit_payout_u1', 'credential', + '$2a$12$LJ3m4ys3Lf.GEHPmwH8Xh.q5Y6oN5K6YKD3lVz8mG0V5Z8Z8Z8Z', NOW(), NOW()); + +INSERT INTO "Domain" (id, name, "createdAt", "updatedAt") +VALUES ('test_unit_payout_dom', 'Technology [test_unit_payout_dom]', NOW(), NOW()) ON CONFLICT (id) DO NOTHING; + +INSERT INTO "ConsultantProfile" (id, "userId", "domainId", "scheduleType", "isVerified", "verificationStatus", "createdAt", "updatedAt") +VALUES ('test_unit_payout_cp1', 'test_unit_payout_u1', 'test_unit_payout_dom', 'WEEKLY', true, 'VERIFIED', NOW(), NOW()); +UPDATE "users" SET "consultantProfileId" = 'test_unit_payout_cp1' WHERE id = 'test_unit_payout_u1'; + +INSERT INTO "cookie_preferences" (id, "userId", essential, "consentGivenAt", "consentUpdatedAt") +VALUES ('test_unit_payout_ck1', 'test_unit_payout_u1', true, NOW(), NOW()); +INSERT INTO "notification_preferences" (id, "userId") VALUES ('test_unit_payout_np1', 'test_unit_payout_u1'); + +COMMIT; diff --git a/backend/prisma/sql/seed.d/19-tax.sql b/backend/prisma/sql/seed.d/19-tax.sql new file mode 100644 index 0000000..bbc45c6 --- /dev/null +++ b/backend/prisma/sql/seed.d/19-tax.sql @@ -0,0 +1,25 @@ +-- Generated from prompts/testing/unit/19-tax.md by scripts/gen-dev-seed.sh. +-- Do not edit directly; edit the prompt and regenerate. + +BEGIN; + +-- Verified consultant +INSERT INTO "users" (id, name, email, "emailVerified", role, "onboardingCompleted", "createdAt", "updatedAt") +VALUES ('test_unit_tax_u1', 'Tax Consultant', 'test_unit_tax@test.com', true, 'CONSULTANT', true, NOW(), NOW()); + +INSERT INTO "accounts" (id, "userId", "accountId", "providerId", password, "createdAt", "updatedAt") +VALUES ('test_unit_tax_a1', 'test_unit_tax_u1', 'test_unit_tax_u1', 'credential', + '$2a$12$LJ3m4ys3Lf.GEHPmwH8Xh.q5Y6oN5K6YKD3lVz8mG0V5Z8Z8Z8Z', NOW(), NOW()); + +INSERT INTO "Domain" (id, name, "createdAt", "updatedAt") +VALUES ('test_unit_tax_dom', 'Technology [test_unit_tax_dom]', NOW(), NOW()) ON CONFLICT (id) DO NOTHING; + +INSERT INTO "ConsultantProfile" (id, "userId", "domainId", "scheduleType", "isVerified", "verificationStatus", "createdAt", "updatedAt") +VALUES ('test_unit_tax_cp1', 'test_unit_tax_u1', 'test_unit_tax_dom', 'WEEKLY', true, 'VERIFIED', NOW(), NOW()); +UPDATE "users" SET "consultantProfileId" = 'test_unit_tax_cp1' WHERE id = 'test_unit_tax_u1'; + +INSERT INTO "cookie_preferences" (id, "userId", essential, "consentGivenAt", "consentUpdatedAt") +VALUES ('test_unit_tax_ck1', 'test_unit_tax_u1', true, NOW(), NOW()); +INSERT INTO "notification_preferences" (id, "userId") VALUES ('test_unit_tax_np1', 'test_unit_tax_u1'); + +COMMIT; diff --git a/backend/prisma/sql/seed.d/20-staff.sql b/backend/prisma/sql/seed.d/20-staff.sql new file mode 100644 index 0000000..7600fdc --- /dev/null +++ b/backend/prisma/sql/seed.d/20-staff.sql @@ -0,0 +1,56 @@ +-- Generated from prompts/testing/unit/20-staff.md by scripts/gen-dev-seed.sh. +-- Do not edit directly; edit the prompt and regenerate. + +BEGIN; + +-- Staff user +INSERT INTO "users" (id, name, email, "emailVerified", role, "onboardingCompleted", "createdAt", "updatedAt") +VALUES ('test_unit_staff_u1', 'Staff User', 'test_unit_staff@test.com', true, 'STAFF', true, NOW(), NOW()); + +INSERT INTO "accounts" (id, "userId", "accountId", "providerId", password, "createdAt", "updatedAt") +VALUES ('test_unit_staff_a1', 'test_unit_staff_u1', 'test_unit_staff_u1', 'credential', + '$2a$12$LJ3m4ys3Lf.GEHPmwH8Xh.q5Y6oN5K6YKD3lVz8mG0V5Z8Z8Z8Z', NOW(), NOW()); + +INSERT INTO "StaffProfile" (id, "userId", department, position, "createdAt", "updatedAt") +VALUES ('test_unit_staff_sp1', 'test_unit_staff_u1', 'Support', 'Manager', NOW(), NOW()); +UPDATE "users" SET "staffProfileId" = 'test_unit_staff_sp1' WHERE id = 'test_unit_staff_u1'; + +-- Consultant with pending verification +INSERT INTO "users" (id, name, email, "emailVerified", role, "onboardingCompleted", "createdAt", "updatedAt") +VALUES ('test_unit_staff_u2', 'Pending Consultant', 'test_unit_staff_cnt@test.com', true, 'CONSULTANT', true, NOW(), NOW()); + +INSERT INTO "Domain" (id, name, "createdAt", "updatedAt") +VALUES ('test_unit_staff_dom', 'Technology [test_unit_staff_dom]', NOW(), NOW()) ON CONFLICT (id) DO NOTHING; + +INSERT INTO "ConsultantProfile" (id, "userId", "domainId", "scheduleType", "isVerified", "verificationStatus", "createdAt", "updatedAt") +VALUES ('test_unit_staff_cp1', 'test_unit_staff_u2', 'test_unit_staff_dom', 'WEEKLY', false, 'PENDING_VERIFICATION', NOW(), NOW()); +UPDATE "users" SET "consultantProfileId" = 'test_unit_staff_cp1' WHERE id = 'test_unit_staff_u2'; + +-- Pending verification request +INSERT INTO "ConsultantProfileVerification" (id, status, "consultantProfileId", "submittedAt", notes, "createdAt", "updatedAt") +VALUES ('test_unit_staff_pv1', 'PENDING', 'test_unit_staff_cp1', NOW(), 'Please verify my credentials', NOW(), NOW()); + +-- User with open support ticket +INSERT INTO "users" (id, name, email, "emailVerified", role, "onboardingCompleted", "createdAt", "updatedAt") +VALUES ('test_unit_staff_u3', 'Ticket User', 'test_unit_staff_tkt@test.com', true, 'CONSULTEE', true, NOW(), NOW()); + +INSERT INTO "ConsulteeProfile" (id, "userId", "createdAt", "updatedAt") +VALUES ('test_unit_staff_cep1', 'test_unit_staff_u3', NOW(), NOW()); +UPDATE "users" SET "consulteeProfileId" = 'test_unit_staff_cep1' WHERE id = 'test_unit_staff_u3'; + +INSERT INTO "support_tickets" (id, title, description, priority, status, "userId", "createdAt", "updatedAt") +VALUES ('test_unit_staff_t1', 'Staff Test Ticket', 'Need help with booking issue', 'HIGH', 'OPEN', 'test_unit_staff_u3', NOW(), NOW()); + +-- Pending feedback +INSERT INTO "feedbacks" (id, title, description, rating, status, "userId", "createdAt", "updatedAt") +VALUES ('test_unit_staff_fb1', 'Staff Test Feedback', 'Great platform but needs improvement', 4, 'PENDING', 'test_unit_staff_u3', NOW(), NOW()); + +INSERT INTO "cookie_preferences" (id, "userId", essential, "consentGivenAt", "consentUpdatedAt") +VALUES + ('test_unit_staff_ck1', 'test_unit_staff_u1', true, NOW(), NOW()), + ('test_unit_staff_ck2', 'test_unit_staff_u2', true, NOW(), NOW()), + ('test_unit_staff_ck3', 'test_unit_staff_u3', true, NOW(), NOW()); +INSERT INTO "notification_preferences" (id, "userId") +VALUES ('test_unit_staff_np1', 'test_unit_staff_u1'), ('test_unit_staff_np2', 'test_unit_staff_u2'), ('test_unit_staff_np3', 'test_unit_staff_u3'); + +COMMIT; diff --git a/backend/prisma/sql/seed.d/21-announcements.sql b/backend/prisma/sql/seed.d/21-announcements.sql new file mode 100644 index 0000000..aad08eb --- /dev/null +++ b/backend/prisma/sql/seed.d/21-announcements.sql @@ -0,0 +1,26 @@ +-- Generated from prompts/testing/unit/21-announcements.md by scripts/gen-dev-seed.sh. +-- Do not edit directly; edit the prompt and regenerate. + +BEGIN; + +-- Active announcement +INSERT INTO "announcements" (id, title, content, "isActive", "startDate", "endDate", "backgroundColor", "textColor", "linkUrl", "linkText", "createdBy", "createdAt", "updatedAt") +VALUES ('test_unit_announce_1', 'Platform Update', 'We are rolling out new features this week! Check out the new booking flow.', true, NOW() - INTERVAL '1 day', NOW() + INTERVAL '7 days', '#1E40AF', '#FFFFFF', 'https://familiarise.com/updates', 'Learn More', 'system', NOW(), NOW()); + +-- User to view the announcement +INSERT INTO "users" (id, name, email, "emailVerified", role, "onboardingCompleted", "createdAt", "updatedAt") +VALUES ('test_unit_announce_u1', 'Announce User', 'test_unit_announce@test.com', true, 'CONSULTEE', true, NOW(), NOW()); + +INSERT INTO "accounts" (id, "userId", "accountId", "providerId", password, "createdAt", "updatedAt") +VALUES ('test_unit_announce_a1', 'test_unit_announce_u1', 'test_unit_announce_u1', 'credential', + '$2a$12$LJ3m4ys3Lf.GEHPmwH8Xh.q5Y6oN5K6YKD3lVz8mG0V5Z8Z8Z8Z', NOW(), NOW()); + +INSERT INTO "ConsulteeProfile" (id, "userId", "createdAt", "updatedAt") +VALUES ('test_unit_announce_cep1', 'test_unit_announce_u1', NOW(), NOW()); +UPDATE "users" SET "consulteeProfileId" = 'test_unit_announce_cep1' WHERE id = 'test_unit_announce_u1'; + +INSERT INTO "cookie_preferences" (id, "userId", essential, "consentGivenAt", "consentUpdatedAt") +VALUES ('test_unit_announce_ck1', 'test_unit_announce_u1', true, NOW(), NOW()); +INSERT INTO "notification_preferences" (id, "userId") VALUES ('test_unit_announce_np1', 'test_unit_announce_u1'); + +COMMIT; diff --git a/backend/prisma/sql/seed.d/22-collaborations.sql b/backend/prisma/sql/seed.d/22-collaborations.sql new file mode 100644 index 0000000..da71d2c --- /dev/null +++ b/backend/prisma/sql/seed.d/22-collaborations.sql @@ -0,0 +1,42 @@ +-- Generated from prompts/testing/unit/22-collaborations.md by scripts/gen-dev-seed.sh. +-- Do not edit directly; edit the prompt and regenerate. + +BEGIN; + +-- Two consultants: one invites the other to collaborate on a webinar +INSERT INTO "users" (id, name, email, "emailVerified", role, "onboardingCompleted", "createdAt", "updatedAt") +VALUES + ('test_unit_collab_u1', 'Collab Host', 'test_unit_collab_host@test.com', true, 'CONSULTANT', true, NOW(), NOW()), + ('test_unit_collab_u2', 'Collab Invited', 'test_unit_collab_inv@test.com', true, 'CONSULTANT', true, NOW(), NOW()); + +INSERT INTO "accounts" (id, "userId", "accountId", "providerId", password, "createdAt", "updatedAt") +VALUES + ('test_unit_collab_a1', 'test_unit_collab_u1', 'test_unit_collab_u1', 'credential', '$2a$12$LJ3m4ys3Lf.GEHPmwH8Xh.q5Y6oN5K6YKD3lVz8mG0V5Z8Z8Z8Z', NOW(), NOW()), + ('test_unit_collab_a2', 'test_unit_collab_u2', 'test_unit_collab_u2', 'credential', '$2a$12$LJ3m4ys3Lf.GEHPmwH8Xh.q5Y6oN5K6YKD3lVz8mG0V5Z8Z8Z8Z', NOW(), NOW()); + +INSERT INTO "Domain" (id, name, "createdAt", "updatedAt") +VALUES ('test_unit_collab_dom', 'Technology [test_unit_collab_dom]', NOW(), NOW()) ON CONFLICT (id) DO NOTHING; + +INSERT INTO "ConsultantProfile" (id, "userId", "domainId", "scheduleType", "isVerified", "verificationStatus", "createdAt", "updatedAt") +VALUES + ('test_unit_collab_cp1', 'test_unit_collab_u1', 'test_unit_collab_dom', 'WEEKLY', true, 'VERIFIED', NOW(), NOW()), + ('test_unit_collab_cp2', 'test_unit_collab_u2', 'test_unit_collab_dom', 'WEEKLY', true, 'VERIFIED', NOW(), NOW()); +UPDATE "users" SET "consultantProfileId" = 'test_unit_collab_cp1' WHERE id = 'test_unit_collab_u1'; +UPDATE "users" SET "consultantProfileId" = 'test_unit_collab_cp2' WHERE id = 'test_unit_collab_u2'; + +-- Webinar plan owned by host +INSERT INTO "WebinarPlan" (id, title, description, price, "durationInHours", "maxParticipants", "consultantProfileId", "createdAt", "updatedAt") +VALUES ('test_unit_collab_wp1', 'Collab Webinar', 'A joint webinar on Flutter', 100000, 2, 50, 'test_unit_collab_cp1', NOW(), NOW()); + +-- Collaboration invitation (PENDING) +INSERT INTO "WebinarCollaborator" (id, "consultantProfileId", "webinarPlanId", role, "revenueSharePercentage", status, "invitedById", "createdAt", "updatedAt") +VALUES ('test_unit_collab_wc1', 'test_unit_collab_cp2', 'test_unit_collab_wp1', 'CO_HOST', 30.0, 'PENDING', 'test_unit_collab_cp1', NOW(), NOW()); + +INSERT INTO "cookie_preferences" (id, "userId", essential, "consentGivenAt", "consentUpdatedAt") +VALUES + ('test_unit_collab_ck1', 'test_unit_collab_u1', true, NOW(), NOW()), + ('test_unit_collab_ck2', 'test_unit_collab_u2', true, NOW(), NOW()); +INSERT INTO "notification_preferences" (id, "userId") +VALUES ('test_unit_collab_np1', 'test_unit_collab_u1'), ('test_unit_collab_np2', 'test_unit_collab_u2'); + +COMMIT; diff --git a/backend/prisma/sql/seed.d/23-dashboard.sql b/backend/prisma/sql/seed.d/23-dashboard.sql new file mode 100644 index 0000000..ee27bd2 --- /dev/null +++ b/backend/prisma/sql/seed.d/23-dashboard.sql @@ -0,0 +1,58 @@ +-- Generated from prompts/testing/unit/23-dashboard.md by scripts/gen-dev-seed.sh. +-- Do not edit directly; edit the prompt and regenerate. + +BEGIN; + +-- Consultant with stats/earnings/pending requests +INSERT INTO "users" (id, name, email, "emailVerified", role, "onboardingCompleted", "createdAt", "updatedAt") +VALUES + ('test_unit_dash_u1', 'Dash Consultant', 'test_unit_dash_cnt@test.com', true, 'CONSULTANT', true, NOW(), NOW()), + ('test_unit_dash_u2', 'Dash Consultee', 'test_unit_dash_cee@test.com', true, 'CONSULTEE', true, NOW(), NOW()); + +INSERT INTO "accounts" (id, "userId", "accountId", "providerId", password, "createdAt", "updatedAt") +VALUES + ('test_unit_dash_a1', 'test_unit_dash_u1', 'test_unit_dash_u1', 'credential', '$2a$12$LJ3m4ys3Lf.GEHPmwH8Xh.q5Y6oN5K6YKD3lVz8mG0V5Z8Z8Z8Z', NOW(), NOW()), + ('test_unit_dash_a2', 'test_unit_dash_u2', 'test_unit_dash_u2', 'credential', '$2a$12$LJ3m4ys3Lf.GEHPmwH8Xh.q5Y6oN5K6YKD3lVz8mG0V5Z8Z8Z8Z', NOW(), NOW()); + +INSERT INTO "Domain" (id, name, "createdAt", "updatedAt") +VALUES ('test_unit_dash_dom', 'Technology [test_unit_dash_dom]', NOW(), NOW()) ON CONFLICT (id) DO NOTHING; + +INSERT INTO "ConsultantProfile" (id, "userId", "domainId", "scheduleType", "isVerified", "verificationStatus", "totalRevenue", "pendingRevenue", rating, "createdAt", "updatedAt") +VALUES ('test_unit_dash_cp1', 'test_unit_dash_u1', 'test_unit_dash_dom', 'WEEKLY', true, 'VERIFIED', 500000, 100000, 4.5, NOW(), NOW()); +UPDATE "users" SET "consultantProfileId" = 'test_unit_dash_cp1' WHERE id = 'test_unit_dash_u1'; + +INSERT INTO "ConsulteeProfile" (id, "userId", "createdAt", "updatedAt") +VALUES ('test_unit_dash_cep1', 'test_unit_dash_u2', NOW(), NOW()); +UPDATE "users" SET "consulteeProfileId" = 'test_unit_dash_cep1' WHERE id = 'test_unit_dash_u2'; + +-- Consultation plan +INSERT INTO "ConsultationPlan" (id, title, "durationInHours", price, "consultantProfileId", "createdAt", "updatedAt") +VALUES ('test_unit_dash_plan1', 'Dashboard Plan', 1, 50000, 'test_unit_dash_cp1', NOW(), NOW()); + +-- Pending consultation (shows as pending request for consultant) +INSERT INTO "Consultation" (id, "consultationPlanId", "requestStatus", "requestedById", "requestedAt", "createdAt", "updatedAt") +VALUES ('test_unit_dash_con1', 'test_unit_dash_plan1', 'PENDING', 'test_unit_dash_cep1', NOW(), NOW(), NOW()); + +-- Scheduled consultation (shows as upcoming for both) +INSERT INTO "Consultation" (id, "consultationPlanId", "requestStatus", "requestedById", "requestedAt", "createdAt", "updatedAt") +VALUES ('test_unit_dash_con2', 'test_unit_dash_plan1', 'SCHEDULED', 'test_unit_dash_cep1', NOW(), NOW(), NOW()); + +INSERT INTO "Appointment" (id, "appointmentType", "consultationId", "createdAt", "updatedAt") +VALUES ('test_unit_dash_apt1', 'CONSULTATION', 'test_unit_dash_con2', NOW(), NOW()); + +-- Upcoming slot +INSERT INTO "SlotOfAppointment" (id, "startsAt", "endsAt", "appointmentId", "createdAt", "updatedAt") +VALUES ('test_unit_dash_soa1', NOW() + INTERVAL '2 days', NOW() + INTERVAL '2 days' + INTERVAL '1 hour', 'test_unit_dash_apt1', NOW(), NOW()); + +-- Activity log +INSERT INTO "ActivityLog" (id, "activityType", description, "actorId", "actorName", "consultantProfileId", "createdAt") +VALUES ('test_unit_dash_al1', 'CONSULTATION_BOOKED', 'Dash Consultee booked a consultation', 'test_unit_dash_u2', 'Dash Consultee', 'test_unit_dash_cp1', NOW()); + +INSERT INTO "cookie_preferences" (id, "userId", essential, "consentGivenAt", "consentUpdatedAt") +VALUES + ('test_unit_dash_ck1', 'test_unit_dash_u1', true, NOW(), NOW()), + ('test_unit_dash_ck2', 'test_unit_dash_u2', true, NOW(), NOW()); +INSERT INTO "notification_preferences" (id, "userId") +VALUES ('test_unit_dash_np1', 'test_unit_dash_u1'), ('test_unit_dash_np2', 'test_unit_dash_u2'); + +COMMIT; diff --git a/backend/pubspec.lock b/backend/pubspec.lock new file mode 100644 index 0000000..c46075a --- /dev/null +++ b/backend/pubspec.lock @@ -0,0 +1,1357 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + _fe_analyzer_shared: + dependency: transitive + description: + name: _fe_analyzer_shared + sha256: da0d9209ca76bde579f2da330aeb9df62b6319c834fa7baae052021b0462401f + url: "https://pub.dev" + source: hosted + version: "85.0.0" + adaptive_number: + dependency: transitive + description: + name: adaptive_number + sha256: "3a567544e9b5c9c803006f51140ad544aedc79604fd4f3f2c1380003f97c1d77" + url: "https://pub.dev" + source: hosted + version: "1.0.0" + analyzer: + dependency: transitive + description: + name: analyzer + sha256: "974859dc0ff5f37bc4313244b3218c791810d03ab3470a579580279ba971a48d" + url: "https://pub.dev" + source: hosted + version: "7.7.1" + app_links: + dependency: transitive + description: + name: app_links + sha256: f8db46d2ea9ff6f3a37191a7fd5b7813da1253ace8c32c1b9eadace41d1188ea + url: "https://pub.dev" + source: hosted + version: "7.2.1" + app_links_linux: + dependency: transitive + description: + name: app_links_linux + sha256: f5f7173a78609f3dfd4c2ff2c95bd559ab43c80a87dc6a095921d96c05688c81 + url: "https://pub.dev" + source: hosted + version: "1.0.3" + app_links_platform_interface: + dependency: transitive + description: + name: app_links_platform_interface + sha256: "7546f09a6e93f4a2df2fe2bd40a5c6c64310ac461b036d82b43033be7a59f809" + url: "https://pub.dev" + source: hosted + version: "2.0.4" + app_links_web: + dependency: transitive + description: + name: app_links_web + sha256: af060ed76183f9e2b87510a9480e56a5352b6c249778d07bd2c95fc35632a555 + url: "https://pub.dev" + source: hosted + version: "1.0.4" + args: + dependency: transitive + description: + name: args + sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04 + url: "https://pub.dev" + source: hosted + version: "2.7.0" + async: + dependency: transitive + description: + name: async + sha256: e2eb0491ba5ddb6177742d2da23904574082139b07c1e33b8503b9f46f3e1a37 + url: "https://pub.dev" + source: hosted + version: "2.13.1" + bcrypt: + dependency: "direct main" + description: + name: bcrypt + sha256: "6073a700cbbc59f1d4ab27cd532755e3de5e676c4941f535f351374df849270b" + url: "https://pub.dev" + source: hosted + version: "1.2.0" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + buffer: + dependency: transitive + description: + name: buffer + sha256: "389da2ec2c16283c8787e0adaede82b1842102f8c8aae2f49003a766c5c6b3d1" + url: "https://pub.dev" + source: hosted + version: "1.2.3" + build: + dependency: transitive + description: + name: build + sha256: "51dc711996cbf609b90cbe5b335bbce83143875a9d58e4b5c6d3c4f684d3dda7" + url: "https://pub.dev" + source: hosted + version: "2.5.4" + build_config: + dependency: transitive + description: + name: build_config + sha256: "4ae2de3e1e67ea270081eaee972e1bd8f027d459f249e0f1186730784c2e7e33" + url: "https://pub.dev" + source: hosted + version: "1.1.2" + build_daemon: + dependency: transitive + description: + name: build_daemon + sha256: fd754058c342243718d5171a95f352cfc9fcf0cba8cfa26df67cb13a5836db78 + url: "https://pub.dev" + source: hosted + version: "4.1.2" + build_resolvers: + dependency: transitive + description: + name: build_resolvers + sha256: ee4257b3f20c0c90e72ed2b57ad637f694ccba48839a821e87db762548c22a62 + url: "https://pub.dev" + source: hosted + version: "2.5.4" + build_runner: + dependency: "direct dev" + description: + name: build_runner + sha256: "382a4d649addbfb7ba71a3631df0ec6a45d5ab9b098638144faf27f02778eb53" + url: "https://pub.dev" + source: hosted + version: "2.5.4" + build_runner_core: + dependency: transitive + description: + name: build_runner_core + sha256: "85fbbb1036d576d966332a3f5ce83f2ce66a40bea1a94ad2d5fc29a19a0d3792" + url: "https://pub.dev" + source: hosted + version: "9.1.2" + built_collection: + dependency: transitive + description: + name: built_collection + sha256: "376e3dd27b51ea877c28d525560790aee2e6fbb5f20e2f85d5081027d94e2100" + url: "https://pub.dev" + source: hosted + version: "5.1.1" + built_value: + dependency: transitive + description: + name: built_value + sha256: "34e4067d30ce212937df995f03b69992eea683539ceeac7f679a1f1eba055b56" + url: "https://pub.dev" + source: hosted + version: "8.12.6" + characters: + dependency: transitive + description: + name: characters + sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b + url: "https://pub.dev" + source: hosted + version: "1.4.1" + charcode: + dependency: transitive + description: + name: charcode + sha256: fb0f1107cac15a5ea6ef0a6ef71a807b9e4267c713bb93e00e92d737cc8dbd8a + url: "https://pub.dev" + source: hosted + version: "1.4.0" + checked_yaml: + dependency: transitive + description: + name: checked_yaml + sha256: "959525d3162f249993882720d52b7e0c833978df229be20702b33d48d91de70f" + url: "https://pub.dev" + source: hosted + version: "2.0.4" + cli_config: + dependency: transitive + description: + name: cli_config + sha256: ac20a183a07002b700f0c25e61b7ee46b23c309d76ab7b7640a028f18e4d99ec + url: "https://pub.dev" + source: hosted + version: "0.2.0" + clock: + dependency: transitive + description: + name: clock + sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b + url: "https://pub.dev" + source: hosted + version: "1.1.2" + code_assets: + dependency: transitive + description: + name: code_assets + sha256: bf394f466ba9205f1812a0433b392d6af280f155f56651eda7c18cc32ed493b8 + url: "https://pub.dev" + source: hosted + version: "1.2.1" + code_builder: + dependency: transitive + description: + name: code_builder + sha256: "6a6cab2ba4680d6423f34a9b972a4c9a94ebe1b62ecec4e1a1f2cba91fd1319d" + url: "https://pub.dev" + source: hosted + version: "4.11.1" + collection: + dependency: transitive + description: + name: collection + sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" + url: "https://pub.dev" + source: hosted + version: "1.19.1" + connectivity_plus: + dependency: transitive + description: + name: connectivity_plus + sha256: "762c99f890ca8bf87f7337236f99edd42793843bc6c3631da294a76653a54bd0" + url: "https://pub.dev" + source: hosted + version: "7.3.1" + connectivity_plus_platform_interface: + dependency: transitive + description: + name: connectivity_plus_platform_interface + sha256: "3c09627c536d22fd24691a905cdd8b14520de69da52c7a97499c8be5284a32ed" + url: "https://pub.dev" + source: hosted + version: "2.1.0" + convert: + dependency: transitive + description: + name: convert + sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68 + url: "https://pub.dev" + source: hosted + version: "3.1.2" + coverage: + dependency: transitive + description: + name: coverage + sha256: "956a3de0725ca232ad353565a8290d3357592bf4250f6f298a185e2d949c5d3d" + url: "https://pub.dev" + source: hosted + version: "1.15.1" + crypto: + dependency: "direct main" + description: + name: crypto + sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf + url: "https://pub.dev" + source: hosted + version: "3.0.7" + dart_frog: + dependency: "direct main" + description: + name: dart_frog + sha256: "4ab2323ad74f935f5f76cb2de7e699d0319245e8029753878b0ddc35984f9cfe" + url: "https://pub.dev" + source: hosted + version: "1.2.6" + dart_frog_lint: + dependency: "direct dev" + description: + name: dart_frog_lint + sha256: a3dd7762f8e430de79e047036f86404b130567d3be6cd1bca75c2bdb51a69b62 + url: "https://pub.dev" + source: hosted + version: "0.1.2" + dart_jsonwebtoken: + dependency: "direct main" + description: + name: dart_jsonwebtoken + sha256: "00a0812d2aeaeb0d30bcbc4dd3cee57971dbc0ab2216adf4f0247f37793f15ef" + url: "https://pub.dev" + source: hosted + version: "2.17.0" + dart_style: + dependency: transitive + description: + name: dart_style + sha256: "8a0e5fba27e8ee025d2ffb4ee820b4e6e2cf5e4246a6b1a477eb66866947e0bb" + url: "https://pub.dev" + source: hosted + version: "3.1.1" + dbus: + dependency: transitive + description: + name: dbus + sha256: "0ce9b0a839e6dee59a37a623d2fc26a35bbbe6404213e419b0d6411023d62645" + url: "https://pub.dev" + source: hosted + version: "0.7.14" + dotenv: + dependency: "direct main" + description: + name: dotenv + sha256: "379e64b6fc82d3df29461d349a1796ecd2c436c480d4653f3af6872eccbc90e1" + url: "https://pub.dev" + source: hosted + version: "4.2.0" + ed25519_edwards: + dependency: transitive + description: + name: ed25519_edwards + sha256: "6ce0112d131327ec6d42beede1e5dfd526069b18ad45dcf654f15074ad9276cd" + url: "https://pub.dev" + source: hosted + version: "0.3.1" + ffi: + dependency: transitive + description: + name: ffi + sha256: "6d7fd89431262d8f3125e81b50d3847a091d846eafcd4fdb88dd06f36d705a45" + url: "https://pub.dev" + source: hosted + version: "2.2.0" + file: + dependency: transitive + description: + name: file + sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 + url: "https://pub.dev" + source: hosted + version: "7.0.1" + fixnum: + dependency: transitive + description: + name: fixnum + sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be + url: "https://pub.dev" + source: hosted + version: "1.1.1" + flutter: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + flutter_hooks: + dependency: transitive + description: + name: flutter_hooks + sha256: "8ae1f090e5f4ef5cfa6670ce1ab5dddadd33f3533a7f9ba19d9f958aa2a89f42" + url: "https://pub.dev" + source: hosted + version: "0.21.3+1" + flutter_web_plugins: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + freezed: + dependency: "direct dev" + description: + name: freezed + sha256: "59a584c24b3acdc5250bb856d0d3e9c0b798ed14a4af1ddb7dc1c7b41df91c9c" + url: "https://pub.dev" + source: hosted + version: "2.5.8" + freezed_annotation: + dependency: "direct main" + description: + name: freezed_annotation + sha256: c2e2d632dd9b8a2b7751117abcfc2b4888ecfe181bd9fca7170d9ef02e595fe2 + url: "https://pub.dev" + source: hosted + version: "2.4.4" + frontend_server_client: + dependency: transitive + description: + name: frontend_server_client + sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694 + url: "https://pub.dev" + source: hosted + version: "4.0.0" + functions_client: + dependency: transitive + description: + name: functions_client + sha256: e9685e9ab852a8b8e0579c867f6c9155da27317b294b3df796a536b8b8e0d253 + url: "https://pub.dev" + source: hosted + version: "2.6.4" + glob: + dependency: transitive + description: + name: glob + sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de + url: "https://pub.dev" + source: hosted + version: "2.1.3" + gotrue: + dependency: transitive + description: + name: gotrue + sha256: "360bba9606e58d5acc59a033ab64ae3cf571a6868f7a7267cb8e9a204b7bf1b2" + url: "https://pub.dev" + source: hosted + version: "2.26.0" + gql: + dependency: transitive + description: + name: gql + sha256: "67c32325eb55c15f526f0f5e7d8b38a463dbff2ec3c2e046be4a1a95f0dc93d1" + url: "https://pub.dev" + source: hosted + version: "1.0.1" + gql_dedupe_link: + dependency: transitive + description: + name: gql_dedupe_link + sha256: "10bee0564d67c24e0c8bd08bd56e0682b64a135e58afabbeed30d85d5e9fea96" + url: "https://pub.dev" + source: hosted + version: "2.0.4-alpha+1715521079596" + gql_error_link: + dependency: transitive + description: + name: gql_error_link + sha256: dd0f3fbfbcec848ea050507470cdb5d3dc47d29544ae11044a1c883cbe159ccc + url: "https://pub.dev" + source: hosted + version: "1.0.1" + gql_exec: + dependency: transitive + description: + name: gql_exec + sha256: "394944626fae900f1d34343ecf2d62e44eb984826189c8979d305f0ae5846e38" + url: "https://pub.dev" + source: hosted + version: "1.1.1-alpha+1699813812660" + gql_http_link: + dependency: transitive + description: + name: gql_http_link + sha256: "07635e85a4f313836904961904417fd27844fe8f68f77b410a4e6b81d8e9202e" + url: "https://pub.dev" + source: hosted + version: "1.2.0" + gql_link: + dependency: transitive + description: + name: gql_link + sha256: "0730276ce3a6a0ced073194ff923a8d99b3c78e442cbf096eb54fd0c3fa9f974" + url: "https://pub.dev" + source: hosted + version: "1.1.0" + gql_transform_link: + dependency: transitive + description: + name: gql_transform_link + sha256: b3bb06a6991bc5c9d877e2757455f80e2c14dc684b8327bedae4f4ee67afae8b + url: "https://pub.dev" + source: hosted + version: "1.0.1" + graphql: + dependency: transitive + description: + name: graphql + sha256: a7cb0b5e8719546bf8d4edf5f57c3690ddf0fcce379c0d9d2287fdab73481090 + url: "https://pub.dev" + source: hosted + version: "5.2.4" + graphql_flutter: + dependency: transitive + description: + name: graphql_flutter + sha256: "4164962170998bc88bed833d1aa6efc5c3a85cbc8e66a8e62f790b43f4953980" + url: "https://pub.dev" + source: hosted + version: "5.3.0" + graphs: + dependency: transitive + description: + name: graphs + sha256: "741bbf84165310a68ff28fe9e727332eef1407342fca52759cb21ad8177bb8d0" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + gtk: + dependency: transitive + description: + name: gtk + sha256: "4ff85b2a16724029dd9e5bbb5a94b6918f9973f74ba571c949d2002801879cf5" + url: "https://pub.dev" + source: hosted + version: "2.2.0" + hive_ce: + dependency: transitive + description: + name: hive_ce + sha256: "8e9980e68643afb1e765d3af32b47996552a64e190d03faf622cea07c1294418" + url: "https://pub.dev" + source: hosted + version: "2.19.3" + hooks: + dependency: transitive + description: + name: hooks + sha256: "9a62a50b50b769a737bc0a8ff381f333529df3ab746b2f6b02e83760231455ba" + url: "https://pub.dev" + source: hosted + version: "2.0.2" + hotreloader: + dependency: transitive + description: + name: hotreloader + sha256: "66871df468fc24eee81f1a0a7cb98acc104716f9b7376d355437b48d633c4ebf" + url: "https://pub.dev" + source: hosted + version: "4.4.0" + http: + dependency: "direct main" + description: + name: http + sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412" + url: "https://pub.dev" + source: hosted + version: "1.6.0" + http_methods: + dependency: transitive + description: + name: http_methods + sha256: "6bccce8f1ec7b5d701e7921dca35e202d425b57e317ba1a37f2638590e29e566" + url: "https://pub.dev" + source: hosted + version: "1.1.1" + http_multi_server: + dependency: transitive + description: + name: http_multi_server + sha256: aa6199f908078bb1c5efb8d8638d4ae191aac11b311132c3ef48ce352fb52ef8 + url: "https://pub.dev" + source: hosted + version: "3.2.2" + http_parser: + dependency: transitive + description: + name: http_parser + sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" + url: "https://pub.dev" + source: hosted + version: "4.1.2" + io: + dependency: transitive + description: + name: io + sha256: dfd5a80599cf0165756e3181807ed3e77daf6dd4137caaad72d0b7931597650b + url: "https://pub.dev" + source: hosted + version: "1.0.5" + isolate_channel: + dependency: transitive + description: + name: isolate_channel + sha256: a9d3d620695bc984244dafae00b95e4319d6974b2d77f4b9e1eb4f2efe099094 + url: "https://pub.dev" + source: hosted + version: "0.6.1" + jni: + dependency: transitive + description: + name: jni + sha256: c2230682d5bc2362c1c9e8d3c7f406d9cbba23ab3f2e203a025dd47e0fb2e68f + url: "https://pub.dev" + source: hosted + version: "1.0.0" + jni_flutter: + dependency: transitive + description: + name: jni_flutter + sha256: "8b59e590786050b1cd866677dddaf76b1ade5e7bc751abe04b86e84d379d3ba6" + url: "https://pub.dev" + source: hosted + version: "1.0.1" + js: + dependency: transitive + description: + name: js + sha256: "53385261521cc4a0c4658fd0ad07a7d14591cf8fc33abbceae306ddb974888dc" + url: "https://pub.dev" + source: hosted + version: "0.7.2" + json_annotation: + dependency: "direct main" + description: + name: json_annotation + sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1" + url: "https://pub.dev" + source: hosted + version: "4.9.0" + json_serializable: + dependency: "direct dev" + description: + name: json_serializable + sha256: c50ef5fc083d5b5e12eef489503ba3bf5ccc899e487d691584699b4bdefeea8c + url: "https://pub.dev" + source: hosted + version: "6.9.5" + jwt_decode: + dependency: transitive + description: + name: jwt_decode + sha256: d2e9f68c052b2225130977429d30f187aa1981d789c76ad104a32243cfdebfbb + url: "https://pub.dev" + source: hosted + version: "0.3.1" + logging: + dependency: "direct main" + description: + name: logging + sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61 + url: "https://pub.dev" + source: hosted + version: "1.3.0" + matcher: + dependency: transitive + description: + name: matcher + sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 + url: "https://pub.dev" + source: hosted + version: "0.12.17" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b" + url: "https://pub.dev" + source: hosted + version: "0.13.0" + meta: + dependency: transitive + description: + name: meta + sha256: "1741988757a65eb6b36abe716829688cf01910bbf91c34354ff7ec1c3de2b349" + url: "https://pub.dev" + source: hosted + version: "1.18.0" + mime: + dependency: transitive + description: + name: mime + sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6" + url: "https://pub.dev" + source: hosted + version: "2.0.0" + mocktail: + dependency: "direct dev" + description: + name: mocktail + sha256: "5e1bf53cc7baa8062a33b84424deb61513858ea05c601b8509e683815b5914aa" + url: "https://pub.dev" + source: hosted + version: "1.0.5" + mysql1: + dependency: transitive + description: + name: mysql1 + sha256: "68aec7003d2abc85769bafa1777af3f4a390a90c31032b89636758ff8eb839e9" + url: "https://pub.dev" + source: hosted + version: "0.20.0" + nm: + dependency: transitive + description: + name: nm + sha256: "2c9aae4127bdc8993206464fcc063611e0e36e72018696cd9631023a31b24254" + url: "https://pub.dev" + source: hosted + version: "0.5.0" + node_preamble: + dependency: transitive + description: + name: node_preamble + sha256: "6e7eac89047ab8a8d26cf16127b5ed26de65209847630400f9aefd7cd5c730db" + url: "https://pub.dev" + source: hosted + version: "2.0.2" + normalize: + dependency: transitive + description: + name: normalize + sha256: "703f0af9e6f43a5a71536e977b945238bc89f1a941347e7ba467865a20cc1a9f" + url: "https://pub.dev" + source: hosted + version: "0.10.0" + objective_c: + dependency: transitive + description: + name: objective_c + sha256: "6cb691c686fa2838c6deb34980d426145c2a5d537491cb83d463c33cdbc726ed" + url: "https://pub.dev" + source: hosted + version: "9.4.1" + package_config: + dependency: transitive + description: + name: package_config + sha256: f096c55ebb7deb7e384101542bfba8c52696c1b56fca2eb62827989ef2353bbc + url: "https://pub.dev" + source: hosted + version: "2.2.0" + passkeys_platform_interface: + dependency: transitive + description: + name: passkeys_platform_interface + sha256: e810520c7b79dca629fdc266958564eedd77dc85a955f5e94430dfccb92eef68 + url: "https://pub.dev" + source: hosted + version: "2.9.0" + path: + dependency: transitive + description: + name: path + sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" + url: "https://pub.dev" + source: hosted + version: "1.9.1" + path_provider: + dependency: transitive + description: + name: path_provider + sha256: a7f4874f987173da295a61c181b8ee71dab59b332a486b391babf26a1b884825 + url: "https://pub.dev" + source: hosted + version: "2.1.6" + path_provider_android: + dependency: transitive + description: + name: path_provider_android + sha256: "69cbd515a62b94d32a7944f086b2f82b4ac40a1d45bebfc00813a430ab2dabcd" + url: "https://pub.dev" + source: hosted + version: "2.3.1" + path_provider_foundation: + dependency: transitive + description: + name: path_provider_foundation + sha256: "2a376b7d6392d80cd3705782d2caa734ca4727776db0b6ec36ef3f1855197699" + url: "https://pub.dev" + source: hosted + version: "2.6.0" + path_provider_linux: + dependency: transitive + description: + name: path_provider_linux + sha256: "58c2005f147315b11e9b4a7bc889cd5203e250cba8e3f012dae259b4972b5c16" + url: "https://pub.dev" + source: hosted + version: "2.2.2" + path_provider_platform_interface: + dependency: transitive + description: + name: path_provider_platform_interface + sha256: "484838772624c3a4b94f1e44a3e19897fee738f2d5c4ce448443b0417f7c9dda" + url: "https://pub.dev" + source: hosted + version: "2.1.3" + path_provider_windows: + dependency: transitive + description: + name: path_provider_windows + sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 + url: "https://pub.dev" + source: hosted + version: "2.3.0" + petitparser: + dependency: transitive + description: + name: petitparser + sha256: "91bd59303e9f769f108f8df05e371341b15d59e995e6806aefab827b58336675" + url: "https://pub.dev" + source: hosted + version: "7.0.2" + platform: + dependency: transitive + description: + name: platform + sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" + url: "https://pub.dev" + source: hosted + version: "3.1.6" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" + url: "https://pub.dev" + source: hosted + version: "2.1.8" + pointycastle: + dependency: transitive + description: + name: pointycastle + sha256: "4be0097fcf3fd3e8449e53730c631200ebc7b88016acecab2b0da2f0149222fe" + url: "https://pub.dev" + source: hosted + version: "3.9.1" + pool: + dependency: transitive + description: + name: pool + sha256: "978783255c543aa3586a1b3c21f6e9d720eb315376a915872c61ef8b5c20177d" + url: "https://pub.dev" + source: hosted + version: "1.5.2" + postgres: + dependency: "direct main" + description: + name: postgres + sha256: "123de5cbadc56a7e8d9fa485c780b6b56940b4081f4c74f3a5578682757c299b" + url: "https://pub.dev" + source: hosted + version: "3.5.12" + postgrest: + dependency: transitive + description: + name: postgrest + sha256: "10e3f195e131eec944fa872644aed89b33b5be998f3fc77c87325db3927bc646" + url: "https://pub.dev" + source: hosted + version: "2.8.0" + prisma_flutter_connector: + dependency: "direct main" + description: + name: prisma_flutter_connector + sha256: be501a935ef506f0e55a29b24c1e0fa64c23cb4e16ba20b44ecb6b22d104bc96 + url: "https://pub.dev" + source: hosted + version: "0.6.0" + pub_semver: + dependency: transitive + description: + name: pub_semver + sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585" + url: "https://pub.dev" + source: hosted + version: "2.2.0" + pubspec_parse: + dependency: transitive + description: + name: pubspec_parse + sha256: "0560ba233314abbed0a48a2956f7f022cce7c3e1e73df540277da7544cad4082" + url: "https://pub.dev" + source: hosted + version: "1.5.0" + realtime_client: + dependency: transitive + description: + name: realtime_client + sha256: "0cfbe0047e2591eba348938e9b4e620d5b1a8df6c374757e8eb8645252b8387f" + url: "https://pub.dev" + source: hosted + version: "2.11.0" + record_use: + dependency: transitive + description: + name: record_use + sha256: "2551bd8eecfe95d14ae75f6021ad0248be5c27f138c2ec12fcb52b500b3ba1ed" + url: "https://pub.dev" + source: hosted + version: "0.6.0" + retry: + dependency: transitive + description: + name: retry + sha256: "822e118d5b3aafed083109c72d5f484c6dc66707885e07c0fbcb8b986bba7efc" + url: "https://pub.dev" + source: hosted + version: "3.1.2" + rxdart: + dependency: transitive + description: + name: rxdart + sha256: "5c3004a4a8dbb94bd4bf5412a4def4acdaa12e12f269737a5751369e12d1a962" + url: "https://pub.dev" + source: hosted + version: "0.28.0" + sentry: + dependency: "direct main" + description: + name: sentry + sha256: "599701ca0693a74da361bc780b0752e1abc98226cf5095f6b069648116c896bb" + url: "https://pub.dev" + source: hosted + version: "8.14.2" + shared_preferences: + dependency: transitive + description: + name: shared_preferences + sha256: c3025c5534b01739267eb7d76959bbc25a6d10f6988e1c2a3036940133dd10bf + url: "https://pub.dev" + source: hosted + version: "2.5.5" + shared_preferences_android: + dependency: transitive + description: + name: shared_preferences_android + sha256: "0634e64bd719f89c012f392938e173521f535d3ecaf66558fa94a056d22b5cc7" + url: "https://pub.dev" + source: hosted + version: "2.4.27" + shared_preferences_foundation: + dependency: transitive + description: + name: shared_preferences_foundation + sha256: "4e7eaffc2b17ba398759f1151415869a34771ba11ebbccd1b0145472a619a64f" + url: "https://pub.dev" + source: hosted + version: "2.5.6" + shared_preferences_linux: + dependency: transitive + description: + name: shared_preferences_linux + sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + shared_preferences_platform_interface: + dependency: transitive + description: + name: shared_preferences_platform_interface + sha256: "649dc798a33931919ea356c4305c2d1f81619ea6e92244070b520187b5140ef9" + url: "https://pub.dev" + source: hosted + version: "2.4.2" + shared_preferences_web: + dependency: transitive + description: + name: shared_preferences_web + sha256: c49bd060261c9a3f0ff445892695d6212ff603ef3115edbb448509d407600019 + url: "https://pub.dev" + source: hosted + version: "2.4.3" + shared_preferences_windows: + dependency: transitive + description: + name: shared_preferences_windows + sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + shelf: + dependency: transitive + description: + name: shelf + sha256: e7dd780a7ffb623c57850b33f43309312fc863fb6aa3d276a754bb299839ef12 + url: "https://pub.dev" + source: hosted + version: "1.4.2" + shelf_hotreload: + dependency: transitive + description: + name: shelf_hotreload + sha256: "449f68ce2d087a030c2bfbb40e6925c63fac3dcb502a67388a7139ce72102e7a" + url: "https://pub.dev" + source: hosted + version: "1.6.0" + shelf_packages_handler: + dependency: transitive + description: + name: shelf_packages_handler + sha256: "89f967eca29607c933ba9571d838be31d67f53f6e4ee15147d5dc2934fee1b1e" + url: "https://pub.dev" + source: hosted + version: "3.0.2" + shelf_static: + dependency: transitive + description: + name: shelf_static + sha256: c87c3875f91262785dade62d135760c2c69cb217ac759485334c5857ad89f6e3 + url: "https://pub.dev" + source: hosted + version: "1.1.3" + shelf_web_socket: + dependency: transitive + description: + name: shelf_web_socket + sha256: "3632775c8e90d6c9712f883e633716432a27758216dfb61bd86a8321c0580925" + url: "https://pub.dev" + source: hosted + version: "3.0.0" + sky_engine: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + source_gen: + dependency: transitive + description: + name: source_gen + sha256: "35c8150ece9e8c8d263337a265153c3329667640850b9304861faea59fc98f6b" + url: "https://pub.dev" + source: hosted + version: "2.0.0" + source_helper: + dependency: transitive + description: + name: source_helper + sha256: a447acb083d3a5ef17f983dd36201aeea33fedadb3228fa831f2f0c92f0f3aca + url: "https://pub.dev" + source: hosted + version: "1.3.7" + source_map_stack_trace: + dependency: transitive + description: + name: source_map_stack_trace + sha256: c0713a43e323c3302c2abe2a1cc89aa057a387101ebd280371d6a6c9fa68516b + url: "https://pub.dev" + source: hosted + version: "2.1.2" + source_maps: + dependency: transitive + description: + name: source_maps + sha256: "190222579a448b03896e0ca6eca5998fa810fda630c1d65e2f78b3f638f54812" + url: "https://pub.dev" + source: hosted + version: "0.10.13" + source_span: + dependency: transitive + description: + name: source_span + sha256: "56a02f1f4cd1a2d96303c0144c93bd6d909eea6bee6bf5a0e0b685edbd4c47ab" + url: "https://pub.dev" + source: hosted + version: "1.10.2" + sqflite: + dependency: transitive + description: + name: sqflite + sha256: "58a799e6ac17dd32fbab93813d39ed835a75ccc0f8f85b8955fe318c6712b082" + url: "https://pub.dev" + source: hosted + version: "2.4.3" + sqflite_android: + dependency: transitive + description: + name: sqflite_android + sha256: d0548f9d7422a2dae99ec6f8b0a3074463b132d216fa5ba0d230eeefc901983b + url: "https://pub.dev" + source: hosted + version: "2.4.3" + sqflite_common: + dependency: transitive + description: + name: sqflite_common + sha256: "5bf6a55c166e73bf651ba7ec3ed486e577620e3dc8f3a9c6a258a8031b624590" + url: "https://pub.dev" + source: hosted + version: "2.5.11" + sqflite_darwin: + dependency: transitive + description: + name: sqflite_darwin + sha256: c86ca18b8f666bbf903924687fe21cc16fc385d086005067e26619ca530bef9f + url: "https://pub.dev" + source: hosted + version: "2.4.3+1" + sqflite_platform_interface: + dependency: transitive + description: + name: sqflite_platform_interface + sha256: f84939f84350d92d04416f8bc4dc52d3896aec7716cc9e80cf0146342139dc50 + url: "https://pub.dev" + source: hosted + version: "2.4.1" + stack_trace: + dependency: transitive + description: + name: stack_trace + sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" + url: "https://pub.dev" + source: hosted + version: "1.12.1" + storage_client: + dependency: transitive + description: + name: storage_client + sha256: "221263cfbe0c01575b7d7fe7543e44abff380eb4d38d936372844a1caa85ba12" + url: "https://pub.dev" + source: hosted + version: "2.6.0" + stream_channel: + dependency: transitive + description: + name: stream_channel + sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + stream_transform: + dependency: transitive + description: + name: stream_transform + sha256: ad47125e588cfd37a9a7f86c7d6356dde8dfe89d071d293f80ca9e9273a33871 + url: "https://pub.dev" + source: hosted + version: "2.1.1" + string_scanner: + dependency: transitive + description: + name: string_scanner + sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" + url: "https://pub.dev" + source: hosted + version: "1.4.1" + supabase: + dependency: transitive + description: + name: supabase + sha256: "3879996256325fcc9abb03b62b12c07e4eaae2e008e1bf043cc1525184159a43" + url: "https://pub.dev" + source: hosted + version: "2.14.0" + supabase_flutter: + dependency: transitive + description: + name: supabase_flutter + sha256: c9916c1cd512ebf3107ec0f83c9dca13d2e1e789629c2a5df896586bff46ce5f + url: "https://pub.dev" + source: hosted + version: "2.16.0" + synchronized: + dependency: transitive + description: + name: synchronized + sha256: "61894a1956de6b4fc1aefd0892e109514a1a706cbece3ac59decd90ff5a7a423" + url: "https://pub.dev" + source: hosted + version: "3.4.1+1" + term_glyph: + dependency: transitive + description: + name: term_glyph + sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" + url: "https://pub.dev" + source: hosted + version: "1.2.2" + test: + dependency: "direct dev" + description: + name: test + sha256: "75906bf273541b676716d1ca7627a17e4c4070a3a16272b7a3dc7da3b9f3f6b7" + url: "https://pub.dev" + source: hosted + version: "1.26.3" + test_api: + dependency: transitive + description: + name: test_api + sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55 + url: "https://pub.dev" + source: hosted + version: "0.7.7" + test_core: + dependency: transitive + description: + name: test_core + sha256: "0cc24b5ff94b38d2ae73e1eb43cc302b77964fbf67abad1e296025b78deb53d0" + url: "https://pub.dev" + source: hosted + version: "0.6.12" + timing: + dependency: transitive + description: + name: timing + sha256: "62ee18aca144e4a9f29d212f5a4c6a053be252b895ab14b5821996cff4ed90fe" + url: "https://pub.dev" + source: hosted + version: "1.0.2" + typed_data: + dependency: transitive + description: + name: typed_data + sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 + url: "https://pub.dev" + source: hosted + version: "1.4.0" + url_launcher: + dependency: transitive + description: + name: url_launcher + sha256: f6a7e5c4835bb4e3026a04793a4199ca2d14c739ec378fdfe23fc8075d0439f8 + url: "https://pub.dev" + source: hosted + version: "6.3.2" + url_launcher_android: + dependency: transitive + description: + name: url_launcher_android + sha256: b413d49b73867ac08dd2f9890efd3cc11f2a0e577618d50843440a1fb3776c32 + url: "https://pub.dev" + source: hosted + version: "6.3.32" + url_launcher_ios: + dependency: transitive + description: + name: url_launcher_ios + sha256: "580fe5dfb51671ae38191d316e027f6b76272b026370708c2d898799750a02b0" + url: "https://pub.dev" + source: hosted + version: "6.4.1" + url_launcher_linux: + dependency: transitive + description: + name: url_launcher_linux + sha256: d5e14138b3bc193a0f63c10a53c94b91d399df0512b1f29b94a043db7482384a + url: "https://pub.dev" + source: hosted + version: "3.2.2" + url_launcher_macos: + dependency: transitive + description: + name: url_launcher_macos + sha256: "368adf46f71ad3c21b8f06614adb38346f193f3a59ba8fe9a2fd74133070ba18" + url: "https://pub.dev" + source: hosted + version: "3.2.5" + url_launcher_platform_interface: + dependency: transitive + description: + name: url_launcher_platform_interface + sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + url_launcher_web: + dependency: transitive + description: + name: url_launcher_web + sha256: "85c81589622fbc87c1c683aaea164d3604a7777495a79d91e39ffcdec39ddb34" + url: "https://pub.dev" + source: hosted + version: "2.4.3" + url_launcher_windows: + dependency: transitive + description: + name: url_launcher_windows + sha256: "712c70ab1b99744ff066053cbe3e80c73332b38d46e5e945c98689b2e66fc15f" + url: "https://pub.dev" + source: hosted + version: "3.1.5" + uuid: + dependency: "direct main" + description: + name: uuid + sha256: "9b129329f58692f6e6578329498a8fe9fbe98f090beb764ffbb8ee2eadd01dcd" + url: "https://pub.dev" + source: hosted + version: "4.6.0" + vector_math: + dependency: transitive + description: + name: vector_math + sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b + url: "https://pub.dev" + source: hosted + version: "2.2.0" + vm_service: + dependency: transitive + description: + name: vm_service + sha256: "0016aef94fc66495ac78af5859181e3f3bf2026bd8eecc72b9565601e19ab360" + url: "https://pub.dev" + source: hosted + version: "15.2.0" + watcher: + dependency: transitive + description: + name: watcher + sha256: "1398c9f081a753f9226febe8900fce8f7d0a67163334e1c94a2438339d79d635" + url: "https://pub.dev" + source: hosted + version: "1.2.1" + web: + dependency: transitive + description: + name: web + sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" + url: "https://pub.dev" + source: hosted + version: "1.1.1" + web_socket: + dependency: transitive + description: + name: web_socket + sha256: "34d64019aa8e36bf9842ac014bb5d2f5586ca73df5e4d9bf5c936975cae6982c" + url: "https://pub.dev" + source: hosted + version: "1.0.1" + web_socket_channel: + dependency: transitive + description: + name: web_socket_channel + sha256: d645757fb0f4773d602444000a8131ff5d48c9e47adfe9772652dd1a4f2d45c8 + url: "https://pub.dev" + source: hosted + version: "3.0.3" + webkit_inspection_protocol: + dependency: transitive + description: + name: webkit_inspection_protocol + sha256: "87d3f2333bb240704cd3f1c6b5b7acd8a10e7f0bc28c28dcf14e782014f4a572" + url: "https://pub.dev" + source: hosted + version: "1.2.1" + xdg_directories: + dependency: transitive + description: + name: xdg_directories + sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15" + url: "https://pub.dev" + source: hosted + version: "1.1.0" + xml: + dependency: transitive + description: + name: xml + sha256: "971043b3a0d3da28727e40ed3e0b5d18b742fa5a68665cca88e74b7876d5e025" + url: "https://pub.dev" + source: hosted + version: "6.6.1" + yaml: + dependency: transitive + description: + name: yaml + sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce + url: "https://pub.dev" + source: hosted + version: "3.1.3" + yet_another_json_isolate: + dependency: transitive + description: + name: yet_another_json_isolate + sha256: eaa26beb5990b25a49d942374fd5a0c5aa67a837e03b14b4c26134aaa1ed01a9 + url: "https://pub.dev" + source: hosted + version: "2.1.1" +sdks: + dart: ">=3.12.0 <4.0.0" + flutter: ">=3.44.0" diff --git a/backend/routes/api/health.dart b/backend/routes/api/health.dart new file mode 100644 index 0000000..5c0d13f --- /dev/null +++ b/backend/routes/api/health.dart @@ -0,0 +1,19 @@ +import 'dart:io'; + +import 'package:dart_frog/dart_frog.dart'; + +/// GET /api/health +/// Railway uses this endpoint to verify the server is running. +Response onRequest(RequestContext context) { + if (context.request.method != HttpMethod.get) { + return Response(statusCode: HttpStatus.methodNotAllowed); + } + + return Response.json( + body: { + 'status': 'ok', + 'timestamp': DateTime.now().toIso8601String(), + 'service': 'familiarise-mobile-api', + }, + ); +} diff --git a/backend/scripts/ensure-generated.sh b/backend/scripts/ensure-generated.sh new file mode 100755 index 0000000..d51df68 --- /dev/null +++ b/backend/scripts/ensure-generated.sh @@ -0,0 +1,80 @@ +#!/usr/bin/env bash +# Regenerate backend codegen only when its inputs actually changed. +# +# ── The problem this solves ────────────────────────────────────────────────── +# +# scripts/regenerate-build.sh used to `rm -rf lib/generated` and delete every +# *.freezed.dart unconditionally, on every invocation. lib/generated is ~735k +# lines across 498 files, so that destroyed build_runner's asset graph and +# forced a full cold rebuild every single time — even when nothing had changed, +# which is the overwhelmingly common case (schema.prisma is launch-frozen). +# +# Only four things can invalidate the output: +# - prisma/schema.prisma (what the models are) +# - prisma_flutter_connector version (the generator's output shape) +# - freezed version (the .freezed.dart output shape) +# - build.yaml (which builders run, over what) +# +# So hash those, stamp the result, and skip when it matches. +# +# Usage: +# ./scripts/ensure-generated.sh # regenerate if stale +# FORCE_CODEGEN=1 ./scripts/ensure-generated.sh # always regenerate + +set -euo pipefail +cd "$(dirname "$0")/.." + +FORCE="${FORCE_CODEGEN:-0}" +# The stamp lives inside lib/generated so that `rm -rf lib/generated` — by this +# script or by hand — also invalidates it. A stamp outside would survive the +# wipe and wrongly report "up to date" against an empty directory. +STAMP="lib/generated/.codegen-stamp" + +sha() { shasum -a 256 | cut -d' ' -f1; } + +# Read a package's resolved version out of pubspec.lock without needing yq. +locked() { + awk -v pkg=" $1:" ' + $0 == pkg { f = 1; next } + f && /^ version:/ { gsub(/"/, "", $2); print $2; exit } + f && /^ [a-z_]+:/ { exit } + ' pubspec.lock +} + +[ -f pubspec.lock ] || dart pub get + +WANT="$( { + shasum -a 256 prisma/schema.prisma + echo "connector=$(locked prisma_flutter_connector)" + echo "freezed=$(locked freezed)" + [ -f build.yaml ] && shasum -a 256 build.yaml +} | sha )" + +if [ "$FORCE" != "1" ] \ + && [ -f "$STAMP" ] \ + && [ "$(cat "$STAMP")" = "$WANT" ] \ + && [ -f lib/generated/schema_registry.g.dart ]; then + echo "[codegen] up to date (${WANT:0:12}) — skipping" + exit 0 +fi + +echo "[codegen] stale or forced — regenerating" +dart pub get + +# Full wipe ONLY on a real input change. +# +# Clear the CONTENTS rather than the directory itself: under docker-compose +# lib/generated is a named-volume mount point, and unlinking a mount point +# fails with "Device or resource busy". Emptying it works in both places. +mkdir -p lib/generated +find lib/generated -mindepth 1 -delete + +dart run prisma_flutter_connector:generate \ + --schema prisma/schema.prisma \ + --output lib/generated \ + --server + +dart run build_runner build --delete-conflicting-outputs + +echo "$WANT" > "$STAMP" +echo "[codegen] done (${WANT:0:12})" diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..da31e93 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,160 @@ +# Local development stack. +# +# docker compose up → everything (Postgres + API + Flutter web) +# docker compose up db-init → Postgres only, provisioned (the fast path) +# docker compose down → stop, KEEP the database and build caches +# docker compose down -v → stop and destroy them (full cold rebuild next time) +# +# ── Two things in here are load-bearing; do not "clean them up" ────────────── +# +# 1. `?sslmode=disable` on every local DIRECT_URL. +# DatabaseClient.initialize() only treats `localhost` / `127.0.0.1` as local +# and otherwise defaults to SslMode.require. Inside this network the host is +# `db`, so without the explicit parameter the backend attempts TLS against a +# plain postgres image and the handshake fails. +# +# 2. The `FAM_` prefix on every interpolated variable. +# Compose automatically reads the repo-root .env for interpolation, and that +# file contains DATABASE_URL / DIRECT_URL pointing at production Supabase. +# Namespacing means nothing in it can silently reach a container. Never +# reference a bare ${DATABASE_URL} or ${DIRECT_URL} here. +# +# DART_ENV is also deliberately unset: routes/_middleware.dart switches to +# production CORS when it is `production`, which would block localhost:3000. + +name: familiarise + +services: + # ── Postgres ─────────────────────────────────────────────────────────────── + db: + image: postgres:17-alpine + environment: + POSTGRES_USER: familiarise + POSTGRES_PASSWORD: familiarise + POSTGRES_DB: familiarise + # Durability traded for speed. This database is disposable: every byte in + # it is reproducible from `prisma db push` + the seed. + command: > + postgres + -c fsync=off + -c synchronous_commit=off + -c full_page_writes=off + -c max_connections=200 + -c shared_buffers=256MB + ports: + # 5433 on the host: 5432 is often taken by a Homebrew postgres. + - "${FAM_PG_PORT:-5433}:5432" + volumes: + # NAMED, never a bind mount — a Postgres data directory on macOS + # virtiofs is both dramatically slower and a corruption risk. + - pgdata:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U familiarise -d familiarise"] + interval: 3s + timeout: 3s + retries: 30 + start_period: 5s + + # ── Schema provisioning (one-shot, exits 0) ──────────────────────────────── + db-init: + build: + context: ./docker/db-init + image: familiarise/db-init:local + depends_on: + db: + condition: service_healthy + environment: + DIRECT_URL: ${FAM_DIRECT_URL:-postgresql://familiarise:familiarise@db:5432/familiarise?sslmode=disable} + volumes: + # Read-only: the in-repo schema and SQL stay the single source of truth. + - ./backend/prisma:/work/prisma:ro + restart: "no" + + # ── Dart Frog API (hot reload) ───────────────────────────────────────────── + api: + build: + context: ./backend + dockerfile: Dockerfile.dev + image: familiarise/api:dev + depends_on: + db-init: + condition: service_completed_successfully + environment: + DIRECT_URL: ${FAM_DIRECT_URL:-postgresql://familiarise:familiarise@db:5432/familiarise?sslmode=disable} + DATABASE_URL: ${FAM_DIRECT_URL:-postgresql://familiarise:familiarise@db:5432/familiarise?sslmode=disable} + PORT: "8080" + APP_BASE_URL: http://localhost:3000 + # Dev-only defaults. Override via FAM_* to exercise the real integrations. + JWT_SECRET: ${FAM_JWT_SECRET:-dev-only-jwt-secret-do-not-use-in-production} + PAN_ENCRYPTION_KEY: ${FAM_PAN_ENCRYPTION_KEY:-0000000000000000000000000000000000000000000000000000000000000000} + SENTRY_DSN: ${FAM_SENTRY_DSN:-} + SUPABASE_URL: ${FAM_SUPABASE_URL:-} + SUPABASE_SERVICE_ROLE_KEY: ${FAM_SUPABASE_SERVICE_ROLE_KEY:-} + STREAM_API_KEY: ${FAM_STREAM_API_KEY:-} + STREAM_API_SECRET: ${FAM_STREAM_API_SECRET:-} + RAZORPAY_KEY_ID: ${FAM_RAZORPAY_KEY_ID:-} + RAZORPAY_KEY_SECRET: ${FAM_RAZORPAY_KEY_SECRET:-} + RAZORPAY_WEBHOOK_SECRET: ${FAM_RAZORPAY_WEBHOOK_SECRET:-} + STRIPE_SECRET_KEY: ${FAM_STRIPE_SECRET_KEY:-} + STRIPE_WEBHOOK_SECRET: ${FAM_STRIPE_WEBHOOK_SECRET:-} + ports: + - "8080:8080" + volumes: + # Source is bind-mounted so edits hot-reload... + - ./backend:/app + # ...but every heavy generated/cache directory is shadowed by a named + # volume so it lives on the VM's native filesystem instead of virtiofs. + # For .dart_tool this is not merely an optimisation: package_config.json + # holds absolute /Users/... paths that do not exist inside the container. + - api_dart_tool:/app/.dart_tool + - api_generated:/app/lib/generated + - api_dart_frog:/app/.dart_frog + - api_build:/app/build + - pub_cache:/root/.pub-cache + healthcheck: + test: ["CMD-SHELL", "curl -fsS http://localhost:8080/api/health || exit 1"] + interval: 10s + timeout: 5s + retries: 90 + # First boot regenerates ~735k lines of Dart. Be generous. + start_period: 600s + stdin_open: true + tty: true + + # ── Flutter web ──────────────────────────────────────────────────────────── + # Slowest and least essential part of the stack — see docs/local-dev.md. + # If it drags, drop it: `docker compose up db-init api`. + web: + build: + context: . + dockerfile: docker/web/Dockerfile + image: familiarise/web:dev + depends_on: + api: + condition: service_healthy + environment: + # Baked into env_config.g.dart by envied at codegen time. This must be + # correct from the BROWSER's point of view (the browser runs on the + # host), so it is the published host port — not http://api:8080. + FAM_API_BASE_URL: ${FAM_API_BASE_URL:-http://localhost:8080} + ports: + - "3000:3000" + volumes: + - ./lib:/app/lib + - ./web:/app/web + - ./assets:/app/assets + - web_dart_tool:/app/.dart_tool + - web_build:/app/build + - pub_cache:/root/.pub-cache + stdin_open: true + tty: true + +volumes: + pgdata: + pub_cache: + api_dart_tool: + api_generated: + api_dart_frog: + api_build: + web_dart_tool: + web_build: diff --git a/docker/db-init/Dockerfile b/docker/db-init/Dockerfile new file mode 100644 index 0000000..38a1177 --- /dev/null +++ b/docker/db-init/Dockerfile @@ -0,0 +1,39 @@ +# One-shot provisioner for the local development Postgres. +# +# Deliberately NOT alpine: we need the `psql` client to apply the sidecar SQL +# (ledger triggers + CHECK constraints), and postgresql-client is a one-line +# install on bookworm. bookworm also ships OpenSSL 3, which is what Prisma's +# linux-*-openssl-3.0.x query engines expect. +# +# Prisma is pinned to 7.7.0 to match familiarise_web, which owns the schema. +# +# NOTE: no package.json and no prisma.config.ts. `prisma db push --schema X +# --url Y` reads neither a config file nor `datasource.url`, which is exactly +# what we need — backend/prisma/schema.prisma declares a datasource with no +# url (Prisma 7 moved those to config), and that file is owned by another PR +# and must not be edited here. + +FROM node:24-bookworm-slim + +RUN apt-get update && apt-get install -y --no-install-recommends \ + postgresql-client \ + ca-certificates \ + openssl \ + && rm -rf /var/lib/apt/lists/* + +RUN npm install -g prisma@7.7.0 + +# Force the query/schema engine download into the image so `docker compose up` +# never hits the network. This also makes a missing arm64 engine fail the +# image BUILD with a clear message, rather than failing confusingly at runtime. +RUN prisma version + +WORKDIR /work + +COPY entrypoint.sh /usr/local/bin/db-init +RUN chmod +x /usr/local/bin/db-init + +# prisma/ (schema + sql/) is bind-mounted read-only by docker-compose, so the +# in-repo copies stay the single source of truth. + +ENTRYPOINT ["/usr/local/bin/db-init"] diff --git a/docker/db-init/entrypoint.sh b/docker/db-init/entrypoint.sh new file mode 100755 index 0000000..9d9189b --- /dev/null +++ b/docker/db-init/entrypoint.sh @@ -0,0 +1,95 @@ +#!/bin/bash +# Provision the local development Postgres: +# 1. prisma db push — create the 129 tables from the in-repo schema +# 2. sidecar SQL — ledger triggers + CHECK constraints (db push +# does not manage triggers or checks) +# 3. seed-dev.sql — fixtures the test prompts assume, guarded so a +# repeat `docker compose up` does not re-insert +# +# Safe to run on every `docker compose up`: db push on an unchanged schema is a +# fast no-op and the sidecars are idempotent (DROP IF EXISTS + ADD). + +set -euo pipefail + +: "${DIRECT_URL:?DIRECT_URL must be set}" + +# ── Safety gate ──────────────────────────────────────────────── +# This script runs `db push --accept-data-loss`, which will happily drop +# columns to make the database match the schema. That is fine for a disposable +# local container and catastrophic anywhere else, so refuse to run against any +# host that is not the local compose network or loopback. +_rest=${DIRECT_URL#*://} # strip scheme +_rest=${_rest##*@} # strip user:password@ if present +DB_HOST=${_rest%%[:/?]*} # keep up to the first ':', '/' or '?' +case "$DB_HOST" in + db | localhost | 127.0.0.1 | postgres) ;; + *) + echo "REFUSING to provision: DIRECT_URL points at '$DB_HOST', which is not a" >&2 + echo "local database. db-init runs destructive schema pushes and must only" >&2 + echo "ever target the compose Postgres. Check your DIRECT_URL." >&2 + exit 1 + ;; +esac + +echo "==> Target: $DB_HOST" + +# ── 1. Schema ────────────────────────────────────────────────── +# --url bypasses both prisma.config.ts and datasource.url, so the in-repo +# schema (which declares neither) needs no edits. +# +# There is deliberately no --skip-generate: Prisma 7's `db push` does not +# invoke the generator at all, and passing the flag is a hard error. The +# schema's `generator client = prisma-client-js` block is therefore inert +# here — the Dart client comes from prisma_flutter_connector instead. +echo "==> [1/3] prisma db push" +prisma db push \ + --schema prisma/schema.prisma \ + --url "$DIRECT_URL" \ + --accept-data-loss + +# ── 2. Sidecars ──────────────────────────────────────────────── +# psql runs each file whole; the upstream `-- SPLIT` markers are just comments. +echo "==> [2/3] sidecar SQL (ledger triggers + CHECK constraints)" +psql "$DIRECT_URL" -v ON_ERROR_STOP=1 -q \ + -f prisma/sql/ledger-triggers.sql \ + -f prisma/sql/check-constraints.sql + +# ── 3. Seed ──────────────────────────────────────────────────── +# One file per test prompt, each applied in its own transaction. The prompts +# were written to run standalone against the shared database, so a few of them +# reference tables this schema no longer has. Applying them independently means +# that drift costs you one block instead of the whole seed — and gets reported +# rather than silently swallowed. +# +# The blocks are not individually idempotent, so gate the whole step on a +# marker row from the first one. +echo "==> [3/3] dev seed" +SEEDED=$(psql "$DIRECT_URL" -tAc \ + "SELECT count(*) FROM \"users\" WHERE id = 'test_unit_auth_u1'") + +if [ "$SEEDED" != "0" ]; then + echo " already seeded, skipping" +else + ok=0 + skipped="" + for f in prisma/sql/seed.d/*.sql; do + if err=$(psql "$DIRECT_URL" -v ON_ERROR_STOP=1 -q -f "$f" 2>&1); then + ok=$((ok + 1)) + else + skipped="$skipped $(basename "$f" .sql)" + # First ERROR line is the useful one; the rest is context. + echo " ! $(basename "$f"): $(echo "$err" | grep -m1 ERROR || echo "$err" | head -1)" + fi + done + + echo " seeded $ok/$(ls prisma/sql/seed.d/*.sql | wc -l | tr -d ' ') blocks" + if [ -n "$skipped" ]; then + echo " skipped:$skipped" + echo " (these prompts reference tables this schema does not have —" + echo " prompt drift, not a provisioning failure)" + fi +fi + +TABLES=$(psql "$DIRECT_URL" -tAc \ + "SELECT count(*) FROM information_schema.tables WHERE table_schema='public'") +echo "==> Done. $TABLES tables in public schema." diff --git a/docker/web/Dockerfile b/docker/web/Dockerfile new file mode 100644 index 0000000..fbc7257 --- /dev/null +++ b/docker/web/Dockerfile @@ -0,0 +1,31 @@ +# Flutter web dev server for docker-compose. Build context is the repo root. +# +# This is the slowest and least essential service in the stack (see +# docs/local-dev.md for measured expectations). It exists so that +# `docker compose up` brings up a working frontend at http://localhost:3000, +# matching the APP_URL the E2E prompts in prompts/testing/ already assume. +# +# Mobile is NOT built here and cannot be: an iOS simulator needs macOS + Xcode, +# which cannot be containerised, and the Android emulator needs KVM, which is +# unavailable inside Docker Desktop's VM. Mobile stays native on the host and +# talks to the same API on localhost:8080. + +FROM ghcr.io/cirruslabs/flutter:stable + +WORKDIR /app + +RUN flutter config --enable-web + +# Resolve dependencies in their own layer so source edits don't re-run pub. +COPY pubspec.yaml pubspec.lock ./ +RUN flutter pub get + +COPY docker/web/entrypoint.sh /usr/local/bin/web-entrypoint +RUN chmod +x /usr/local/bin/web-entrypoint + +EXPOSE 3000 + +ENTRYPOINT ["/usr/local/bin/web-entrypoint"] + +CMD ["flutter", "run", "-d", "web-server", \ + "--web-hostname", "0.0.0.0", "--web-port", "3000"] diff --git a/docker/web/entrypoint.sh b/docker/web/entrypoint.sh new file mode 100755 index 0000000..324351a --- /dev/null +++ b/docker/web/entrypoint.sh @@ -0,0 +1,49 @@ +#!/usr/bin/env bash +# Entrypoint for the Flutter web dev container. + +set -euo pipefail +cd /app +export PATH="/root/.pub-cache/bin:$PATH" + +API_BASE_URL="${FAM_API_BASE_URL:-http://localhost:8080}" + +# envied reads .env at CODEGEN time and bakes the values into +# lib/core/config/env_config.g.dart — they are not read at runtime. So the file +# has to exist before build_runner, and API_BASE_URL has to be correct from the +# BROWSER's point of view. The browser runs on the host, so that is the +# published host port (http://localhost:8080), NOT the compose service name. +# +# Written fresh on every start rather than bind-mounted, so the container never +# depends on a host .env that may hold production values. +if [ ! -f .env ] || ! grep -q "^API_BASE_URL=${API_BASE_URL}$" .env 2>/dev/null; then + echo "==> writing .env (API_BASE_URL=${API_BASE_URL})" + cat > .env < flutter pub get (cold volume)" + flutter pub get +fi + +# envied/freezed/riverpod codegen. build_runner keeps its asset graph in the +# .dart_tool named volume, so this is incremental after the first run. +echo "==> build_runner" +dart run build_runner build --delete-conflicting-outputs + +echo "==> starting: $*" +exec "$@" diff --git a/docs/getting-started/04-local-testing-guide.md b/docs/getting-started/04-local-testing-guide.md index bd8f588..8128703 100644 --- a/docs/getting-started/04-local-testing-guide.md +++ b/docs/getting-started/04-local-testing-guide.md @@ -29,16 +29,31 @@ During development, the mobile app needs to communicate with the backend server. ## Local Development Setup +> **Docker is now the fastest way to get a full stack running** — Postgres, the +> API and Flutter web with one command, against a local database instead of the +> shared Supabase one. See +> [05-docker-local-dev.md](./05-docker-local-dev.md). +> +> ```bash +> make up # everything +> make db # just Postgres, if you prefer running the backend natively +> ``` + ### Starting the Backend Server -#### Development Mode (with hot reload) +#### Development Mode — hot reload (recommended) ```bash +source scripts/use-db.sh local # or `supabase` cd backend dart_frog dev ``` -> Note: May have terminal stdin issues in some environments +Edits to routes reload in place. Prefer this: the production-build path below +re-analyses every route and all ~735k lines of generated code on each change. + +> Note: may have terminal stdin issues in some non-interactive environments. -#### Production Build (recommended) +#### Production Build +Use when you need to reproduce the deployed binary exactly, not for iteration. ```bash cd backend dart_frog build diff --git a/docs/getting-started/05-docker-local-dev.md b/docs/getting-started/05-docker-local-dev.md new file mode 100644 index 0000000..89676a0 --- /dev/null +++ b/docs/getting-started/05-docker-local-dev.md @@ -0,0 +1,212 @@ +# Docker Local Development + +Runs Postgres, the Dart Frog API and Flutter web locally, so the dev loop stops +depending on the shared Supabase database. + +## TL;DR + +```bash +make doctor # check the toolchain (Docker Desktop must be running) +make up # Postgres + API (:8080) + Flutter web (:3000) +``` + +If the containers feel slow on macOS, drop to the fast path — it delivers most +of the benefit: + +```bash +make db # Postgres only, schema pushed and seeded +source scripts/use-db.sh local # point the host at it +cd backend && dart_frog dev # native, full speed +``` + +## Why this exists + +Two separate problems, two separate fixes. + +**The database was remote.** `backend/.env` pointed at Supabase's Mumbai pooler, +about 40 ms away. `appointment_repository.dart` alone issues 121 queries; a +request touching twenty of them spent roughly 800 ms on network round-trips +before doing any work. A local Postgres answers in well under a millisecond. +It is also disposable, so destructive tests stop being scary. + +**Codegen was rebuilt from scratch every time.** `backend/lib/generated/` is +about 735,000 lines across 498 files, and it is gitignored. +`scripts/regenerate-build.sh` used to `rm -rf` it on every run, which threw away +build_runner's asset graph and forced a cold rebuild even when nothing had +changed. Docker cannot fix that — see [Codegen](#codegen) below for what does. + +## Requirements + +Docker Desktop with **at least 8 GB RAM, 4 CPUs and 60 GB disk**. The defaults +are too small: a Flutter web build and build_runner over 500 files will OOM. + +You also need **real free space on the host** — around 15 GB. The Flutter image +is roughly 4 GB, and Docker's VM disk grows on demand. If the host fills up +while Docker is writing, the writes fail *inside* the VM and corrupt its +containerd content store. The symptom is unmistakable: + +``` +failed to solve: failed to compute cache key: input/output error +Error response from daemon: ... blob sha256:… : input/output error +write /var/lib/desktop-containerd/…/meta.db: input/output error +``` + +Once that happens, `docker system prune` cannot fix it — pruning has to read +the very blobs that are unreadable — and Docker Desktop may stop launching. +Recover with **Docker Desktop → Troubleshoot → Reset to factory defaults** (or +*Purge data*), then free host space before retrying. Nothing here is lost by +that reset: `make db` rebuilds the image in about 20 s and reprovisions the +database in under a second. + +`make doctor` reports the daemon state before you start. + +## What runs where + +| | Where | Why | +|---|---|---| +| Postgres | container, host port **5433** | 5432 is usually taken by a Homebrew postgres | +| Dart Frog API | container, **:8080** | `dart_frog dev`, hot reload | +| Flutter **web** | container, **:3000** | matches the `APP_URL` the E2E prompts assume | +| Flutter **mobile** | **host**, natively | see below | + +Mobile cannot be containerised. An iOS simulator needs macOS and Xcode, which +Docker cannot run at all; the Android emulator needs KVM, which is not available +inside Docker Desktop's VM. Keep using `./scripts/quick-ios.sh` and +`./scripts/quick-android.sh` — they talk to the same API on `localhost:8080`. + +## Choosing a database + +The backend resolves configuration as `.env` → `.env.local` → the process +environment, **last one winning**. So an exported variable always beats the +files, and nothing needs editing to switch. + +```bash +# Containers — the compose default is already the local Postgres. +docker compose up + +# Containers, pointed at Supabase instead: +FAM_DIRECT_URL='postgresql://…@aws-0-….pooler.supabase.com:5432/postgres' docker compose up + +# Native backend: +source scripts/use-db.sh local +source scripts/use-db.sh supabase # needs backend/.env.supabase +``` + +On every boot the API logs the database it actually resolved: + +``` +INFO: [Startup] Connecting to database... (host=db:5432, db=familiarise) +``` + +**Read that line.** If it names a `*.supabase.com` host when you expected local, +stop — you are about to operate on shared data. Credentials are never printed. + +As a second layer, `docker/db-init` refuses to provision any host that is not +`db`, `postgres`, `localhost` or `127.0.0.1`, so a mistyped URL cannot cause it +to run `prisma db push --accept-data-loss` against production. + +## How the schema gets there + +There is no `migrations/` directory in this repo — `familiarise_web` owns +migrations. Provisioning therefore runs, in order: + +1. `prisma db push` from the in-repo `backend/prisma/schema.prisma`, using + Prisma 7.7.0 to match `familiarise_web`. Takes well under a second and + produces 127 tables and 100 enums. +2. `backend/prisma/sql/ledger-triggers.sql` and `check-constraints.sql` — + vendored from `familiarise_web`, because `db push` does not manage triggers + or CHECK constraints. Both are idempotent. +3. `backend/prisma/sql/seed.d/*.sql` — one file per test prompt, extracted from + the `## Data Seeding` blocks in `prompts/testing/unit/*.md`. Regenerate with + `make seed-sql`. + +Steps 1 and 2 run on every `docker compose up` and are near no-ops when nothing +changed. Step 3 is skipped once a marker row exists. + +Each seed block is applied in its own transaction, because the prompts were +written to run standalone and a few have drifted from the schema. At the time +of writing 19 of 23 apply; the rest are reported by name, e.g. + +``` +! 16-support.sql: ERROR: relation "support_tickets" does not exist +! 23-dashboard.sql: ERROR: column "totalRevenue" of relation "ConsultantProfile" does not exist +``` + +Those are **stale prompts, not provisioning failures** — the model is +`SupportTicket`, not `support_tickets`. Fixing the prompts is worthwhile but out +of scope here; running them per-block means the drift is visible and costs one +fixture set instead of the entire seed. + +To refresh the vendored SQL after an upstream change: + +```bash +cp ~/Desktop/familiarise_web/prisma/sql/*.sql backend/prisma/sql/ +``` + +## Codegen + +This is where the minutes actually were. + +**`backend/build.yaml`** disables `json_serializable` for the backend. It was +running over every file and producing nothing — there is not one +`part '*.g.dart'` directive in `backend/lib` or `backend/routes`, because the +Prisma connector writes `fromJson`/`toJson` by hand. Disabling it also drops +`source_gen|combining_builder` and `part_cleanup`, and removes freezed's +`runs_before` ordering barrier. (`lib/generated/schema_registry.g.dart` comes +from the Prisma generator, not build_runner, so it is unaffected.) + +**`backend/scripts/ensure-generated.sh`** hashes `prisma/schema.prisma`, the +resolved `prisma_flutter_connector` and `freezed` versions, and `build.yaml`, +and skips regeneration when they all match the previous run. Since the schema is +launch-frozen, this is almost always a hit. + +```bash +make regen # skip if unchanged +make regen-force # wipe lib/generated and rebuild +``` + +Inside the container, `lib/generated` and `.dart_tool` live in named volumes, so +they survive `docker compose down` and the regen does not re-run on restart. +`docker compose down -v` destroys them and the next start is a full cold build. + +## Performance notes + +Named volumes shadow every heavy directory (`.dart_tool`, `lib/generated`, +`.dart_frog`, `build`, the pub cache) so they stay on the VM's native +filesystem. Only human-authored source crosses the macOS virtiofs boundary. + +For `.dart_tool` this is correctness, not tuning: `package_config.json` contains +absolute `/Users/...` paths that do not exist inside the container, so sharing +it between host and container is broken, not merely slow. + +Consequence: the container's `lib/generated` is a **different tree** from the +host's. Your IDE and `dart analyze` keep using the host copy. Run +`./scripts/regenerate-build.sh --prisma` on the host at least once. + +Flutter web in a container is the slowest and least essential piece — expect a +first compile of several minutes and hot restarts noticeably slower than native. +If it is not paying for itself, skip it with `make api` and run +`flutter run -d chrome` on the host. + +Note that the web container's build_runner writes `env_config.g.dart` back into +the bind-mounted `lib/`, so it will overwrite the host's copy with +`API_BASE_URL=http://localhost:8080`. That is the same value the host would +normally bake; if you have customised `PHYSICAL_DEVICE_API_URL` for a physical +device, rerun the host codegen afterwards. + +## Troubleshooting + +**Port already in use** — `make doctor` reports 5433/8080/3000. Override the +database port with `FAM_PG_PORT=5434 docker compose up`. + +**TLS/handshake error connecting to the database** — the URL lost its +`?sslmode=disable`. `DatabaseClient` treats only `localhost` and `127.0.0.1` as +local and otherwise requires TLS; inside compose the host is `db`, so the +parameter is required. + +**Hot reload stopped firing** — file-watch propagation over virtiofs is the +flakiest part of the stack. `docker compose restart api`, or fall back to the +native path. + +**`db-init` refuses to run** — it is telling you `DIRECT_URL` does not point at a +local database. That guard is deliberate; check the URL rather than removing it. diff --git a/scripts/dev-backend.sh b/scripts/dev-backend.sh new file mode 100755 index 0000000..a024428 --- /dev/null +++ b/scripts/dev-backend.sh @@ -0,0 +1,46 @@ +#!/usr/bin/env bash +# Run the backend natively with hot reload, against whichever database is +# currently selected. +# +# source scripts/use-db.sh local +# ./scripts/dev-backend.sh +# +# Why not just `cd backend && dart_frog dev`? +# +# Several call sites read Platform.environment directly rather than going +# through the DotEnv instance built in main.dart — storage_utils.dart, +# routes/api/upload/{image,document}.dart, route_handlers/user_reserved_handlers.dart +# (SUPABASE_URL, SUPABASE_SERVICE_ROLE_KEY), routes/_middleware.dart (DART_ENV, +# ALLOWED_ORIGINS), utils/slot_lock.dart (UPSTASH_REDIS_*), and others. Values +# that live only in backend/.env are invisible to those. Exporting the file into +# the process environment first makes the native run behave like the container, +# where compose supplies everything as real environment variables. + +set -euo pipefail +cd "$(dirname "$0")/../backend" + +# Export .env, then .env.local, without clobbering anything already exported — +# an explicit `source scripts/use-db.sh` must still win. +load_env() { + [ -f "$1" ] || return 0 + while IFS= read -r line; do + case "$line" in ''|'#'*) continue ;; esac + key=${line%%=*} + key=${key// /} + [ -n "${!key:-}" ] && continue # already exported: leave it alone + export "${key?}=${line#*=}" + done < "$1" +} + +load_env .env +load_env .env.local + +if [ -z "${DIRECT_URL:-}${DATABASE_URL:-}" ]; then + echo "No DIRECT_URL/DATABASE_URL set." >&2 + echo "Run: source scripts/use-db.sh local" >&2 + exit 1 +fi + +./scripts/ensure-generated.sh + +exec "${HOME}/.pub-cache/bin/dart_frog" dev --port "${PORT:-8080}" diff --git a/scripts/gen-dev-seed.sh b/scripts/gen-dev-seed.sh new file mode 100755 index 0000000..64298dd --- /dev/null +++ b/scripts/gen-dev-seed.sh @@ -0,0 +1,66 @@ +#!/bin/bash +# Regenerate backend/prisma/sql/seed.d/ from the test prompts. +# +# The `## Data Seeding` blocks in prompts/testing/unit/*.md are the fixtures the +# agent-driven test prompts already assume exist. Rather than maintain a second, +# drifting copy of that data, this script extracts them. +# +# One file per prompt, NOT one concatenated script: the prompts were written to +# run standalone, so some reference tables that no longer exist in +# backend/prisma/schema.prisma. docker/db-init applies each file in its own +# transaction and reports the ones that do not apply, so drift is visible +# instead of aborting the whole seed. +# +# Run after editing any prompts/testing/unit/*.md seeding block. + +set -euo pipefail +cd "$(dirname "$0")/.." + +OUT_DIR=backend/prisma/sql/seed.d +rm -rf "$OUT_DIR" +mkdir -p "$OUT_DIR" + +count=0 +for f in prompts/testing/unit/*.md; do + base=$(basename "$f" .md) + out="$OUT_DIR/${base}.sql" + + { + printf -- '-- Generated from prompts/testing/unit/%s by scripts/gen-dev-seed.sh.\n' "$(basename "$f")" + printf -- '-- Do not edit directly; edit the prompt and regenerate.\n\n' + printf -- 'BEGIN;\n\n' + # The fenced ```sql block following the "## Data Seeding" heading. + # `-- execute_sql` is an agent directive, not SQL. + awk '/^## Data Seeding/{flag=1;next} flag&&/^```sql/{inb=1;next} inb&&/^```/{exit} inb{print}' "$f" \ + | grep -v '^-- execute_sql$' + printf -- '\nCOMMIT;\n' + } > "$out" + + # ── Disambiguate Domain.name ────────────────────────────────────────────── + # 17 prompts insert a Domain with a distinct id but the same name, + # 'Technology'. Domain.name is UNIQUE (Domain_name_key), and the prompts' + # `ON CONFLICT (id) DO NOTHING` does not cover a name collision, so they + # cannot coexist in one database. The rows cannot simply be dropped either: + # each block's ConsultantProfile references its OWN domainId. Suffixing the + # name with the row id keeps every foreign key intact and the names unique. + python3 - "$out" <<'PY' +import re, sys +p = sys.argv[1] +s = open(p).read() +s = re.sub( + r'INSERT INTO "Domain"[^;]*;', + lambda m: re.sub( + r"\('([^']+)',\s*'([^']+)'", + lambda t: "('%s', '%s [%s]'" % (t.group(1), t.group(2), t.group(1)), + m.group(0), + ), + s, +) +open(p, 'w').write(s) +PY + + count=$((count + 1)) +done + +echo "Wrote $count files to $OUT_DIR/" +grep -c 'INSERT INTO\|^UPDATE' "$OUT_DIR"/*.sql | awk -F: '{n+=$2} END{print " " n " statements total"}' diff --git a/scripts/regenerate-build.sh b/scripts/regenerate-build.sh index f3904f4..45e4bbb 100755 --- a/scripts/regenerate-build.sh +++ b/scripts/regenerate-build.sh @@ -18,6 +18,7 @@ export PATH="$HOME/.pub-cache/bin:$PATH" RUN_PRISMA=false RUN_DARTFROG=false RUN_FLUTTER=false +FORCE=0 if [ $# -eq 0 ]; then RUN_PRISMA=true @@ -36,9 +37,17 @@ else --prisma) RUN_PRISMA=true ;; + --force) + FORCE=1 + ;; *) echo "Unknown option: $arg" - echo "Usage: $0 [--backend] [--frontend] [--prisma]" + echo "Usage: $0 [--backend] [--frontend] [--prisma] [--force]" + echo "" + echo " --force Wipe and regenerate even when inputs are unchanged." + echo " Without it, codegen is skipped when prisma/schema.prisma," + echo " the connector/freezed versions and build.yaml all match" + echo " the last run." exit 1 ;; esac @@ -46,43 +55,23 @@ else fi # ============================================ -# 1. Prisma Dart Client (backend/lib/generated/) +# 1-2. Backend codegen (Prisma client + freezed), hash-guarded # ============================================ +# Previously these were two stages that each began by deleting their own +# output — `rm -rf backend/lib/generated` and a `find … | xargs rm -f` over +# every *.freezed.dart. That ran on every invocation regardless of whether +# anything had changed, destroying build_runner's asset graph and forcing a +# cold rebuild of ~735k generated lines each time. +# +# ensure-generated.sh now owns both stages and skips them when schema.prisma, +# the connector/freezed versions and build.yaml are all unchanged. Pass +# --force to get the old wipe-and-rebuild behaviour. if [ "$RUN_PRISMA" = true ]; then echo "" - echo "=== [1/4] Prisma Dart Client ===" - echo "Deleting backend/lib/generated/..." - rm -rf backend/lib/generated - - echo "Running dart pub get in backend/..." - cd "$ROOT_DIR/backend" - dart pub get - - echo "Generating Prisma client from schema..." - dart run prisma_flutter_connector:generate \ - --schema prisma/schema.prisma \ - --output lib/generated \ - --server - - cd "$ROOT_DIR" - echo "Prisma client generated." - - # ============================================ - # 2. Backend CodeGen (freezed/json for generated models) - # ============================================ - echo "" - echo "=== [2/4] Backend CodeGen (freezed/json for Prisma models) ===" - echo "Deleting backend *.g.dart and *.freezed.dart files..." + echo "=== [1-2/4] Backend codegen (Prisma client + freezed) ===" cd "$ROOT_DIR/backend" - # Keep lib/generated/schema_registry.g.dart: it is emitted by the Prisma - # generator (step 1), not by build_runner, so deleting it here loses it - find lib \( -name "*.g.dart" ! -path "*/generated/schema_registry.g.dart" -o -name "*.freezed.dart" \) | xargs rm -f 2>/dev/null || true - - echo "Running build_runner in backend/..." - dart run build_runner build --delete-conflicting-outputs - + FORCE_CODEGEN="$FORCE" ./scripts/ensure-generated.sh cd "$ROOT_DIR" - echo "Backend codegen complete." fi # ============================================ @@ -108,8 +97,15 @@ fi if [ "$RUN_FLUTTER" = true ]; then echo "" echo "=== [4/4] Flutter CodeGen ===" - echo "Deleting *.g.dart and *.freezed.dart files..." - find lib -name "*.g.dart" -o -name "*.freezed.dart" | xargs rm -f 2>/dev/null || true + # Only wipe on --force. Deleting every generated file discards build_runner's + # asset graph, which turns an incremental rebuild (seconds) into a cold one + # (minutes) across freezed + json_serializable + riverpod_generator + envied. + # --delete-conflicting-outputs below already resolves the collisions this + # was guarding against. + if [ "$FORCE" = "1" ]; then + echo "Deleting *.g.dart and *.freezed.dart files (--force)..." + find lib -name "*.g.dart" -o -name "*.freezed.dart" | xargs rm -f 2>/dev/null || true + fi echo "Running flutter pub get..." flutter pub get diff --git a/scripts/use-db.sh b/scripts/use-db.sh new file mode 100755 index 0000000..32119a5 --- /dev/null +++ b/scripts/use-db.sh @@ -0,0 +1,70 @@ +#!/usr/bin/env bash +# Point the NATIVE backend (`dart_frog dev` on the host) at a database. +# +# source scripts/use-db.sh local # the docker compose Postgres +# source scripts/use-db.sh supabase # the shared cloud database +# ./scripts/use-db.sh # just show what is currently set +# +# MUST be sourced, not executed — it exports into your shell. Because +# backend/main.dart applies Platform.environment last, an exported DIRECT_URL +# beats anything in backend/.env, so this works without editing any file. +# +# In Docker you do not need this: docker-compose sets DIRECT_URL directly. +# To point the containers at Supabase instead, set FAM_DIRECT_URL. + +_use_db_target="${1:-}" + +# Host port for the compose Postgres. Keep in sync with FAM_PG_PORT in +# docker-compose.yml (5432 is usually taken by a Homebrew postgres). +_use_db_local="postgresql://familiarise:familiarise@localhost:${FAM_PG_PORT:-5433}/familiarise?sslmode=disable" + +case "$_use_db_target" in + local) + export DIRECT_URL="$_use_db_local" + export DATABASE_URL="$_use_db_local" + echo "DB target: LOCAL (localhost:${FAM_PG_PORT:-5433}/familiarise)" + ;; + + supabase) + # Read from backend/.env.supabase rather than storing credentials here. + _use_db_file="$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")/.." && pwd)/backend/.env.supabase" + if [ ! -f "$_use_db_file" ]; then + echo "Missing $_use_db_file" >&2 + echo "Copy backend/.env.supabase.example to it and fill in the URLs." >&2 + return 1 2>/dev/null || exit 1 + fi + set -a + # shellcheck disable=SC1090 + . "$_use_db_file" + set +a + echo "DB target: SUPABASE (from backend/.env.supabase)" + ;; + + "") + if [ -n "${DIRECT_URL:-}" ]; then + # Strip credentials before printing. + _use_db_shown=${DIRECT_URL#*://} + echo "DB target: ${_use_db_shown##*@}" + else + echo "DB target: unset — backend/.env will decide." + echo "Run: source scripts/use-db.sh local" + fi + ;; + + *) + echo "Usage: source scripts/use-db.sh [local|supabase]" >&2 + return 1 2>/dev/null || exit 1 + ;; +esac + +# A DIRECT_URL left in backend/.env is harmless for precedence (the exported +# one wins) but is a trap for anyone reading the file, so say so once. +_use_db_envfile="$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")/.." && pwd)/backend/.env" +if [ -n "$_use_db_target" ] && [ -f "$_use_db_envfile" ] \ + && grep -qE '^\s*(DIRECT_URL|DATABASE_URL)=' "$_use_db_envfile"; then + echo "note: backend/.env still defines DIRECT_URL/DATABASE_URL." >&2 + echo " The exported value above takes precedence, but consider" >&2 + echo " moving those lines to backend/.env.supabase to avoid confusion." >&2 +fi + +unset _use_db_target _use_db_local _use_db_file _use_db_shown _use_db_envfile