diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7537a7b..4dcee7b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,11 @@
All notable changes to Epicure are documented here. This file is mirrored in-app at `/changelog` (and in the admin dashboard) via `apps/web/lib/changelog.ts` — update both together.
+## 0.76.0 — 2026-07-24 12:15
+
+### Added
+- Settings → Billing now has a billing/invoice details section: full name (required), address, and phone number (both optional). Separate from your display name — this is what will appear on future invoices.
+
## 0.75.0 — 2026-07-24 11:30
### Added
diff --git a/FEATURE_AUDIT.md b/FEATURE_AUDIT.md
index 97c141f..bdf7b1e 100644
--- a/FEATURE_AUDIT.md
+++ b/FEATURE_AUDIT.md
@@ -70,6 +70,7 @@ Status legend: **Exists** (fully working) · **Partial** (works but with a real
| Pantry manual CRUD | Exists | Includes a bulk case-insensitive name+unit merge endpoint (used by the scan-confirm flow), undocumented until this pass | `apps/web/app/api/v1/pantry/**` |
| Auto-deduct pantry on cook | Exists (closed 2026-07-24) | Both UI callers that previously hardcoded `deductFromPantry: false` (`batch-cook-dishes.tsx`, `meal-planner.tsx`) now pass `true`. The new general "Mark cooked" feature (see below) additionally exposes it as a per-cook checkbox, default checked, rather than a silent always-on. | `apps/web/app/api/v1/recipes/[id]/cooked/route.ts`, `apps/web/components/recipe/batch-cook-dishes.tsx`, `apps/web/components/meal-plan/meal-planner.tsx` |
| Mark recipe as cooked (any recipe, not just batch-cook) | Exists (new, 2026-07-24) | A recipe can be logged as cooked any number of times — each log is a new `cookingHistory` row, no unique constraint, this was already true at the schema level, just not exposed for plain (non-batch) recipes before. New dialog on the recipe page: date (defaults today, can backdate), servings, and a deduct-from-pantry toggle (default on). Shows a "cooked N times · last DATE" indicator once logged. | `apps/web/components/recipe/{mark-cooked-dialog,mark-cooked-section}.tsx`, `apps/web/app/api/v1/recipes/[id]/cooked/route.ts` (`cookedAt` field, new) |
+| Billing/invoice details (full name, address, phone) | Exists (new, 2026-07-24) | New `userBillingDetails` table (1:1 with `users`), separate from the display `name` field. `fullName` is required by the form/API but nullable at the DB level (no backfill for existing users); address lines/city/postal code/country/phone are all optional. Lives under `Settings → Billing`, feeds future invoice generation — not wired into Stripe yet. | `packages/db/src/schema/billing.ts` (`userBillingDetails`), `apps/web/app/api/v1/users/me/billing-details/route.ts`, `apps/web/components/settings/billing-details-form.tsx` |
| Expiring-soon tracking | Exists | With "use it up" recipe suggestions. The leftover-expiry push/email cron only covers batch-cook dishes — plain pantry items only get an in-app badge, no reminder. | `apps/web/components/pantry/expiring-soon-suggestions.tsx`, `apps/web/app/api/internal/cron/leftover-reminders/route.ts` |
| **Barcode scan (pantry)** | **Exists** | Real Open Food Facts API lookup by UPC/EAN | `apps/web/app/api/v1/pantry/scan/barcode` |
| Photo scan (pantry) | Exists | Vision-model based | `apps/web/app/api/v1/pantry/scan/photo` |
diff --git a/apps/web/app/(app)/settings/billing/page.tsx b/apps/web/app/(app)/settings/billing/page.tsx
index 74a92ad..0e4dd03 100644
--- a/apps/web/app/(app)/settings/billing/page.tsx
+++ b/apps/web/app/(app)/settings/billing/page.tsx
@@ -1,11 +1,12 @@
import type { Metadata } from "next";
import { headers } from "next/headers";
import { auth } from "@/lib/auth/server";
-import { db, users, tierDefinitions, tierEnum, userUsage, eq, and } from "@epicure/db";
+import { db, users, tierDefinitions, tierEnum, userUsage, userBillingDetails, eq, and } from "@epicure/db";
import { getRecipeCount, getStorageUsedMb, UNLIMITED } from "@/lib/tiers";
import { BillingPlanCards } from "@/components/settings/billing-plan-cards";
import { ManageBillingButton } from "@/components/settings/manage-billing-button";
import { UsageQuotaSection } from "@/components/settings/usage-quota-section";
+import { BillingDetailsForm } from "@/components/settings/billing-details-form";
import { Badge } from "@/components/ui/badge";
export const metadata: Metadata = {};
@@ -23,7 +24,7 @@ export default async function BillingPage({ searchParams }: { searchParams: Prom
const currentMonth = new Date().toISOString().slice(0, 7);
- const [dbUser, allTierDefs, usage, recipeCount, storageUsedMb] = await Promise.all([
+ const [dbUser, allTierDefs, usage, recipeCount, storageUsedMb, billingDetails] = await Promise.all([
db.query.users.findFirst({
where: eq(users.id, session.user.id),
columns: { tier: true, subscriptionStatus: true, currentPeriodEnd: true, stripeCustomerId: true },
@@ -32,6 +33,7 @@ export default async function BillingPage({ searchParams }: { searchParams: Prom
db.query.userUsage.findFirst({ where: and(eq(userUsage.userId, session.user.id), eq(userUsage.month, currentMonth)) }),
getRecipeCount(session.user.id),
getStorageUsedMb(session.user.id),
+ db.query.userBillingDetails.findFirst({ where: eq(userBillingDetails.userId, session.user.id) }),
]);
const currentTier = dbUser?.tier ?? "free";
@@ -100,6 +102,14 @@ export default async function BillingPage({ searchParams }: { searchParams: Prom
+
+
+
Billing details
+
Used on invoices. Full name is required; address and phone are optional.