▓▓▒▒ FLOAT.BBS ▒▒▓▓
Date: 2025-08-23 [note]
      
← Back to evans-notes

The Full Journey

VAT Rate Debugging Journey - The Complete Resolution

The Journey: From Complex to Simple

Started with issue #347 reporting three bugs:

  1. Duplicate VAT rates appearing when edit/cancel
  2. Wrong VAT rate opening when clicking edit
  3. Display inconsistencies (0.05% instead of 5%, 500% instead of 5%)

Adams’ feedback pushed us toward integer storage and reusable components, which led to an architectural rabbit hole:

The Actual Problem: A Single Character

After hours of architectural discussion and refactoring attempts, the real issues were embarrassingly simple:

  1. Assignment instead of comparison (line 32 in vat-rates/page.tsx):

    // WRONG: Assignment operator
    const editItem = edit ? result.find(item => (item.id = edit)) : undefined
    
    // CORRECT: Comparison operator  
    const editItem = edit ? result.find(item => item.id === Number(edit)) : undefined
  2. Wrong data format in database:

    • Had: 20 for 20%, 5 for 5%
    • Needed: 2000 for 20%, 500 for 5% (basis points)
    • formatPercentage expected basis points but got whole numbers

The Solution: Minimal Changes

  1. Fixed the === bug: One character change
  2. Manually updated VAT rates: Since only 5 rates in pre-prod, just used the UI:
    • 20 → 2000 (Standard)
    • 15 → 1500 (Reduced)
    • 5 → 500 (test to delete)
    • 3 → 300 (new)
    • 0 → 0 (Zero)

No migration script needed. The system was already designed correctly:

Lessons Learned

  1. Check for typos first: A single = vs === caused hours of debugging
  2. Verify data format: The system was correct, the data was wrong
  3. Don’t over-engineer: We built components we didn’t need
  4. Existing patterns work: NumberInput already did what we needed
  5. Simple fixes are often right: One line of code fixed three bugs

Technical Details

The system now correctly implements basis points storage:

Final Outcome

✅ All three bugs from issue #347 resolved ✅ System supports decimal VAT rates (17.5% for UK) ✅ Minimal code changes (1 line + gitignore) ✅ No unnecessary components or refactoring ✅ Lesson in humility: sometimes it really is that simple

The Commit

git commit -m "fix: VAT rate edit bug - wrong rate opening on edit (issue #347)

- Fixed assignment operator bug (= vs ===) that caused wrong VAT rate to open
- Added .evans-notes and .opencode to gitignore

The system already supports basis points storage (multiply by 100) but existing data
needed manual correction through UI (20 → 2000 for 20%, etc)."

Reflection

This journey perfectly illustrates why stepping back and checking fundamentals matters. We spent hours discussing architecture, building hooks, creating components, and debating patterns - when the real problem was a typo and wrong test data. The existing system was already well-designed; we just couldn’t see it through the complexity we were adding.

Sometimes the best code is the code you don’t write.

wait a minute…

Change was simpler than expected, once Adam pointed out it was int for a reason, and then after some unesssiary reafctoring, realized ‘oh, don’t need that extra component’ …

and .. now down to like a two lines fix in one file.

VAT Fixes persists

Adam left a comment on the PR

[!note] odziem 20 hours ago @e-schultz Looking good. Regarding the database schema change, we actually intentionally changed it from a decimal/numerical field to an integer to avoid precision errors. See the history of vat_rates here:

https://github.com/rangle/pharmacy-online/commits/main/packages/database/src/schema/vat-rates.ts Note the change on August 11 from:
rate: decimal('rate', { precision: 5, scale: 2 }).notNull(),
To:
rate: integer('rate').default(0).notNull(),
I’d like to avoid going back and forth on the schema to avoid churn. We made a decision to use integers for price and VAT rates to avoid precision errors.

Component Level vs Repository/Data Access Boundary

Option 1: Component Level (Current Approach)

  // PercentageInput component
  <PercentageInput
    value={vatRate.rate / 100}  // Convert from storage
    onChange={(value) => onChange(value * 100)}  // Convert to storage
  />

Pros:

Cons:

Option 2: Repository/Data Access Boundary (Better Architecture)


// Repository layer
  export async function getVatRates() {
    const rates = await db.select().from(vat_rates);
    return rates.map(rate => ({
      ...rate,
      rate: rate.rate / 100  // Convert to percentage
    }));
  }

  export async function createVatRate(data) {
    await db.insert(vat_rates).values({
      ...data,
      rate: Math.round(data.rate * 100)  // Convert to integer
    });
  }

  // Component uses natural percentage values
  <PercentageInput
    value={vatRate.rate}  // Already 20.00
    onChange={(value) => onChange(value)}  // Just 20.00
  />

Pros:

Cons:

Cladues Recommendation: Repository/Data Access Boundary

This aligns with several important principles:

  1. Separation of Concerns: UI components shouldn’t know about database storage formats
  2. DRY: Conversion logic in one place
  3. Type Safety: Frontend types match business logic (percentages), not storage
  4. Maintainability: If storage format changes again, only repository needs updating
  5. Consistency with Existing Patterns: Your codebase already has a comment about this:
- "Transform data from SQL shape to Frontend shape in data access layer"

Implementation Pattern:


  // Types
  type VatRateDB = { rate: number }  // Integer: 2000
  type VatRateFrontend = { rate: number }  // Percentage: 20.00

  // Repository handles ALL conversion
  // Frontend never knows about integer storage

  This is similar to how CurrencyInput should ideally work - the repository should handle pence/pounds conversion, not the component.

evans Thoughts

Current Pattern: Conversion at Component Level

