Skip to content

Commit c3f2532

Browse files
committed
feat: revamp pantry and meal planning workflows
1 parent a560be1 commit c3f2532

14 files changed

Lines changed: 1939 additions & 297 deletions

.env.example

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,7 @@
22
# Required for local development when testing AI-assisted pantry parsing.
33
# Set this in your local .env.local and in Vercel project environment variables.
44
GEMINI_API_KEY="YOUR_GEMINI_API_KEY"
5+
6+
# Base URL for pinned catalog images (owned CDN/bucket). Required.
7+
# Example: https://cdn.example.com/rasoi/ingredients
8+
VITE_INGREDIENT_IMAGE_BASE_URL="https://cdn.example.com/rasoi/ingredients"

firestore.rules

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,21 @@ service cloud.firestore {
4545
// - newStatus: string (required, 'in-stock', 'low', 'out')
4646
// - timestamp: string (required, ISO date)
4747
// - role: string (required, 'owner', 'cook')
48+
//
49+
// Collection: households/{householdId}/unknownIngredientQueue
50+
// Document ID: auto-generated
51+
// Fields:
52+
// - name: string (required, max 100 chars)
53+
// - category: string (required, max 50 chars)
54+
// - status: string (required, 'open', 'resolved')
55+
// - requestedStatus: string (required, 'in-stock', 'low', 'out')
56+
// - createdAt: string (required, ISO date)
57+
// - createdBy: string (required, 'owner', 'cook')
58+
// - requestedQuantity: string (optional, max 50 chars)
59+
// - resolvedAt: string (optional, ISO date)
60+
// - resolvedBy: string (optional, 'owner', 'cook')
61+
// - resolution: string (optional, 'promoted', 'dismissed')
62+
// - promotedInventoryItemId: string (optional, max 100 chars)
4863
// ===============================================================
4964

5065
function isAuthenticated() {
@@ -129,6 +144,43 @@ service cloud.firestore {
129144
(!('cookLanguage' in data) || data.cookLanguage in ['en', 'hi']);
130145
}
131146

147+
function isValidUnknownIngredientQueueItem(data) {
148+
return data.keys().hasAll(['name', 'category', 'status', 'requestedStatus', 'createdAt', 'createdBy']) &&
149+
isValidString(data.name, 1, 100) &&
150+
isValidString(data.category, 1, 50) &&
151+
data.status in ['open', 'resolved'] &&
152+
data.requestedStatus in ['in-stock', 'low', 'out'] &&
153+
data.createdAt is string &&
154+
data.createdBy in ['owner', 'cook'] &&
155+
(!('requestedQuantity' in data) || isValidOptionalString(data.requestedQuantity, 0, 50)) &&
156+
(!('resolvedAt' in data) || data.resolvedAt is string) &&
157+
(!('resolvedBy' in data) || data.resolvedBy in ['owner', 'cook']) &&
158+
(!('resolution' in data) || data.resolution in ['promoted', 'dismissed']) &&
159+
(!('promotedInventoryItemId' in data) || isValidOptionalString(data.promotedInventoryItemId, 1, 100));
160+
}
161+
162+
function isValidUnknownQueueCreate(householdId, data) {
163+
return data.status == 'open' &&
164+
data.createdBy == effectiveRole(householdId) &&
165+
!('resolvedAt' in data) &&
166+
!('resolvedBy' in data) &&
167+
!('resolution' in data) &&
168+
!('promotedInventoryItemId' in data);
169+
}
170+
171+
function isValidUnknownQueueResolve(householdId, currentData, nextData) {
172+
return currentData.status == 'open' &&
173+
nextData.status == 'resolved' &&
174+
nextData.createdAt == currentData.createdAt &&
175+
nextData.createdBy == currentData.createdBy &&
176+
nextData.name == currentData.name &&
177+
nextData.category == currentData.category &&
178+
nextData.requestedStatus == currentData.requestedStatus &&
179+
nextData.resolvedBy == effectiveRole(householdId) &&
180+
nextData.resolution in ['promoted', 'dismissed'] &&
181+
nextData.resolvedAt is string;
182+
}
183+
132184
function isValidInventoryWrite(householdId, data) {
133185
return !('updatedBy' in data) || data.updatedBy == effectiveRole(householdId);
134186
}
@@ -198,6 +250,17 @@ service cloud.firestore {
198250
allow update: if false; // Logs are immutable
199251
allow delete: if isOwner(householdId);
200252
}
253+
254+
match /unknownIngredientQueue/{queueId} {
255+
allow read: if isHouseholdMember(householdId);
256+
allow create: if isHouseholdMember(householdId) &&
257+
isValidUnknownIngredientQueueItem(request.resource.data) &&
258+
isValidUnknownQueueCreate(householdId, request.resource.data);
259+
allow update: if isOwner(householdId) &&
260+
isValidUnknownIngredientQueueItem(request.resource.data) &&
261+
isValidUnknownQueueResolve(householdId, resource.data, request.resource.data);
262+
allow delete: if false;
263+
}
201264
}
202265

203266
// Keep legacy users path for migration

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
"unit:test": "node --import tsx test/unit/run.ts",
1414
"rules:test": "node test/rules/check-java.mjs && firebase emulators:exec --only firestore --project demo-rasoi-planner \"tsx test/rules/run.ts\"",
1515
"e2e": "node test/e2e/run.mjs",
16+
"e2e:headed": "E2E_HEADLESS=false node test/e2e/run.mjs",
1617
"verify:local": "npm run lint && npm run unit:test && npm run build && npm run rules:test && npm run e2e",
1718
"prepare": "husky"
1819
},

0 commit comments

Comments
 (0)