feat(billing): derive next-invoice amount from billing state (FE-1245)#13599
feat(billing): derive next-invoice amount from billing state (FE-1245)#13599dante01yoon wants to merge 9 commits into
Conversation
📝 WalkthroughWalkthroughAdds invoice price derivation from subscription billing data, a reactive ChangesNext invoice billing
Estimated code review effort: 2 (Simple) | ~10 minutes Suggested labels: Suggested reviewers: Sequence Diagram(s)sequenceDiagram
participant Caller
participant useNextInvoice
participant useBillingContext
participant deriveNextInvoiceCents
Caller->>useNextInvoice: initialize composable
useNextInvoice->>useBillingContext: read subscription and billing data
useNextInvoice->>deriveNextInvoiceCents: compute invoice amount
deriveNextInvoiceCents-->>useNextInvoice: cents or null
useNextInvoice-->>Caller: nextInvoiceCents
Important Pre-merge checks failedPlease resolve all errors before merging. Addressing warnings is optional. ❌ Failed checks (1 inconclusive)
✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
🎨 Storybook: ✅ Built — View Storybook🎭 Playwright: ✅ 1690 passed, 0 failed · 4 flaky📊 Browser Reports
📦 Bundle: 7.83 MB gzip 🔴 +36 BDetailsSummary
Category Glance App Entry Points — 53.2 kB (baseline 53.2 kB) • ⚪ 0 BMain entry bundles and manifests
Status: 1 added / 1 removed Graph Workspace — 1.27 MB (baseline 1.27 MB) • ⚪ 0 BGraph editor runtime, canvas, workflow orchestration
Status: 1 added / 1 removed Views & Navigation — 110 kB (baseline 110 kB) • ⚪ 0 BTop-level views, pages, and routed surfaces
Status: 11 added / 11 removed / 3 unchanged Panels & Settings — 550 kB (baseline 550 kB) • ⚪ 0 BConfiguration panels, inspectors, and settings screens
Status: 11 added / 11 removed / 16 unchanged User & Accounts — 29.1 kB (baseline 29.1 kB) • ⚪ 0 BAuthentication, profile, and account management bundles
Status: 7 added / 7 removed / 3 unchanged Editors & Dialogs — 117 kB (baseline 117 kB) • ⚪ 0 BModals, dialogs, drawers, and in-app editors
Status: 4 added / 4 removed / 1 unchanged UI Components — 57.5 kB (baseline 57.5 kB) • ⚪ 0 BReusable component library chunks
Status: 5 added / 5 removed / 8 unchanged Data & Services — 269 kB (baseline 269 kB) • ⚪ 0 BStores, services, APIs, and repositories
Status: 13 added / 13 removed / 3 unchanged Utilities & Hooks — 3.4 MB (baseline 3.4 MB) • ⚪ 0 BHelpers, composables, and utility bundles
Status: 16 added / 16 removed / 19 unchanged Vendor & Third-Party — 15.3 MB (baseline 15.3 MB) • ⚪ 0 BExternal libraries and shared vendor chunks Status: 16 unchanged Other — 11.8 MB (baseline 11.8 MB) • ⚪ 0 BBundles that do not match a named category
Status: 67 added / 67 removed / 98 unchanged ⚡ Performance Report
Show regressions
All metrics
Historical variance (last 15 runs)
Trend (last 15 commits on main)
Raw data{
"timestamp": "2026-07-11T13:23:41.790Z",
"gitSha": "d4d12f4468b7d24d0c8412ed570e5776aba4b595",
"branch": "jaewon/fe-1245-next-invoice-data",
"measurements": [
{
"name": "canvas-idle",
"durationMs": 2056.59399999999,
"styleRecalcs": 10,
"styleRecalcDurationMs": 9.02,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 429.956,
"heapDeltaBytes": -7545468,
"heapUsedBytes": 62433552,
"domNodes": -277,
"jsHeapTotalBytes": 19283968,
"scriptDurationMs": 18.159,
"eventListeners": -199,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "canvas-idle",
"durationMs": 2012.945000000002,
"styleRecalcs": 10,
"styleRecalcDurationMs": 9.682,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 381.93999999999994,
"heapDeltaBytes": 3565764,
"heapUsedBytes": 69232752,
"domNodes": 20,
"jsHeapTotalBytes": 20729856,
"scriptDurationMs": 18.127999999999997,
"eventListeners": 4,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333335,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "canvas-mouse-sweep",
"durationMs": 1969.0099999999973,
"styleRecalcs": 75,
"styleRecalcDurationMs": 41.888999999999996,
"layouts": 12,
"layoutDurationMs": 3.8939999999999997,
"taskDurationMs": 880.545,
"heapDeltaBytes": -4616336,
"heapUsedBytes": 48151196,
"domNodes": -273,
"jsHeapTotalBytes": 17428480,
"scriptDurationMs": 123.484,
"eventListeners": -199,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "canvas-mouse-sweep",
"durationMs": 1810.0039999999922,
"styleRecalcs": 73,
"styleRecalcDurationMs": 36.276,
"layouts": 12,
"layoutDurationMs": 3.3629999999999995,
"taskDurationMs": 782.414,
"heapDeltaBytes": -12573348,
"heapUsedBytes": 57284648,
"domNodes": -238,
"jsHeapTotalBytes": 18497536,
"scriptDurationMs": 115.902,
"eventListeners": -199,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.799999999999272
},
{
"name": "canvas-zoom-sweep",
"durationMs": 1738.6319999999955,
"styleRecalcs": 33,
"styleRecalcDurationMs": 17.480000000000004,
"layouts": 6,
"layoutDurationMs": 0.599,
"taskDurationMs": 328.169,
"heapDeltaBytes": 191324,
"heapUsedBytes": 51728324,
"domNodes": -241,
"jsHeapTotalBytes": 6942720,
"scriptDurationMs": 17.482000000000003,
"eventListeners": -186,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.799999999999272
},
{
"name": "canvas-zoom-sweep",
"durationMs": 1740.0900000000092,
"styleRecalcs": 31,
"styleRecalcDurationMs": 19.293999999999997,
"layouts": 6,
"layoutDurationMs": 0.6130000000000001,
"taskDurationMs": 341.0570000000001,
"heapDeltaBytes": 2239280,
"heapUsedBytes": 61197912,
"domNodes": 78,
"jsHeapTotalBytes": 24379392,
"scriptDurationMs": 23.872,
"eventListeners": 19,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.699999999999818
},
{
"name": "dom-widget-clipping",
"durationMs": 608.5510000000056,
"styleRecalcs": 12,
"styleRecalcDurationMs": 9.944999999999999,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 384.27900000000005,
"heapDeltaBytes": -22751620,
"heapUsedBytes": 47275244,
"domNodes": -275,
"jsHeapTotalBytes": 6963200,
"scriptDurationMs": 63.11399999999999,
"eventListeners": -201,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.699999999999818
},
{
"name": "dom-widget-clipping",
"durationMs": 589.7919999999885,
"styleRecalcs": 10,
"styleRecalcDurationMs": 6.419999999999999,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 375.62800000000004,
"heapDeltaBytes": 4339804,
"heapUsedBytes": 56513820,
"domNodes": -293,
"jsHeapTotalBytes": 11661312,
"scriptDurationMs": 58.745000000000005,
"eventListeners": -203,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333332,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "large-graph-idle",
"durationMs": 2031.158000000005,
"styleRecalcs": 10,
"styleRecalcDurationMs": 8.565,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 515.9309999999999,
"heapDeltaBytes": -2695688,
"heapUsedBytes": 57991020,
"domNodes": -270,
"jsHeapTotalBytes": -1355776,
"scriptDurationMs": 86.812,
"eventListeners": -197,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "large-graph-idle",
"durationMs": 2060.8990000000063,
"styleRecalcs": 10,
"styleRecalcDurationMs": 16.682999999999996,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 541.261,
"heapDeltaBytes": -2629528,
"heapUsedBytes": 58482576,
"domNodes": -268,
"jsHeapTotalBytes": -1880064,
"scriptDurationMs": 96.39299999999999,
"eventListeners": -197,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "large-graph-pan",
"durationMs": 2066.185999999959,
"styleRecalcs": 67,
"styleRecalcDurationMs": 15.023999999999996,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 985.5939999999998,
"heapDeltaBytes": 27010996,
"heapUsedBytes": 81831412,
"domNodes": 10,
"jsHeapTotalBytes": 466944,
"scriptDurationMs": 366.82599999999996,
"eventListeners": 6,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "large-graph-pan",
"durationMs": 2084.3280000000277,
"styleRecalcs": 67,
"styleRecalcDurationMs": 14.532000000000004,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 987.796,
"heapDeltaBytes": 28359544,
"heapUsedBytes": 81569944,
"domNodes": -279,
"jsHeapTotalBytes": 204800,
"scriptDurationMs": 368.727,
"eventListeners": -197,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "large-graph-zoom",
"durationMs": 3098.397000000034,
"styleRecalcs": 63,
"styleRecalcDurationMs": 15.691,
"layouts": 60,
"layoutDurationMs": 7.068,
"taskDurationMs": 1298.557,
"heapDeltaBytes": 14660184,
"heapUsedBytes": 68527128,
"domNodes": -279,
"jsHeapTotalBytes": 1048576,
"scriptDurationMs": 512.9200000000001,
"eventListeners": 8,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.699999999999818
},
{
"name": "large-graph-zoom",
"durationMs": 3130.3680000000327,
"styleRecalcs": 66,
"styleRecalcDurationMs": 19.281999999999996,
"layouts": 60,
"layoutDurationMs": 7.296,
"taskDurationMs": 1314.0210000000002,
"heapDeltaBytes": 13825020,
"heapUsedBytes": 76214648,
"domNodes": -268,
"jsHeapTotalBytes": 4468736,
"scriptDurationMs": 477.578,
"eventListeners": -128,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "minimap-idle",
"durationMs": 2038.5989999999765,
"styleRecalcs": 7,
"styleRecalcDurationMs": 6.29,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 491.653,
"heapDeltaBytes": -3325404,
"heapUsedBytes": 59633628,
"domNodes": -274,
"jsHeapTotalBytes": -2142208,
"scriptDurationMs": 86.74499999999999,
"eventListeners": -197,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "minimap-idle",
"durationMs": 2016.931999999997,
"styleRecalcs": 8,
"styleRecalcDurationMs": 8.088000000000001,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 602.176,
"heapDeltaBytes": 10225360,
"heapUsedBytes": 67356712,
"domNodes": 16,
"jsHeapTotalBytes": 5505024,
"scriptDurationMs": 106.13,
"eventListeners": 6,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333335,
"p95FrameDurationMs": 16.699999999999818
},
{
"name": "subgraph-dom-widget-clipping",
"durationMs": 563.410000000033,
"styleRecalcs": 47,
"styleRecalcDurationMs": 10.717999999999998,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 343.63500000000005,
"heapDeltaBytes": 13060068,
"heapUsedBytes": 71224352,
"domNodes": 20,
"jsHeapTotalBytes": 18350080,
"scriptDurationMs": 115.705,
"eventListeners": 6,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333332,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "subgraph-dom-widget-clipping",
"durationMs": 608.2700000000614,
"styleRecalcs": 46,
"styleRecalcDurationMs": 11.286000000000001,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 419.542,
"heapDeltaBytes": -4007228,
"heapUsedBytes": 47811524,
"domNodes": -277,
"jsHeapTotalBytes": 913408,
"scriptDurationMs": 121.63799999999999,
"eventListeners": -197,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333332,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "subgraph-idle",
"durationMs": 2026.0879999999872,
"styleRecalcs": 10,
"styleRecalcDurationMs": 9.4,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 395.03599999999994,
"heapDeltaBytes": -7722588,
"heapUsedBytes": 62286264,
"domNodes": -277,
"jsHeapTotalBytes": 19021824,
"scriptDurationMs": 12.757,
"eventListeners": -199,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "subgraph-idle",
"durationMs": 2003.575000000012,
"styleRecalcs": 10,
"styleRecalcDurationMs": 10.43,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 388.424,
"heapDeltaBytes": 19628928,
"heapUsedBytes": 72195816,
"domNodes": -288,
"jsHeapTotalBytes": 16117760,
"scriptDurationMs": 14.559999999999999,
"eventListeners": -199,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333332,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "subgraph-mouse-sweep",
"durationMs": 1743.912000000023,
"styleRecalcs": 76,
"styleRecalcDurationMs": 38.717999999999996,
"layouts": 16,
"layoutDurationMs": 4.17,
"taskDurationMs": 717.921,
"heapDeltaBytes": -16813624,
"heapUsedBytes": 53162464,
"domNodes": -234,
"jsHeapTotalBytes": 19283968,
"scriptDurationMs": 94.17399999999999,
"eventListeners": -199,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333335,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "subgraph-mouse-sweep",
"durationMs": 1683.384999999987,
"styleRecalcs": 75,
"styleRecalcDurationMs": 35.384,
"layouts": 16,
"layoutDurationMs": 3.9290000000000003,
"taskDurationMs": 665.9849999999999,
"heapDeltaBytes": -9007548,
"heapUsedBytes": 52527344,
"domNodes": 63,
"jsHeapTotalBytes": 26738688,
"scriptDurationMs": 90.81900000000002,
"eventListeners": 4,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "subgraph-transition-enter",
"durationMs": 1050.2680000000169,
"styleRecalcs": 19,
"styleRecalcDurationMs": 36.702,
"layouts": 15,
"layoutDurationMs": 18.814999999999998,
"taskDurationMs": 835.018,
"heapDeltaBytes": 30081836,
"heapUsedBytes": 101479572,
"domNodes": 13673,
"jsHeapTotalBytes": 16252928,
"scriptDurationMs": 30.197000000000003,
"eventListeners": 2535,
"totalBlockingTimeMs": 168,
"frameDurationMs": 16.66333333333332,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "viewport-pan-sweep",
"durationMs": 8136.322000000007,
"styleRecalcs": 250,
"styleRecalcDurationMs": 50.689,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 3708.0679999999998,
"heapDeltaBytes": 2631600,
"heapUsedBytes": 71128700,
"domNodes": -261,
"jsHeapTotalBytes": 3862528,
"scriptDurationMs": 1271.8729999999998,
"eventListeners": -183,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.670000000000012,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "viewport-pan-sweep",
"durationMs": 8160.708000000057,
"styleRecalcs": 250,
"styleRecalcDurationMs": 54.19799999999999,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 3845.8629999999994,
"heapDeltaBytes": 16766052,
"heapUsedBytes": 76067340,
"domNodes": -263,
"jsHeapTotalBytes": 9072640,
"scriptDurationMs": 1232.263,
"eventListeners": -118,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.799999999999272
},
{
"name": "vue-large-graph-idle",
"durationMs": 12033.555999999975,
"styleRecalcs": 0,
"styleRecalcDurationMs": 0,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 12017.131999999998,
"heapDeltaBytes": -45019124,
"heapUsedBytes": 174373060,
"domNodes": -8314,
"jsHeapTotalBytes": 23064576,
"scriptDurationMs": 516.615,
"eventListeners": -16378,
"totalBlockingTimeMs": 0,
"frameDurationMs": 17.219999999999953,
"p95FrameDurationMs": 16.80000000000291
},
{
"name": "vue-large-graph-idle",
"durationMs": 13069.73400000004,
"styleRecalcs": 0,
"styleRecalcDurationMs": 0,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 13052.764,
"heapDeltaBytes": -14979908,
"heapUsedBytes": 171316264,
"domNodes": -3302,
"jsHeapTotalBytes": 21204992,
"scriptDurationMs": 527.4839999999999,
"eventListeners": -16375,
"totalBlockingTimeMs": 0,
"frameDurationMs": 17.223333333333358,
"p95FrameDurationMs": 16.80000000000291
},
{
"name": "vue-large-graph-pan",
"durationMs": 15272.167000000025,
"styleRecalcs": 74,
"styleRecalcDurationMs": 17.702999999999996,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 15249.003,
"heapDeltaBytes": -14998132,
"heapUsedBytes": 174113076,
"domNodes": -3302,
"jsHeapTotalBytes": 15876096,
"scriptDurationMs": 780.338,
"eventListeners": -16374,
"totalBlockingTimeMs": 42,
"frameDurationMs": 17.219999999999953,
"p95FrameDurationMs": 16.799999999999272
},
{
"name": "vue-large-graph-pan",
"durationMs": 15260.744000000046,
"styleRecalcs": 76,
"styleRecalcDurationMs": 19.121,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 15234.297,
"heapDeltaBytes": -21558888,
"heapUsedBytes": 168430136,
"domNodes": -3300,
"jsHeapTotalBytes": 15728640,
"scriptDurationMs": 863.777,
"eventListeners": -16366,
"totalBlockingTimeMs": 8,
"frameDurationMs": 17.219999999999953,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "workflow-execution",
"durationMs": 446.5940000000046,
"styleRecalcs": 13,
"styleRecalcDurationMs": 17.017,
"layouts": 2,
"layoutDurationMs": 0.4050000000000001,
"taskDurationMs": 97.895,
"heapDeltaBytes": 5094408,
"heapUsedBytes": 63225148,
"domNodes": 130,
"jsHeapTotalBytes": 1572864,
"scriptDurationMs": 8.570999999999998,
"eventListeners": 65,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.669999999999998,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "workflow-execution",
"durationMs": 474.21200000007957,
"styleRecalcs": 12,
"styleRecalcDurationMs": 16.57,
"layouts": 2,
"layoutDurationMs": 0.31499999999999995,
"taskDurationMs": 102.8,
"heapDeltaBytes": 4865672,
"heapUsedBytes": 62976532,
"domNodes": 117,
"jsHeapTotalBytes": 1572864,
"scriptDurationMs": 9.374999999999998,
"eventListeners": 67,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.670000000000012,
"p95FrameDurationMs": 16.800000000000182
}
]
} |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 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.
Inline comments:
In `@src/composables/billing/useNextInvoice.ts`:
- Around line 39-41: Update the pricing logic in the next-invoice calculation to
check whether subscription.duration is null before selecting annual or monthly
stop pricing; return null for a null duration, and only access price.price_cents
when a valid duration is present.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: daddbb2a-65df-4bcb-ab3c-2002b20972ad
📒 Files selected for processing (3)
knip.config.tssrc/composables/billing/useNextInvoice.test.tssrc/composables/billing/useNextInvoice.ts
| const price = | ||
| subscription.duration === 'ANNUAL' ? stop.yearly : stop.monthly | ||
| return price.price_cents |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Verify SubscriptionDuration type and TeamCreditStop interface definitions
rg -n 'SubscriptionDuration' src/composables/billing/types.ts --type ts -C2
rg -n 'interface TeamCreditStop\b' src/platform/workspace/api/workspaceApi.ts --type ts -A15
rg -n 'SubscriptionDuration' src/platform/workspace/api/workspaceApi.ts --type ts -C2Repository: Comfy-Org/ComfyUI_frontend
Length of output: 1508
🏁 Script executed:
#!/bin/bash
set -euo pipefail
ast-grep outline src/composables/billing/useNextInvoice.ts --view expanded
cat -n src/composables/billing/useNextInvoice.ts | sed -n '1,220p'
echo '---'
rg -n 'useNextInvoice|next invoice|team_credit_stops|subscription_duration' src -g '*.ts' -g '*.vue' -C 2Repository: Comfy-Org/ComfyUI_frontend
Length of output: 18651
🏁 Script executed:
#!/bin/bash
set -euo pipefail
ast-grep outline src/composables/billing/useNextInvoice.test.ts --view expanded
cat -n src/composables/billing/useNextInvoice.test.ts | sed -n '1,240p'
echo '---'
rg -n 'duration|null|monthly|annual|deriveNextInvoiceCents' src/composables/billing/useNextInvoice.test.ts src/composables/billing -C 2Repository: Comfy-Org/ComfyUI_frontend
Length of output: 45477
🏁 Script executed:
#!/bin/bash
set -euo pipefail
rg -n 'duration: null|subscription_duration: null|makeSubscription\(\{[^}]*duration' src/composables/billing src/platform/cloud/subscription -C 2
rg -n 'currentTeamCreditStop|teamCreditStops' src/composables/billing/useBillingContext.ts src/composables/billing/useLegacyBilling.ts src/platform/workspace/composables/useWorkspaceBilling.ts -C 3Repository: Comfy-Org/ComfyUI_frontend
Length of output: 5324
Handle null subscription duration before selecting stop pricing. SubscriptionInfo.duration is nullable, and the stop branch currently treats null as monthly. Return null instead so the banner stays hidden when the amount can’t be resolved.
🤖 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 `@src/composables/billing/useNextInvoice.ts` around lines 39 - 41, Update the
pricing logic in the next-invoice calculation to check whether
subscription.duration is null before selecting annual or monthly stop pricing;
return null for a null duration, and only access price.price_cents when a valid
duration is present.
Codecov Report❌ Patch coverage is
@@ Coverage Diff @@
## main #13599 +/- ##
==========================================
- Coverage 78.00% 78.00% -0.01%
==========================================
Files 1658 1659 +1
Lines 100249 100262 +13
Branches 35005 35006 +1
==========================================
+ Hits 78202 78210 +8
- Misses 21293 21297 +4
- Partials 754 755 +1
Flags with carried forward coverage won't be shown. Click here to find out more.
... and 2 files with indirect coverage changes 🚀 New features to boost your workflow:
|
Summary
Adds the data seam for the Settings > Plan & Credits Invoices tab (FE-1245): derives the "Next month invoice" amount from billing state already fetched by the billing context, replacing the prototype's hardcoded mock.
Changes
useNextInvoicecomposable + purederiveNextInvoiceCentsinsrc/composables/billing/— resolves the renewal price from the subscribed team credit stop (monthly/annual) or the current plan'sprice_cents; returnsnull(banner hidden) when inactive, cancelled, or unresolvable (incl. legacy billing's empty plan list). Unit tests cover all branches.Intentionally excludes usage/overage pending charges until the backend exposes an authoritative upcoming-invoice amount (see FE-1245 for the
pending_charges_microspost-cutover question).Standalone on main: #13591 (
split/plan-credits-tabs) leavesuseWorkspaceInvoicesas the endpoint seam with the banner hidden — once it lands, the seam swaps its mock for this composable (knip-ignored until then, same pending-integration convention as the split stack).Review Focus
Team plans: current credit stop price (status
team_credit_stop.idmatched against theteam_credit_stopsladder) intentionally takes precedence over the plan-slug priceFE-1245 / DES-497 · consumed by feat: Plan & Credits settings tabs #13591
Verified against feat: Plan & Credits settings tabs #13591's actual seam (
split/plan-credits-tabs):useWorkspaceInvoicesreturns{ nextInvoiceCents: ComputedRef<number | null> }with null hiding the banner — this composable matches that contract exactly (same name, unit, null semantics), so the swap is a one-line body changeAnnual subscriptions return null (banner hidden): DES-469 specifies the banner is "hidden when there's no meaningful upcoming invoice", and an annual sub has no monthly invoice — resolved per the implemented design
Screenshots
Live verification in Vite dev —
deriveNextInvoiceCentsimported from the running app bundle and exercised across the derivation branches (plan price, team credit stop monthly/annual, cancelled, legacy):Design reference — the consumer UI this feeds (DES-497/DES-469, implemented in #13591)
Invoices tab, banner visible (amount from this composable):
Paused subscription — banner hidden, paused banner hosts the history action (null path):