The Receipt That Revealed the Gap: A Phase 3 Tale
Classic. One of those "10-minute fixes" that any developer worth their salt can knock out before lunch. It's probably just a conditional render gone wrong. Maybe someone forgot a ternary operator.
1. The Bug Report (This Will Be Quick)
"Receipt button is always showing 'generating...' for all donations."
Classic. One of those "10-minute fixes" that any developer worth their salt can knock out before lunch. It's probably just a conditional render gone wrong. Maybe someone forgot a ternary operator. Maybe they passed the callback unconditionally. This is literally Programming 101.
I opened the file with that special kind of confidence that comes from seeing a bug report and immediately knowing exactly what's wrong. You know the feeling — that warm glow of "I've fixed this before, I'll fix it again, and I'll be a hero by noon."
Narrator voice: He would not be a hero by noon.
The fix was indeed simple: one ternary operator. Change onDownloadReceipt={() => handleDownloadReceipt(item.id)} to onDownloadReceipt={item.receiptUrl ? () => handleDownloadReceipt(item.id) : undefined}. Done. Shipped. High-five yourself. Time to write the scribe log and move on to actual hard problems.
Except... maybe I should test it? Just to be sure? You know, professional due diligence and all that?
So I made a test payment.
And that's when things got interesting.
2. Following the Thread (Growing Dread)
The donation didn't appear in the app.
Not immediately. Not after 5 seconds. Not after I force-quit and reopened. Not after I sacrificed a rubber duck to the debugging gods.
"That's weird," I thought, with the serene optimism of someone who hasn't yet realized they're standing at the edge of a cliff. "Must be the real-time listener. Probably just needs to be initialized. Another 10-minute fix."
I opened DonationHistoryScreen.tsx to check the data source.
const donations = FEATURE_FLAGS.USE_MOCK_DATA
? MOCK_DONATIONS
: useAppSelector(state => state.donations.history);
Ah. Mock data. Right. That's why donations are showing — they're not real. They're the 83 lines of lovingly handcrafted MOCK_DONATIONS that someone (past me, probably) created during development. Complete with realistic names, amounts, and timestamps. So realistic, in fact, that I'd been testing receipt downloads against literal fiction this whole time.
TypeScript had been lying to me. Well, not lying exactly. More like... keeping up appearances. Maintaining a polite fiction that everything was fine while the actual database sat empty and unused, like a gym membership in February.
The plot thickens.
3. The "Looks Complete" Illusion (File-Based Completion Metrics Are a Lie)
I did what any developer does when facing mysterious bugs: I went looking for the implementation.
And oh boy, did I find implementation.
Files I Found:
- ✅
src/types/models.ts— Beautiful TypeScript interfaces forNeedandChild - ✅
src/store/slices/needsSlice.ts— Complete Redux slice with CRUD operations - ✅
src/store/slices/childrenSlice.ts— Another complete Redux slice - ✅
src/screens/NeedFormScreen.tsx— Production-ready UI form with validation - ✅
src/screens/DashboardScreen.tsx— Charity dashboard with "Add Child" button - ✅
backend/app/services/webhook_handler.py— Payment webhook looking up children/needs - ✅
backend/app/services/receipt_generator.py— Receipt service expecting child/need data - ✅
backend/app/services/firestore_service.py— Helper methods for get_child(), get_need()
Seven files. Over 2,000 lines of code. Thoughtfully organized. Well-typed. Following best practices. Passing all type checks with flying colors.
In any code review, this would get approved. "Phase 3: Needs Management" would get a ✅ in the project tracker. The interfaces are clean, the Redux patterns are consistent, the UI is polished. By every metric that matters to a file-counting project manager, this feature is 80% complete.
Firestore Collections:
/children/— 0 documents/needs/— 0 documents
Oh.
Oh no.
4. The Architecture That Was Never Built (The Beautiful Useless Engine)
Let me paint you a picture of what I discovered:
Imagine building a car. You've got:
- A pristine engine (TypeScript interfaces, perfectly designed)
- Beautiful leather seats (Redux slices, complete with actions and reducers)
- Gorgeous paint job (React Native UI components, pixel-perfect)
- State-of-the-art dashboard (DashboardScreen with all the bells and whistles)
- Professional-grade owner's manual (comprehensive JSDoc comments)
And then you look underneath and realize: there's no driveshaft.
The wheels aren't connected to the engine. The engine has never turned. The car has literally never moved. But hey, the paint looks fantastic, and TypeScript says everything's fine!
Here's what existed:
// Beautiful, pristine, never-been-called CRUD operations
export const createChild = createAsyncThunk(
'children/createChild',
async (childData: Partial<Child>, {rejectWithValue}) => {
try {
const docRef = await addDocument('children', childData);
return {id: docRef.id, ...childData};
} catch (error) {
return rejectWithValue(error.message);
}
}
);
This is gorgeous. Clean separation of concerns. Proper error handling. Type-safe. Ten out of ten, would approve in code review.
Here's what didn't exist:
Any code, anywhere, that actually calls createChild().
The "Add Child" button in DashboardScreen? It shows an alert telling you to "Please select a child from the Home tab, then create a need from their profile." Which is hilarious, because there are no children to select. The Home tab is showing mock data. The whole thing is a beautifully painted placeholder pointing at itself.
It's like those fake doors in hotels that are just painted on the wall. They look great! Very convincing! But you can't actually go through them, and if you try, you'll just end up with a bruised forehead and some existential questions about the nature of reality.
5. The Cascading Failure (Five Layers of "Oh No")
Once I started pulling the thread, the sweater unraveled fast.
Layer 1: The Receipt Failure
- Webhook tries to generate receipt
- Calls
firestore_service.get_child('4') - Returns
Nonebecause child doesn't exist - Receipt generation fails with "Child not found: 4"
Layer 2: The Missing Children
- Checkout screen passes
childId: '4'to payment intent - Child ID comes from mock data (hard-coded in Redux slice)
- Mock data has ID '4', Firestore has... nothing
- Payment succeeds, but receipt can't be generated
Layer 3: The Invisible Needs
- Need creation workflow exists (beautiful
NeedFormScreen.tsx) - Workflow is never accessible because child detail screen has no "Add Need" button
- Even if you somehow triggered it, there are no children to attach needs to
- So needs can never be created
- So donations can never reference real needs
- So receipts can never show what the donation was for
Layer 4: The Charity Dashboard Mirage
- Dashboard shows "Total Children: 12" (from mock data)
- "Active Needs: 24" (also mock data)
- "Create Need" button that shows an alert telling you to go somewhere else
- "Add Child" button that navigates to a form that isn't in the navigation stack
- The whole thing is a magnificent theatrical production with no audience
Layer 5: The Security Rules
- Firestore security rules carefully written to protect children/needs data
- Rules tested and deployed
- Protecting exactly zero documents
- Like putting a state-of-the-art alarm system on an empty warehouse
Each layer was hiding the next. The mock data masked the missing Firestore documents. The TypeScript types masked the missing function calls. The UI components masked the missing navigation flows. The Redux thunks masked the missing initialization logic.
It was architectural Russian dolls, except instead of finding smaller dolls inside, I kept finding increasingly large voids.
6. The Lesson: Code Structure ≠ Functionality (TypeScript Lied to Us)
Here's the thing that gets me:
Everything compiled.
TypeScript was happy. ESLint was happy. The type checker gave me a confident thumbs-up. The interfaces matched the implementations. The Redux actions had the right signatures. The components rendered without errors.
In TypeScript's world, this code was perfect.
In the real world, it was decorative.
I'd fallen into the classic trap: confusing "architecturally sound" with "actually works." The codebase had the structure of a working feature without the substance of a working feature. Like those cardboard cutouts of celebrities — looks right from a distance, but try to have a conversation with one and you'll be disappointed.
The False Signals:
- ✅ All functions have proper TypeScript signatures → But nobody calls them
- ✅ Redux state shape matches Firestore schema → But no data flows between them
- ✅ UI components have all the right props → But they're never rendered
- ✅ Error handling covers all cases → Of operations that never execute
It's the software equivalent of building a bridge that looks structurally perfect in the blueprints but was never actually connected to either side of the river. Sure, it could support traffic... if there were any way to get onto it.
What TypeScript Can't Tell You:
- Whether your functions are ever called
- Whether your data sources are populated
- Whether your navigation flows make sense
- Whether your feature is discoverable by users
- Whether clicking "Add Child" does literally anything
These are the gaps between "compiles" and "works." And I'd been living in those gaps for what was apparently several completed sprints.
7. The Fix: From Analysis to Action (753 Lines of Documentation FUN)
So I did what any sane developer does when facing an existential crisis: I wrote a 753-line analysis document.
Contents of needs-management-analysis.md:
- Executive Summary (the "how bad is it?" section)
- What Exists (the "look at all this beautiful useless code" section)
- What's Missing (the "oh god there's more" section)
- Recommended Task Structure (the "this will take 6 phases" section)
I broke it down into 6 phases:
- Phase 3.1: Firestore Schema & Security Rules (create the actual database)
- Phase 3.2: Child Management Workflows (connect the UI to the functions)
- Phase 3.3: Need Management Workflows (same but for needs)
- Phase 3.4: Charity Dashboard Analytics (make the numbers real)
- Phase 3.5: Data Seeding & Demo Environment (populate the void)
- Phase 3.6: Receipt Generation Fix (unblock Phase 4, which is currently blocked on Phase 3 which was marked complete)
Estimated time: 2 weeks.
Remember when this was going to be a 10-minute fix?
Things I Learned While Writing This Document:
- "80% complete" is a dangerous phrase when 100% of the implementation is scaffolding
- TODO comments age like milk (found one from 3 months ago: "TODO: Implement charity child creation flow")
- Code reviews that pass = features that work is a logical fallacy
- Phase numbers are sequential but completion is not (Phase 4 depends on Phase 3, which wasn't actually done)
8. The Deeper Issue: Visibility Gaps (We Tested Everything Except "Does It Work")
The really humbling part? This should have been caught earlier. Like, way earlier.
What We Tested:
- ✅ Type checking (passed)
- ✅ Linting (passed)
- ✅ Component rendering (passed)
- ✅ Redux actions (passed)
- ✅ Navigation transitions (passed)
- ✅ UI animations (beautiful)
What We Didn't Test:
- ❌ Can a charity user actually create a child?
- ❌ Can a child have needs added to them?
- ❌ Does a donation reference real data?
- ❌ Does the receipt generation work end-to-end?
- ❌ Are there any children in Firestore?
We had unit test coverage but not user workflow coverage. We tested that functions had the right signatures but not that anyone was calling them. We verified that components rendered but not that users could reach them.
It's like testing that a door is well-constructed without checking whether there's a hallway leading to it.
The Visibility Gap:
When you work in a well-architected codebase with strong typing and good component patterns, it's easy to mistake "clean" for "complete." The code felt done because it was well-organized. The Redux slice had all the right actions. The TypeScript interfaces were comprehensive. The components had proper prop validation.
But nobody had ever:
- Logged in as a charity user
- Clicked "Add Child"
- Filled out the form
- Submitted it
- Checked if a Firestore document was created
If we had, we would've discovered that step 2 shows an alert, step 3 never happens, and step 4 doesn't exist.
How This Happened:
Feature development went like this:
- Design data models → ✅ Perfect TypeScript interfaces
- Create Redux slices → ✅ All CRUD operations
- Build UI components → ✅ Production-ready forms
- Move to next phase → ⚠️ Nobody actually wired it up
The task tracker showed "Phase 3: Needs Management" as complete because all the files were created. But creating files ≠ creating functionality. It's the software engineering equivalent of buying all the ingredients for a recipe and declaring dinner ready without actually cooking anything.
9. What I'd Do Differently (Sarcastic Hindsight Wisdom)
Prevention Strategies I Will Definitely Implement This Time:
- End-to-End Smoke Tests
- Before marking a feature "complete," actually use it
- Revolutionary concept, I know
- "Can a user do the thing" is apparently a valid test case
- Firestore Collection Audits
- Check that collections have documents before building features that depend on them
- Crazy idea: verify the database has data before querying it
- Navigation Flow Verification
- Make sure buttons actually go somewhere
- Bonus points if they go somewhere that exists
- Extra bonus points if that somewhere does something
- Mock Data Expiration Dates
- Add comments: "TODO: Remove this mock data by [DATE]"
- Set calendar reminders
- Accept that I will ignore those reminders
- Feel guilty later when mock data becomes permanent
- Code Review Checklists
- "Does this function get called?" ☐
- "Does this collection have documents?" ☐
- "Can users access this UI?" ☐
- "Have you actually tested this end-to-end?" ☐
- "Or did you just check that it compiles?" ☐
- Architecture Documentation Reviews
- Document not just what exists, but what's wired up
- Separate "scaffolding" from "implementation"
- Stop using "80% complete" as a measure of anything
10. The Silver Lining (At Least We Found It Before Production)
Look, it could be worse.
We could have shipped this to production. Imagine the support tickets:
"I donated $100 but didn't get a receipt" "I can't add children to my charity profile" "The needs I created aren't showing up" "Your app is showing me made-up children named Emma Johnson who don't exist"
Instead, we found it during development. The donation system is working — it's creating Firestore documents, generating receipts, uploading PDFs to storage. The backend is solid. The webhook is processing payments. The infrastructure is sound.
We just... forgot to connect the frontend to it.
It's like building a house with plumbing but forgetting to install the faucets. The water's there! The pipes work! You just can't actually use any of it without some minor renovations.
What Actually Works:
- ✅ Payment processing (Stripe integration solid)
- ✅ Webhook handling (creating donations in Firestore)
- ✅ Receipt generation (PDFs are beautiful)
- ✅ File storage (Firebase Storage working great)
- ✅ Security rules (protecting that empty database like Fort Knox)
- ✅ Data models (TypeScript interfaces are chef's kiss)
What Needs Love:
- ❌ Connecting UI to CRUD operations
- ❌ Populating Firestore with initial data
- ❌ Making navigation buttons go to actual screens
- ❌ Testing that user workflows work
- ❌ Replacing mock data with real data
- ❌ Actually finishing Phase 3 before Phase 4
The foundation is good! The architecture is sound! We just need to, you know, build the rest of the house.
11. For Other Developers (You Will Make This Mistake Too)
If you're reading this and thinking "I would never make this mistake," congratulations on your confidence. Check back with me after you've been in the industry for a few years and tell me how that worked out.
This mistake is seductive because:
- TypeScript gives false confidence
- Everything compiles = everything works, right? (Wrong.)
- Type safety ≠ functionality
- The types can lie through omission
- Good architecture looks like progress
- Clean code feels productive
- Organized files look impressive in PRs
- But scaffolding ≠ implementation
- Mock data is too good
- Well-crafted test data hides missing real data
- "Looks right in the UI" becomes "is right in the database" (it isn't)
- The longer mock data lives, the harder it is to remove
- Component-based thinking obscures workflows
- Each component works in isolation = system works, right? (Wrong again.)
- You can have perfect components with no path between them
- Integration is a feature, not an assumption
- Code reviews optimize for code quality, not feature completeness
- Reviewers check: "Is this code good?" (Yes)
- Reviewers don't check: "Does this feature work?" (No)
- Both questions matter
Warning Signs You're Heading Here:
- ✋ You have Redux thunks that are never dispatched
- ✋ You have UI components that aren't in the navigation tree
- ✋ You have Firestore collections with zero documents
- ✋ You have feature flags that default to "use mock data"
- ✋ You have TODO comments from more than a month ago
- ✋ Your "Add X" buttons show alerts instead of adding X
- ✋ Your phase dependencies are out of order (Phase 4 blocked on incomplete Phase 3)
- ✋ You haven't actually used your own feature as a user would
If you see these signs: stop. Put down the keyboard. Test the feature end-to-end. Check the database. Click the buttons. Try to break it.
Because if you don't, you'll write a 753-line analysis document about how you built a beautiful car with no driveshaft.
(Trust me on this one.)
12. Where We Go From Here (Process Fixes With Checkbox Humor)
The Plan:
- Discover bug in receipt button
- Fix bug with one ternary operator
- Feel briefly competent
- Test the fix
- Discover donations don't appear
- Discover mock data
- Discover missing Firestore documents
- Discover incomplete Phase 3
- Write 753-line analysis
- Question life choices
- Write sarcastic blog post
- Actually implement Phase 3.1-3.6
- Populate Firestore with real data
- Test end-to-end workflows
- Remove mock data (for real this time)
- Update completion metrics to reflect reality
- Never trust "80% complete" again
Process Improvements:
- Define "Done" More Carefully
- Done = "files created" ❌
- Done = "types compile" ❌
- Done = "user can complete workflow" ✅
- Add E2E Workflow Tests
- Not just unit tests
- Not just component tests
- Actual "user does the thing" tests
- Database Checks Before Feature Approval
- Does the collection exist? ☐
- Does it have documents? ☐
- Can users create documents? ☐
- Can users read their documents? ☐
- Mock Data Sunset Policy
- All mock data gets TODO with date
- After 30 days, mock data is removed or justified
- Feature flags default to "real data" in production
- Dependency Validation
- Before starting Phase N, verify Phase N-1 is actually complete
- "Complete" means "works," not "files exist"
- Checkpoint Verification Includes Database
- After implementing CRUD, verify documents exist
- After building UI, verify it's navigable
- After marking complete, verify end-to-end
Timeline:
- Week 1: Phase 3.1-3.3 (Firestore + workflows)
- Week 2: Phase 3.4-3.6 (analytics + testing)
- Week 3: Phase 4 (actually complete payment integration)
- Week 4: Write another blog post about what went wrong this time
Lessons Learned (This Time For Real):
- Code structure is necessary but not sufficient
- TypeScript can't save you from missing features
- Mock data is training wheels, not a permanent solution
- End-to-end testing isn't optional
- "80% complete" is 0% complete
- Phase dependencies matter (don't skip Phase 3 and call Phase 4 done)
- Good architecture is worthless without implementation
- Check the database before celebrating
- TODO comments expire faster than you think
- One ternary operator can reveal an architecture gap
