Skip to content

Commit e2141dd

Browse files
committed
feat: update monetary handling across the application to use Decimal type for improved precision, enhance product and purchase models, and refactor related functions for consistent currency management
1 parent d7da39c commit e2141dd

33 files changed

Lines changed: 976 additions & 109 deletions

File tree

.env.example

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@ DIRECT_URL="postgresql://postgres.[project-ref]:[password]@aws-0-[region].pooler
88
# Supabase Client Variables
99
NEXT_PUBLIC_SUPABASE_URL="https://[project-ref].supabase.co"
1010
NEXT_PUBLIC_SUPABASE_ANON_KEY="your-anon-key"
11+
# Server-only: signed download URLs, product file listing, admin storage deletes
12+
# Dashboard: Project Settings > API > service_role (never expose to the client)
13+
SUPABASE_SERVICE_ROLE_KEY="your-service-role-key"
1114

1215
# Upstash Redis Variables
1316
UPSTASH_REDIS_REST_URL="https://[project-url].upstash.io"

AGENTS.md

Lines changed: 257 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,257 @@
1+
# AGENTS.md
2+
3+
Guidance for AI coding agents working in the Devix repository. Primary branch: `main`. This file is authoritative over the stale root `README.md`.
4+
5+
For architecture, data flows, trade-offs, and technical debt, see [`DESIGN.md`](DESIGN.md).
6+
7+
## Project Summary
8+
9+
Devix is a premium software-agency platform: marketing site, portfolio, project estimator, digital product store, and admin CMS/dashboard. Single Next.js app (not a monorepo), managed with Bun, deployed on Vercel (region `sin1`).
10+
11+
## Stack & Tooling
12+
13+
- **Framework:** Next.js 16 App Router, React 19, TypeScript 6, React Compiler, typed routes/env
14+
- **Database:** Prisma 7 + `@prisma/adapter-pg` → Supabase PostgreSQL (pooler for runtime, direct for migrations)
15+
- **Storage:** Supabase Storage (product files, media library)
16+
- **Cache / rate limits:** Upstash Redis (optional in dev — graceful fallback when unset)
17+
- **Email:** Resend
18+
- **Payments:** Stripe and Lemon Squeezy behind a provider abstraction
19+
- **Auth:** Custom admin sessions (bcrypt + JWE cookies) — **not** Supabase Auth or NextAuth
20+
- **Testing:** Vitest (unit), Playwright (e2e visual baselines — not in CI)
21+
- **Lint/format:** ESLint flat config, Prettier with Tailwind plugin
22+
23+
Use **Bun** for all scripts (`bun dev`, `bun test`, etc.). Do not assume npm/pnpm.
24+
25+
| Command | Purpose |
26+
|---------|---------|
27+
| `bun dev` | Prisma generate + Next dev server |
28+
| `bun build` / `bun start` | Production build and serve |
29+
| `bun lint` / `bun type-check` / `bun test` | CI-equivalent checks |
30+
| `bun db:seed` | Seed CMS, portfolio, sample products |
31+
32+
## Repository Layout
33+
34+
```
35+
src/
36+
├── app/ # Routes: (public)/, admin/(dashboard)/, api/webhooks/
37+
├── actions/ # Server actions (public + admin/)
38+
├── lib/ # Business logic, payment, queries, schemas, redis
39+
├── components/ # atoms/, molecules/, organisms/, admin/
40+
├── proxy.ts # Request proxy (rate limits + admin auth gate)
41+
├── hooks/, types/, utils/, content/
42+
prisma/ # schema.prisma, migrations/, seed.ts
43+
e2e/ # Playwright tests
44+
```
45+
46+
**Layering:** `app/` pages → `actions/` (mutations) or `lib/queries/` (reads) → `lib/` → Prisma / Supabase / Redis / payment SDKs.
47+
48+
## Local Setup & Required Env
49+
50+
1. Copy [`.env.example`](.env.example)`.env.local`
51+
2. Fill Supabase Postgres URLs (`DATABASE_URL` pooler port 6543, `DIRECT_URL` direct port 5432)
52+
3. Run migrations: `bunx prisma migrate deploy` (uses `DIRECT_URL` via [`prisma.config.ts`](prisma.config.ts))
53+
54+
**Build fails without:** `DATABASE_URL`, `JWT_SECRET`, `NEXT_PUBLIC_SUPABASE_URL`, `NEXT_PUBLIC_SUPABASE_ANON_KEY` (enforced in [`next.config.ts`](next.config.ts)).
55+
56+
See [`.env.example`](.env.example) for Stripe, Lemon Squeezy, Resend, Turnstile, Redis, and payment provider vars. Do not duplicate the full list here.
57+
58+
## Architecture Overview
59+
60+
```mermaid
61+
flowchart LR
62+
subgraph public [PublicSite]
63+
Pages[AppRouterPages]
64+
Actions[ServerActions]
65+
Queries[lib/queries]
66+
end
67+
subgraph admin [AdminDashboard]
68+
AdminActions[admin/actions]
69+
Proxy[proxy.ts]
70+
end
71+
subgraph infra [Infrastructure]
72+
Prisma[(PostgreSQL)]
73+
Storage[SupabaseStorage]
74+
Redis[UpstashRedis]
75+
Pay[StripeOrLemonSqueezy]
76+
end
77+
Pages --> Queries --> Prisma
78+
Pages --> Actions --> Prisma
79+
Actions --> Redis
80+
AdminActions --> Proxy
81+
AdminActions --> Prisma
82+
AdminActions --> Storage
83+
StoreCheckout[StoreCheckout] --> Pay
84+
Pay --> Webhooks[api/webhooks]
85+
Webhooks --> Fulfillment[purchase-fulfillment]
86+
Fulfillment --> Prisma
87+
Fulfillment --> Email[Resend]
88+
DownloadRoute["/download/token"] --> Storage
89+
```
90+
91+
Three domains to respect:
92+
93+
1. **Public site** — cached Prisma reads, form actions (contact, estimator, checkout)
94+
2. **Admin CMS** — authenticated server actions, Supabase uploads, cache invalidation
95+
3. **Store** — checkout starts payment; **webhooks only** create `Purchase` rows and download tokens
96+
97+
## Request Proxy, Auth & Security
98+
99+
**Proxy:** [`src/proxy.ts`](src/proxy.ts) is the Next.js 16 request proxy (not `middleware.ts`). It applies global and `/api/` rate limits and gates `/admin/*` routes via session cookies.
100+
101+
**Admin auth (custom):**
102+
103+
- Users in Prisma `User` model; passwords hashed with bcrypt
104+
- Sessions: JWE-encrypted HTTP-only cookies via `jose` ([`src/lib/auth.ts`](src/lib/auth.ts), [`src/lib/session-token.ts`](src/lib/session-token.ts))
105+
- Cookie names: `__Host-devix_session` (HTTPS prod), `devix_session` (dev)
106+
- Defense in depth: proxy → dashboard layout `verifyAdminSession()` → per-action auth + `verifyCsrfOrigin()`
107+
- Flat admin model: max 4 users, `isActive` flag; public `/admin/register` is disabled (invite-only)
108+
109+
**Other security:** Production CSP and security headers in `next.config.ts`; Cloudflare Turnstile on checkout (optional); Bearer token on [`src/pages/api/health.ts`](src/pages/api/health.ts).
110+
111+
Do not introduce Supabase Auth, NextAuth, or RBAC unless explicitly requested.
112+
113+
## Supabase Usage (Postgres + Storage Only)
114+
115+
- **Queries:** Prisma → Postgres (no Supabase JS for DB access)
116+
- **No RLS** — authorization is application-layer only
117+
- **Browser client:** [`src/lib/supabase-browser.ts`](src/lib/supabase-browser.ts) — admin media uploads
118+
- **Service client:** [`src/lib/supabase-admin.ts`](src/lib/supabase-admin.ts) — signed download URLs, product bucket `products`
119+
- [`src/lib/supabase-server.ts`](src/lib/supabase-server.ts) exists but is unused — do not wire new code through it without reason
120+
121+
Requires `SUPABASE_SERVICE_ROLE_KEY` for downloads and admin storage operations (see `.env.example`).
122+
123+
## Money & Pricing (Decimal)
124+
125+
All persisted money uses Prisma **`Decimal`**, not `Float` or `Int`. Helpers: [`src/lib/money.ts`](src/lib/money.ts).
126+
127+
| Field | Semantics |
128+
|-------|-----------|
129+
| `Product.price` | Major units (`Decimal(19,4)`) — admin input / display |
130+
| `Product.priceMinor` | Smallest currency unit (`Decimal(19,0)`) — canonical charge amount |
131+
| `Purchase.amountMinor` | Paid snapshot at fulfillment |
132+
| `EstimatorLead.budgetUsd` / `deliverableSavingsUsd` | USD quote amounts (`Decimal(19,2)`) |
133+
134+
**Rules for agents:**
135+
136+
- Use `resolveProductAmount()` before checkout; prefer `priceMinor` when set
137+
- Use `formatMinor()` / `formatUsdDecimal()` for display — not raw `.toFixed()` on floats
138+
- Convert to Stripe/Lemon `number` only at SDK boundary via `minorToStripeUnit()` / `stripeUnitToMinor()`
139+
- Import `Decimal` from `@/lib/money` (re-exported from Prisma runtime)
140+
- Do not pass raw `Decimal` to client components — serialize to string in props
141+
142+
See [DESIGN.md §7](DESIGN.md#7-money--pricing-model) for full model and migration notes.
143+
144+
## Payments (Dual Provider Abstraction)
145+
146+
- Interface: [`src/lib/payment/types.ts`](src/lib/payment/types.ts)`PaymentProvider`
147+
- Factory: [`src/lib/payment/index.ts`](src/lib/payment/index.ts)`getPaymentProvider()`
148+
- Active provider resolution ([`src/lib/payment/config.ts`](src/lib/payment/config.ts)): Admin `SiteSettings.paymentProvider``PAYMENT_PROVIDER` env → `"stripe"`
149+
- Stripe: embedded checkout on `/store/checkout`; Lemon Squeezy: overlay checkout, requires `Product.lemonSqueezyVariantId`
150+
151+
**Critical:** Fulfillment is **webhook-only**. Checkout actions in [`src/actions/purchase.ts`](src/actions/purchase.ts) never create `Purchase` records.
152+
153+
- Webhooks: [`src/app/api/webhooks/stripe/route.ts`](src/app/api/webhooks/stripe/route.ts), [`src/app/api/webhooks/lemonsqueezy/route.ts`](src/app/api/webhooks/lemonsqueezy/route.ts)
154+
- Idempotency: `ProcessedPaymentEvent` table
155+
- Fulfillment: [`src/lib/purchase-fulfillment.ts`](src/lib/purchase-fulfillment.ts)
156+
- Revocation (refunds/disputes): [`src/lib/purchase-revocation.ts`](src/lib/purchase-revocation.ts)
157+
158+
When editing Stripe webhooks, follow the existing inline pattern in that route (it partially bypasses the provider abstraction). Lemon webhooks use the abstraction consistently.
159+
160+
## Digital Store & Download Tokens
161+
162+
**Models:** `Product`, `Purchase` in [`prisma/schema.prisma`](prisma/schema.prisma).
163+
164+
**Checkout flow:** User pays → provider webhook → fulfillment creates `Purchase` with `downloadToken` → confirmation email links to `/download/{token}`.
165+
166+
**Download limits (not strictly one-time):**
167+
168+
- Default **max 3 downloads** within a **24-hour** window from fulfillment
169+
- `tokenUsed` set when `downloadCount >= maxDownloads`
170+
- `revokedAt` blocks access (refunds, disputes, admin revoke)
171+
- `deliveredFileKey` snapshots the product file at purchase time
172+
- Each download request gets a signed Supabase URL (300s TTL) via [`src/lib/download-token.ts`](src/lib/download-token.ts)
173+
174+
Status pages: `/download/used`, `/expired`, `/revoked`, `/invalid`, `/error`. Admin can rotate tokens or resend email via [`src/actions/admin/purchases.ts`](src/actions/admin/purchases.ts).
175+
176+
## Data Layer (Prisma)
177+
178+
- `relationMode = "prisma"` — Supabase-friendly; no DB-level FK enforcement
179+
- Client singleton: [`src/lib/prisma.ts`](src/lib/prisma.ts) with `pg` Pool adapter
180+
- Migrations in `prisma/migrations/`; seed with `bun db:seed`
181+
182+
**Model groups:** CMS (`SiteSection`, `FaqItem`, `ServiceItem`, `TeamMember`, `SiteSettings`), portfolio (`Project`), CRM (`ContactSubmission`, `EstimatorLead`, `ConsultationFeedback`), commerce (`Product`, `Purchase`, `ProcessedPaymentEvent`), audit (`ActivityLog`, `MediaAsset`).
183+
184+
## Caching & Rate Limiting
185+
186+
[`src/lib/redis.ts`](src/lib/redis.ts): `cachedQuery()` for read-through cache, `invalidateCache()` on admin writes. Returns `null` when Upstash env is missing — code must not assume Redis is available.
187+
188+
Rate limiters in [`src/lib/rate-limit.ts`](src/lib/rate-limit.ts) cover auth, contact, purchase, download, API, etc. When adding admin mutations that change cached content, follow existing actions and call `invalidateCache()` with the same keys.
189+
190+
## Server Actions vs API Routes
191+
192+
**Default to server actions** (`"use server"` in [`src/actions/`](src/actions/)) for all UI mutations.
193+
194+
**API routes only for:**
195+
196+
- Payment webhooks (raw body + signature verification)
197+
- Admin CSV export ([`src/app/api/admin/leads/export/route.ts`](src/app/api/admin/leads/export/route.ts))
198+
- Legacy health check ([`src/pages/api/health.ts`](src/pages/api/health.ts) — Pages Router)
199+
200+
**Page data:** use [`src/lib/queries/`](src/lib/queries/), not server actions.
201+
202+
**Error patterns:**
203+
204+
- Public actions: return `{ success/ok, error? }` — do not throw
205+
- Admin actions: often `throw new Error(...)` after Zod/auth failures
206+
- Validation: Zod schemas in [`src/lib/schemas.ts`](src/lib/schemas.ts); use `safeParse` + first issue message
207+
208+
Server action body limit: 50mb (large admin media uploads).
209+
210+
## Code Conventions
211+
212+
- Imports: `@/` alias only; keep imports at top of file (no inline imports)
213+
- Semicolons, double quotes, 2-space indent (Prettier)
214+
- Components: PascalCase (`StoreProductCard.tsx`); lib modules: kebab-case (`purchase-fulfillment.ts`)
215+
- Tests: sibling `test/` folders, `*.test.ts`; mock at module boundaries (`@/lib/prisma`, not `@prisma/client`)
216+
- See [`src/lib/test/mocks/README.md`](src/lib/test/mocks/README.md) for mock kit conventions
217+
- `no-console` enforced in production builds
218+
- Use exhaustive `never` checks in switch defaults over discriminated unions
219+
- Minimize scope; match surrounding code style
220+
221+
## Domain Features (Pointers)
222+
223+
- **Project estimator:** [`src/lib/estimator-*.ts`](src/lib/estimator-steps.ts), leads via [`src/actions/estimator-leads.ts`](src/actions/estimator-leads.ts)
224+
- **CMS:** JSON blobs in `SiteSection` plus relational FAQ/services/team tables
225+
- **Maintenance mode:** `SiteSettings.maintenanceMode` + [`MaintenanceGate`](src/components/MaintenanceGate.tsx)
226+
- **Media library:** Supabase Storage + `MediaAsset` metadata in Prisma
227+
228+
## Testing & CI
229+
230+
- **Unit:** Vitest; run `bun test`. Colocated tests with shared mock registry in `src/lib/test/mocks/`
231+
- **E2E:** Playwright in `e2e/` — not run in main CI workflow
232+
- **CI** ([`.github/workflows/ci.yaml`](.github/workflows/ci.yaml) on `main` and `dev`): lint → type-check → test → build
233+
234+
**Before claiming work is done:** run `bun lint`, `bun type-check`, and `bun test`.
235+
236+
## Gotchas & Anti-Patterns
237+
238+
- Do **not** use Supabase Auth or add `middleware.ts` — use [`src/proxy.ts`](src/proxy.ts)
239+
- Do **not** fulfill purchases outside webhook handlers
240+
- Do **not** assume Redis or Upstash is always configured
241+
- Do **not** add RBAC without explicit request (flat admin today)
242+
- Do **not** trust root `README.md` — it is stale create-next-app boilerplate
243+
- CSP changes require updating allowlists in [`next.config.ts`](next.config.ts) (Stripe, Turnstile, Vercel Insights)
244+
- Guest checkout only — no customer accounts; buyers identified by email on `Purchase`
245+
- Legacy Pages Router health endpoint — do not migrate without request
246+
247+
## Read More (When Working in These Areas)
248+
249+
| Area | Source of truth |
250+
|------|-----------------|
251+
| Architecture & trade-offs | [`DESIGN.md`](DESIGN.md) |
252+
| Environment variables | [`.env.example`](.env.example) |
253+
| Database schema | [`prisma/schema.prisma`](prisma/schema.prisma) |
254+
| Money helpers | [`src/lib/money.ts`](src/lib/money.ts) |
255+
| Unit test mocks | [`src/lib/test/mocks/README.md`](src/lib/test/mocks/README.md) |
256+
| Payment providers | [`src/lib/payment/`](src/lib/payment/) |
257+
| Purchase fulfillment | [`src/lib/purchase-fulfillment.ts`](src/lib/purchase-fulfillment.ts) |

0 commit comments

Comments
 (0)