Repository navigation
feat(frontend): amortize-upfront monthly cost toggle - #1114
Conversation
|
Warning Review limit reached
More reviews will be available in 6 minutes and 18 seconds. Learn how PR review limits work. Your organization has run out of usage credits. Purchase more in the billing tab. ⌛ How to resolve this issue?After more reviews become available, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available. Please see our Fair Usage Limits Policy for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (17)
📝 WalkthroughWalkthroughThis PR implements an "Amortize upfront over term" toggle allowing users to view monthly costs as either raw recurring expenses (default) or amortized totals that spread upfront payments evenly across the commitment term. Changes include new state management, a calculation utility, and UI updates across four cost-display surfaces (approval details, purchase history, approval queue, active commitments). ChangesAmortized Monthly Cost Toggle
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
|
@coderabbitai full review |
✅ Action performedFull review finished. |
There was a problem hiding this comment.
🧹 Nitpick comments (1)
frontend/src/state.ts (1)
385-395: 💤 Low valueConsider adding change detection before notifying listeners.
For consistency with
setCurrentProvider(lines 96-104) andsetCurrentAccountIDs(lines 147-158), you could check whether the value actually changed before notifying subscribers, avoiding unnecessary re-renders when the same value is set twice.♻️ Proposed change-detection pattern
export function setAmortizeUpfront(value: boolean): void { + const changed = amortizeUpfrontMemory !== value; amortizeUpfrontMemory = value; try { localStorage.setItem(AMORTIZE_UPFRONT_LS_KEY, String(value)); } catch { // Non-fatal; in-memory fallback remains correct for the session. } + if (changed) { amortizeListeners.forEach((cb) => { try { cb(); } catch (err) { console.warn('subscribeAmortizeUpfront listener error:', err); } }); + } }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@frontend/src/state.ts` around lines 385 - 395, setAmortizeUpfront currently always writes to local storage and calls amortizeListeners even if amortizeUpfrontMemory is unchanged; update the function to capture the previous value (amortizeUpfrontMemory), compare it to the incoming value, and only proceed to set amortizeUpfrontMemory, write AMORTIZE_UPFRONT_LS_KEY to localStorage and iterate amortizeListeners (still protecting each callback with try/catch) when the value actually changes so subscribers aren't notified on no-op sets.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@frontend/src/state.ts`:
- Around line 385-395: setAmortizeUpfront currently always writes to local
storage and calls amortizeListeners even if amortizeUpfrontMemory is unchanged;
update the function to capture the previous value (amortizeUpfrontMemory),
compare it to the incoming value, and only proceed to set amortizeUpfrontMemory,
write AMORTIZE_UPFRONT_LS_KEY to localStorage and iterate amortizeListeners
(still protecting each callback with try/catch) when the value actually changes
so subscribers aren't notified on no-op sets.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: a6d671e2-27a1-4b77-b9a8-7c3a756da7be
📒 Files selected for processing (17)
frontend/src/__tests__/allowed-accounts.test.tsfrontend/src/__tests__/app.test.tsfrontend/src/__tests__/approval-details.test.tsfrontend/src/__tests__/history-approval-queue.test.tsfrontend/src/__tests__/history-approve-button.test.tsfrontend/src/__tests__/history-cancel-button.test.tsfrontend/src/__tests__/history-cancel-permissions.test.tsfrontend/src/__tests__/history-retry-button.test.tsfrontend/src/__tests__/history.test.tsfrontend/src/__tests__/inventory.test.tsfrontend/src/__tests__/utils.test.tsfrontend/src/__tests__/xss-provider-class.test.tsfrontend/src/approval-details.tsfrontend/src/history.tsfrontend/src/inventory.tsfrontend/src/state.tsfrontend/src/utils.ts
a95d3b8 to
989a200
Compare
|
@coderabbitai full review |
✅ Action performedFull review finished. |
Add a shared "Amortize upfront over term" checkbox that folds the one-time upfront cost evenly across the commitment term, letting every payment type (No Upfront / Partial / All Upfront) be compared on a total-cost-per-month basis. New shared helper `amortizedMonthly(monthlyCost, upfrontCost, termYears)` in utils.ts returns `monthlyCost + upfrontCost / (termYears * 12)`. Guards: term <= 0, non-finite term, or null/non-finite upfront all fall back to monthlyCost unchanged (no NaN/Infinity). Toggle state lives in `state.ts` (`getAmortizeUpfront` / `setAmortizeUpfront` / `subscribeAmortizeUpfront`) persisted under `cudly.amortizeUpfront` in localStorage. Same pattern as `getCostPeriod`/`setCostPeriod`. All views read the same key so they stay in sync across tab switches and page reloads. Views updated: - history.ts: checkbox injected into #history-controls and #purchases-approval-queue-section; both renderHistoryList and renderApprovalQueue use amortizedMonthly when the toggle is on; column header updates to "Monthly Cost (amortized)" when active. subscribeAmortizeUpfront re-renders both tables without a refetch. - inventory.ts: checkbox injected into .section-header-actions; buildCommitmentRow uses amortizedMonthly on the monthly_cost cell; last-fetched commitments cached so subscribeAmortizeUpfront can re-render without an API round-trip. - approval-details.ts: "Monthly cost" column added to the per-rec table; header built via DOM methods (no innerHTML); column label updates to "Monthly cost (amortized)" when toggle is on. Modal reads state at render time, no re-render needed. plans.ts and dashboard.ts have no monthly_cost field on their displayed data types (PlannedPurchase / dashboard summary), so the toggle has no effect there and no changes were needed. Tests: 9 new `amortizedMonthly` unit tests in utils.test.ts covering all payment types and every guard path. Existing approval-details, history, and inventory tests updated for the new column and the new state mock entries. Full suite: 2442 pass, 2 pre-existing timezone failures in utils.test.ts (unrelated). Closes #1112
989a200 to
125cee2
Compare
|
@coderabbitai full review |
✅ Action performedFull review finished. |
Summary
amortizedMonthly(monthlyCost, upfrontCost, termYears)inutils.ts; guards against term <= 0, non-finite inputs, and null upfront (all returnmonthlyCostunchanged).localStorage(cudly.amortizeUpfront) viastate.tsget/set/subscribe pattern matching the existingcostPeriodslice; all views share the same key and re-render in sync without refetching.Views changed
history.ts#history-controlsand approval-queue section; both tables re-render on toggle; column header becomes "Monthly Cost (amortized)" when activeinventory.tsbuildCommitmentRowappliesamortizedMonthly; last-fetch cached for zero-refetch re-renderapproval-details.tsinnerHTML); reads toggle state at modal-render timeplans.tsanddashboard.tshave nomonthly_costfield on their displayed types (PlannedPurchase/ dashboard summary) -- no changes needed there.Test plan
amortizedMonthlyunit tests (All Upfront, Partial, No Upfront, term<=0 guard, non-finite term, null/undefined/non-finite upfront)approval-details.test.tsupdated for the new 13-column layout and shifted cell indicesjest.mock('../state')lackedgetAmortizeUpfront/subscribeAmortizeUpfrontupdatedutils.test.ts)tsc --noEmitcleanCloses #1112
🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Tests