@@ -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
0 commit comments