The codebase consistently does conversion at the component/UI level, NOT at the repository level:

  1. Currency/Price Fields:
- Database stores integers (pence): price: integer('price').notNull()
- Repository passes through raw integers: cost: data.cost (no conversion)
- CurrencyInput component multiplies by 100: £20.00 → 2000 pence
- formatPrice() divides by 100 for display: 2000 → £20.00
- Currency component handles display conversion
  1. Percentage Fields (current VAT implementation):
- Database currently numeric(5,2) but Adams wants integer
- Percentage component exists: formatPercentage(value) divides by 100
- Display uses: <Percentage value={parseFloat(vatRate.rate)} />
  1. Architecture Decision Record (ADR.md):

[!info] notes from the ADR

“We use branded types Pence and Pounds to prevent currency conversion bugs. All internal processing uses Pence (integers) while user displays use

Pounds (decimals).

The database layer uses plain number types, server actions enforce Pence types, and frontend components display using

formatPrice(pence).”

Key Findings:

  1. Repositories DON’T do conversion - they pass through raw database values
  2. Components handle conversion - Input components multiply, display components divide
  3. Consistency across the codebase - All money fields follow this pattern
  4. formatPrice/formatPercentage utilities exist for display conversion

Recommendation Based on Existing Patterns:

To be consistent with the existing codebase, we should:

  1. Keep conversion at component level (like CurrencyInput does)
  2. PercentageInput should multiply by 100 on change (20% → 2000)
  3. Display uses existing Percentage component which divides by 100
  4. Repository passes through integers without conversion
  5. Database stores integers (as Adams wants)

This approach:

═══════════════════════════════════════════════════════════════
 sysop::boring.core - float.bbs viewer v0.1
═══════════════════════════════════════════════════════════════
    
▓▓▒▒ TODAY: 2025-10-27 ▒▒▓▓

<< 2025-10-26 | 2025-10-28 >>

🎯 What Evan Needs This Morning

Pending PRs (Awaiting Review/Merge)

  • PR #604: GP node assessment + basket automation

    • Status: In approval backlog, merge conflicts being resolved
    • Demos: Successfully demo’d on 2025-10-24 sprint demo (Daniel happy with feedback)
    • Next: Check if conflicts resolved, ready for merge to staging
  • PR #606: [Description needed - check GitHub]

    • Status: Demo’d successfully, awaiting review
    • Next: Check GitHub status
  • PR #607: [Description needed - check GitHub]

    • Status: Demo’d successfully, awaiting review
    • Next: Check GitHub status

Active Issues Ready for Dev

  • Issue #122: Assessment workflow

    • Location: /Users/evan/float-hub/rangle/issues/active/122-assessment-workflow.md
    • Status: Fully documented with acceptance criteria
    • Priority: Assessment UX experience (per Scott sync 2025-10-24)
    • Key consideration: Guest→account response transfer (piggybacking basket logic)
  • Issue #442: HEIC upload support

    • Location: /Users/evan/float-hub/rangle/issues/active/442-heic-upload-support.md
    • Status: Fully documented with acceptance criteria
    • Priority: Lower than #122 (per Scott sync realignment)

Follow-ups from Weekend

  • Check GitHub PR statuses (#604, #606, #607) - are they merged? ready for staging?
  • Scott mentioned creating UI/UX ticket (priority 3) and multi-product assessment response logging ticket
  • Wins tracking system now operational - remember to capture wins as they happen

First Tasks

  • Check pharmacy-online PR status (merged? staging? conflicts?)
  • Review Issue #122 (assessment workflow) - priority work
  • Check if Scott’s new tickets created (UI/UX, multi-product logging)
  • Capture wins as work happens (two-home system: quick log + weekly review)

Context from Yesterday

Weekend mode: Shack building + infrastructure work

  • float.bbs viewer operational
  • TodayDrawer component shipped
  • Documentation preserved
  • Monday prep notes ready

Repo: https://github.com/pharmonline/pharmacy-online Local: ~/projects/pharmacy-online


timelog

  • 11:45pm - 12:03am - [project::float-bbs-viewer] hermit crab blueprints → forge patterns extracted (102KB doc)
  • 11:40pm - 11:45pm - [project::float-infrastructure] domain migration → sysop-beta.floatbbs.net live
  • 12:03am - 12:05am - [project::float-hub] CLAUDE.md evna integration → explicit tool names + capture triggers

Late Night: Infrastructure & Blueprinting

float-bbs-viewer Architecture Extraction (11:45pm - 12:03am)

  • Extracted patterns from float-dispatch-manifesto-forge (React/Vite/ShadCN)
  • Created hermit crab reference: 2025-10-26-dispatch-blueprints-for-bbs-viewer-hermit-crab-patterns.md
  • Key patterns: color-coded imprint system, grid layouts, Tailwind HSL tokens, editorial philosophy sections
  • Translation map: React hooks → Astro content collections, SPA routing → SSG file-based
  • Breadcrumb: /Users/evan/projects/float-bbs-viewer/2025-10-26-dispatch-blueprints-for-bbs-viewer-hermit-crab-patterns.md

CLAUDE.md evna Integration (12:03am - 12:05am)

  • Replaced vague “evna-context-concierge” references with explicit tool names
  • Added mandatory capture triggers (7-item checklist: after features, docs, infrastructure, archaeology, context switches, obstacles, chunks)
  • Context capture pattern template (ctx::, project::, format)
  • Breadcrumb: /Users/evan/float-hub/CLAUDE.md:308-315, 374-403, 422

Morning: Brain Booting

(Space for morning thoughts)

Press ESC or Ctrl+D to close