Chat mode imported from briosoemilio/ambagan-be (
.github/chatmodes/expense.chatmode.md). Copyright stays with the author.
Master agent for the expenses Firestore collection in this Express/Firebase
Functions backend. An "expense" is money spent out of a project's collected
funds — the counterpart to an ambag (money coming in). This is the newest
of the four collections (see recent history: "Initial changes to expenses
feature"), so expect rougher edges than ambags/projects.
Scope
Owns everything related to the expenses collection: its routes, request
validation schema, and ownership authorization. Aware of but does not own:
projects (every expense belongs to one project via projectId; creating
or deleting an expense updates the parent project's expensesCount and
expensesTotal, and project metrics compute totalExpenses/
remainingBalance from this collection — see the project agent's
buildProjectMetrics), and uploads (a receipt photo can link an expense
back to an upload document, same pattern as ambags).
Key files
functions/src/routes/expenses.ts— CRUD routes, all mounted behindauthenticated:GET /(requiresprojectIdquery param + project membership),GET /:id,POST /(checksproject.expensesEnabledbefore allowing creation),PATCH /:id,DELETE /:id, plusPOST /uploadfor receipt photo upload (delegates tohandleFileUpload, identical to the ambags upload route).functions/src/schemas/ExpenseSchema.ts— Zod schema:projectId,amount(number),createdAt(optional, coercible date),note,receipt {photoUrl, uploadId}. Notably nocontributor/payer field unlikeAmbagSchema— an expense currently has no record of who it was paid to or by beyondcreatedBy. PATCH usesExpenseSchema.omit({createdAt: true}).partial().functions/src/middlewares/authorizeExpenseOwner.ts— allows the expense'screatedByuser or the parent project'screatedBy(owner) to PATCH/DELETE; 403 otherwise. 500s on data-integrity issues (missingprojectId, orphaned project reference), same pattern asauthorizeAmbagOwner.functions/src/middlewares/authorizeProjectMember.ts— gatesGET /(list) to project members/creator; shared with the ambag/project agents' scope.functions/src/middlewares/validateBody.ts— generic Zod-based body validator used byPOST/PATCH.functions/src/utils/handleFileUpload.ts— shared upload handler, same as the one used byPOST /ambags/upload.functions/src/constants/Collection.ts—Collection.EXPENSES.
Conventions to follow
- Auth: router-level
expensesRouter.use(authenticated)plus a redundant per-routeauthenticated— keep both when adding routes (same intentional redundancy asambagsRouter). POST /checksprojectDoc.data()?.expensesEnabled === falseinside the transaction and throws"Expenses are disabled for this project"(mapped to a 403) if so. This is a project-level feature flag, analogous toprogressEnabledin the project agent's scope — preserve this check if touchingPOST /.- Side effects on create/delete: always keep
projectRef.update({expensesCount: FieldValue.increment(...), expensesTotal: FieldValue.increment(...)})in sync when touching create/delete — unlike ambags (count only), expenses track both a count and a running total, and the delete path guardsamountwith atypeof === "number"check before negating it. Don't drop that guard. - All
expenses.tshandlers (GET,POST,PATCH,DELETE) consistentlyreturn res.status(...)...on every branch, including success paths — keep that pattern for any new branches. - Firestore access always goes through
admin.firestore().collection(Collection.EXPENSES)— never hardcode"expenses". - Multi-document writes (expense + upload link) use
admin.firestore().runTransaction; theexpensesCount/expensesTotalincrement happens outside that transaction, after it commits — same non-atomic pattern as ambags, follow it rather than folding it in unless asked to make it atomic. - Timestamps:
createdAtaccepts a client-supplied value and falls back toFieldValue.serverTimestamp();updatedAton PATCH is always server-set.
Known gaps to flag before extending
- No payer/recipient info on an expense (no
contributor-equivalent field) — if asked to show "who paid this expense" in metrics or UI-facing data, this field doesn't exist yet and needs to be added toExpenseSchemafirst.
When making changes
- Keep
routes/expenses.tsfocused on request handling; validation lives inExpenseSchema, ownership checks live inauthorizeExpenseOwner. - If a change affects
amountor the create/delete paths, double check both theexpensesCountandexpensesTotalincrements/decrements on the related project still balance. - If a change affects the
receiptshape, check it stays structurally identical toAmbagSchema'sreceiptfield — both feed the samehandleFileUploadflow. - Run lint/build via the
functionspackage (npm run lint,npm run buildinsidefunctions/) before considering a change done.