@subvalue/cli 0.1.5 → 0.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -2
- package/dist/packages/cli/src/server.js +74 -27
- package/dist/packages/core/src/billing.js +112 -0
- package/dist/packages/core/src/summary.js +69 -70
- package/dist/packages/core/src/types.js +10 -2
- package/dist/packages/receipt/src/index.js +158 -16
- package/dist/public/app.css +185 -0
- package/dist/public/apps/web/src/landing.js +8 -2
- package/dist/public/apps/web/src/main.js +243 -44
- package/dist/public/landing.css +185 -0
- package/dist/public/packages/core/src/billing.js +112 -0
- package/dist/public/packages/receipt/src/index.js +158 -16
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -42,7 +42,7 @@ npm run demo
|
|
|
42
42
|
- **Receipt:** local canvas preview and PNG export of the exact same pixels. No projects, paths, requests or sessions in exports.
|
|
43
43
|
- **Settings:** Sources, Billing, USD, Privacy / data. Inactive providers stay here, with optional manual inclusion.
|
|
44
44
|
|
|
45
|
-
First run
|
|
45
|
+
First run offers subscription plans for each provider with imported usage. Choose a plan, a custom USD amount, or **No subscription · API** independently for Codex and Claude Code. Subscription + API is available as a checkbox. Unconfigured or legacy no-subscription settings default to API as an editable preference; this does not prove how any historical request was billed. Actual API payments remain unknown until entered for a specific period.
|
|
46
46
|
|
|
47
47
|
Month opens a floating calendar-month picker (including historical months). Previous/next arrows move by one calendar month, seven or thirty days for rolling presets, or the selected custom duration. Comparisons use the full declared monthly price for each complete calendar month selected. The current month is marked In progress. 7D, 30D and partial custom ranges show API-equivalent usage without subscription comparisons. Custom ranges covering whole calendar months compare the corresponding number of monthly charges. Amounts and API prices are USD; no invented exchange rates. Billing declarations do not overwrite historical records or prove their original billing mode. Settings offers bundled, verified ChatGPT/Claude plan presets and a custom amount. Annual presets retain the exact annual price divided by 12, with per-seat plans labeled.
|
|
48
48
|
|
|
@@ -68,7 +68,11 @@ Codex excludes inherited ordinal prefixes, uses cumulative deltas, suppresses re
|
|
|
68
68
|
|
|
69
69
|
The versioned catalog supports mappings with confidence and primary-source provenance, half-open effective intervals, cache reads/writes, Claude cache durations, and long-context bands. Rates and release evidence were checked against [OpenAI pricing](https://developers.openai.com/api/docs/pricing) and [Claude pricing](https://platform.claude.com/docs/en/about-claude/pricing) on **2026-10-05**. Historical versions include the July 30 Luna/Terra reductions and August 21 Sol promotion. Exact public GPT-5.6/6 and Claude identifiers carry mapping provenance; long-context rates use the verified 272k request threshold. New-model launch days use their documented initial API-equivalent rate. Calendar-dated changes between rates remain unknown because their exact UTC switch time is not documented. Sol pricing after the confirmed promotion remains unavailable. See [the pricing evidence ledger](dist/PRICING.md). Internal/unknown model identifiers are never mapped by resemblance.
|
|
70
70
|
|
|
71
|
-
Overview and receipt display the sum of priceable events;
|
|
71
|
+
Overview and receipt display the sum of priceable events; partial pricing stays visible and unknown model prices are never invented. Value is the displayed API equivalent minus total paid; Value Multiple is API equivalent divided by total paid (subscription plus declared API payments). A missing payment stays unknown; an explicit zero is accepted but cannot be a multiplier denominator. Comparisons remain limited to complete calendar months. Partial pricing uses observed values without a certain outcome or break-even date.
|
|
72
|
+
|
|
73
|
+
Expense controls appear inside a provider card only for mixed billing, or when that provider has no subscription while the other active provider has a subscription. Select a plan using the same menu as onboarding, or enter actual API spend; mixed mode starts its editable API field at $0. **Save** applies only to the displayed period and provider. A complete single month is stored as a monthly override, shared by Month and an equivalent Custom selection. Other ranges use an exact civil-date range key; amounts are not copied to other selections. API spend over several whole months is a total for that range and is counted once. Settings also provides per-period editing and reset to the default. Saved overrides survive changes to the usual plan, receipt theme changes and relaunch. The scanner and usage records are unchanged.
|
|
74
|
+
|
|
75
|
+
When an active provider has not supplied its expenses, a labeled comparison uses only providers with known expenses; the overall API-equivalent figure still includes all observed usage. Empty periods show imported history bounds.
|
|
72
76
|
|
|
73
77
|
## Architecture
|
|
74
78
|
|
|
@@ -9,6 +9,8 @@ import { demoData } from '../../core/src/demo.js';
|
|
|
9
9
|
import {
|
|
10
10
|
defaultSettings,
|
|
11
11
|
|
|
12
|
+
|
|
13
|
+
|
|
12
14
|
|
|
13
15
|
|
|
14
16
|
} from '../../core/src/types.js';
|
|
@@ -17,6 +19,7 @@ import {
|
|
|
17
19
|
import { isRecord } from '../../core/src/metadata.js';
|
|
18
20
|
import { safeDirectory } from '../../core/src/security.js';
|
|
19
21
|
import { subscriptionPlan } from '../../core/src/subscriptions.js';
|
|
22
|
+
import { shiftCalendarDays } from '../../core/src/calendar.js';
|
|
20
23
|
const mime = {
|
|
21
24
|
'.html': 'text/html; charset=utf-8',
|
|
22
25
|
'.js': 'text/javascript; charset=utf-8',
|
|
@@ -29,6 +32,34 @@ const billingModes = ['UNKNOWN', 'SUBSCRIPTION', 'NO_SUBSCRIPTION', 'API', 'MIXE
|
|
|
29
32
|
const isBillingMode = (value ) =>
|
|
30
33
|
billingModes.some((mode) => mode === value);
|
|
31
34
|
|
|
35
|
+
function validateBilling(provider , value , monthly = false) {
|
|
36
|
+
if (!isRecord(value) || !isBillingMode(value.mode)) throw new Error('Invalid billing settings');
|
|
37
|
+
const amount = (n ) =>
|
|
38
|
+
n === null || (typeof n === 'number' && Number.isFinite(n) && n >= 0 && n <= 1000000);
|
|
39
|
+
if (!amount(value.monthly)) throw new Error('Invalid subscription amount');
|
|
40
|
+
if (value.apiSpend !== undefined && !amount(value.apiSpend)) throw new Error('Invalid API spend');
|
|
41
|
+
if (!monthly && value.apiSpend != null) throw new Error('API spend belongs to a specific month');
|
|
42
|
+
const planId = value.planId;
|
|
43
|
+
if (planId != null && typeof planId !== 'string') throw new Error('Invalid subscription plan');
|
|
44
|
+
const plan = planId == null ? undefined : subscriptionPlan(provider, planId);
|
|
45
|
+
if (planId != null && !plan) throw new Error('Invalid subscription plan');
|
|
46
|
+
const subscribed = value.mode === 'SUBSCRIPTION' || value.mode === 'MIXED';
|
|
47
|
+
if (
|
|
48
|
+
subscribed &&
|
|
49
|
+
plan &&
|
|
50
|
+
(value.monthly === null || Math.abs(Number(value.monthly) - plan.monthly) > 0.005)
|
|
51
|
+
)
|
|
52
|
+
throw new Error('Subscription amount does not match the selected plan');
|
|
53
|
+
return {
|
|
54
|
+
mode: value.mode,
|
|
55
|
+
monthly: subscribed ? (plan?.monthly ?? (value.monthly )) : null,
|
|
56
|
+
...(subscribed && planId !== undefined ? { planId: plan?.id ?? null } : {}),
|
|
57
|
+
...(monthly &&
|
|
58
|
+
(value.mode === 'API' || value.mode === 'MIXED' || value.mode === 'NO_SUBSCRIPTION')
|
|
59
|
+
? { apiSpend: (value.apiSpend ) ?? null }
|
|
60
|
+
: {}),
|
|
61
|
+
};
|
|
62
|
+
}
|
|
32
63
|
export function validateSettings(value ) {
|
|
33
64
|
if (!isRecord(value) || value.currency !== 'USD' || typeof value.onboarded !== 'boolean')
|
|
34
65
|
throw new Error('Invalid settings');
|
|
@@ -42,33 +73,49 @@ export function validateSettings(value ) {
|
|
|
42
73
|
settings.receiptTheme = value.receiptTheme;
|
|
43
74
|
}
|
|
44
75
|
for (const provider of ['codex', 'claude'] ) {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
if (
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
)
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
76
|
+
if (typeof value.include[provider] !== 'boolean') throw new Error('Invalid billing settings');
|
|
77
|
+
settings.billing[provider] = validateBilling(provider, value.billing[provider]);
|
|
78
|
+
settings.include[provider] = value.include[provider];
|
|
79
|
+
}
|
|
80
|
+
if (value.monthlyBilling !== undefined) {
|
|
81
|
+
if (!isRecord(value.monthlyBilling) || Object.keys(value.monthlyBilling).length > 240)
|
|
82
|
+
throw new Error('Invalid monthly billing');
|
|
83
|
+
settings.monthlyBilling = {};
|
|
84
|
+
for (const [month, providers] of Object.entries(value.monthlyBilling)) {
|
|
85
|
+
if (
|
|
86
|
+
!/^(?:19|20|21)\d{2}-(?:0[1-9]|1[0-2])$/.test(month) ||
|
|
87
|
+
!isRecord(providers) ||
|
|
88
|
+
Object.keys(providers).some((provider) => provider !== 'codex' && provider !== 'claude')
|
|
89
|
+
)
|
|
90
|
+
throw new Error('Invalid monthly billing');
|
|
91
|
+
const entry = {};
|
|
92
|
+
for (const provider of ['codex', 'claude'] )
|
|
93
|
+
if (providers[provider] !== undefined)
|
|
94
|
+
entry[provider] = validateBilling(provider, providers[provider], true);
|
|
95
|
+
settings.monthlyBilling[month] = entry;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
if (value.periodBilling !== undefined) {
|
|
99
|
+
if (!isRecord(value.periodBilling) || Object.keys(value.periodBilling).length > 240)
|
|
100
|
+
throw new Error('Invalid period billing');
|
|
101
|
+
settings.periodBilling = {};
|
|
102
|
+
for (const [key, providers] of Object.entries(value.periodBilling)) {
|
|
103
|
+
const [from, to] = key.split(':');
|
|
104
|
+
if (
|
|
105
|
+
!/^\d{4}-\d{2}-\d{2}:\d{4}-\d{2}-\d{2}$/.test(key) ||
|
|
106
|
+
shiftCalendarDays(from, 0) === null ||
|
|
107
|
+
shiftCalendarDays(to, 0) === null ||
|
|
108
|
+
from > to ||
|
|
109
|
+
!isRecord(providers) ||
|
|
110
|
+
Object.keys(providers).some((p) => p !== 'codex' && p !== 'claude')
|
|
111
|
+
)
|
|
112
|
+
throw new Error('Invalid period billing');
|
|
113
|
+
const entry = {};
|
|
114
|
+
for (const provider of ['codex', 'claude'] )
|
|
115
|
+
if (providers[provider] !== undefined)
|
|
116
|
+
entry[provider] = validateBilling(provider, providers[provider], true);
|
|
117
|
+
settings.periodBilling[key] = entry;
|
|
118
|
+
}
|
|
72
119
|
}
|
|
73
120
|
return settings;
|
|
74
121
|
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
import { fullCalendarMonths, shiftCalendarMonth } from './calendar.js';
|
|
4
|
+
|
|
5
|
+
function civilDate(date ) {
|
|
6
|
+
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`;
|
|
7
|
+
}
|
|
8
|
+
export function billingPeriodKey(range ) {
|
|
9
|
+
return (
|
|
10
|
+
(range.calendarFrom ?? civilDate(new Date(range.from))) +
|
|
11
|
+
':' +
|
|
12
|
+
(range.calendarTo ?? civilDate(new Date(Date.parse(range.until) - 1)))
|
|
13
|
+
);
|
|
14
|
+
}
|
|
15
|
+
export function withPeriodBilling(
|
|
16
|
+
settings ,
|
|
17
|
+
provider ,
|
|
18
|
+
range ,
|
|
19
|
+
billing ,
|
|
20
|
+
) {
|
|
21
|
+
const next = structuredClone(settings),
|
|
22
|
+
exactKey = billingPeriodKey(range),
|
|
23
|
+
monthly = fullCalendarMonths(range) === 1,
|
|
24
|
+
key = monthly ? exactKey.slice(0, 7) : exactKey,
|
|
25
|
+
declarations = monthly ? (next.monthlyBilling ??= {}) : (next.periodBilling ??= {});
|
|
26
|
+
// A rolling range can share dates with a calendar month. Editing that month
|
|
27
|
+
// supersedes an earlier exact-date declaration instead of leaving a stale override.
|
|
28
|
+
if (monthly && next.periodBilling?.[exactKey]?.[provider]) {
|
|
29
|
+
delete next.periodBilling[exactKey][provider];
|
|
30
|
+
if (!Object.keys(next.periodBilling[exactKey]).length) delete next.periodBilling[exactKey];
|
|
31
|
+
}
|
|
32
|
+
declarations[key] ??= {};
|
|
33
|
+
if (billing) declarations[key][provider] = billing;
|
|
34
|
+
else delete declarations[key][provider];
|
|
35
|
+
if (!Object.keys(declarations[key]).length) delete declarations[key];
|
|
36
|
+
return next;
|
|
37
|
+
}
|
|
38
|
+
export function billingForSelection(settings , provider , range ) {
|
|
39
|
+
const exact = settings.periodBilling?.[billingPeriodKey(range)]?.[provider];
|
|
40
|
+
if (exact) return exact;
|
|
41
|
+
if (fullCalendarMonths(range) === 1)
|
|
42
|
+
return billingForMonth(settings, provider, billingPeriodKey(range).slice(0, 7));
|
|
43
|
+
return billingForMonth({ ...settings, monthlyBilling: undefined }, provider, '');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Keep the dashboard action limited to mixed billing or a second provider without a plan. */
|
|
47
|
+
export function showExpenseAction(
|
|
48
|
+
provider ,
|
|
49
|
+
providers ,
|
|
50
|
+
) {
|
|
51
|
+
if (!provider.records) return false;
|
|
52
|
+
if (provider.mode === 'MIXED') return true;
|
|
53
|
+
return (
|
|
54
|
+
(provider.mode === 'API' ||
|
|
55
|
+
provider.mode === 'NO_SUBSCRIPTION' ||
|
|
56
|
+
provider.mode === 'UNKNOWN') &&
|
|
57
|
+
providers.some(
|
|
58
|
+
(other) =>
|
|
59
|
+
other.provider !== provider.provider &&
|
|
60
|
+
other.records > 0 &&
|
|
61
|
+
(other.mode === 'SUBSCRIPTION' || other.mode === 'MIXED') &&
|
|
62
|
+
(other.monthly != null || (other.comparison.subscription ?? 0) > 0),
|
|
63
|
+
)
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** A default is a user preference, not evidence of how a historical request was billed. */
|
|
68
|
+
export function billingForMonth(settings , provider , month ) {
|
|
69
|
+
const saved = settings.monthlyBilling?.[month]?.[provider] ?? settings.billing[provider];
|
|
70
|
+
return {
|
|
71
|
+
...saved,
|
|
72
|
+
mode: saved.mode === 'UNKNOWN' || saved.mode === 'NO_SUBSCRIPTION' ? 'API' : saved.mode,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function billingForRange(settings , provider , range ) {
|
|
77
|
+
const count = fullCalendarMonths(range);
|
|
78
|
+
const exact = settings.periodBilling?.[billingPeriodKey(range)]?.[provider];
|
|
79
|
+
const start = new Date(range.from);
|
|
80
|
+
const first =
|
|
81
|
+
range.calendarFrom?.slice(0, 7) ??
|
|
82
|
+
`${start.getFullYear()}-${String(start.getMonth() + 1).padStart(2, '0')}`;
|
|
83
|
+
const months = Array.from(
|
|
84
|
+
{ length: count ?? 1 },
|
|
85
|
+
(_, index) => exact ?? billingForMonth(settings, provider, shiftCalendarMonth(first, index) ),
|
|
86
|
+
);
|
|
87
|
+
const mode = months.every((b) => b.mode === months[0].mode)
|
|
88
|
+
? months[0].mode
|
|
89
|
+
: 'MIXED';
|
|
90
|
+
const subscriptionKnown = months.every((b) => b.mode === 'API' || b.monthly !== null);
|
|
91
|
+
const apiKnown = months.every((b) => b.mode === 'SUBSCRIPTION' || b.apiSpend != null);
|
|
92
|
+
const subscription =
|
|
93
|
+
count !== null && subscriptionKnown
|
|
94
|
+
? months.reduce((total, b) => total + (b.mode === 'API' ? 0 : b.monthly ), 0)
|
|
95
|
+
: null;
|
|
96
|
+
const apiSpend =
|
|
97
|
+
count !== null && apiKnown
|
|
98
|
+
? exact
|
|
99
|
+
? exact.mode === 'SUBSCRIPTION'
|
|
100
|
+
? 0
|
|
101
|
+
: exact.apiSpend
|
|
102
|
+
: months.reduce((total, b) => total + (b.mode === 'SUBSCRIPTION' ? 0 : b.apiSpend ), 0)
|
|
103
|
+
: null;
|
|
104
|
+
return {
|
|
105
|
+
mode,
|
|
106
|
+
monthly: months.length === 1 ? months[0].monthly : null,
|
|
107
|
+
subscription,
|
|
108
|
+
apiSpend,
|
|
109
|
+
paid: subscription !== null && apiSpend !== null ? subscription + apiSpend : null,
|
|
110
|
+
needsBilling: !subscriptionKnown || !apiKnown,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { billingForRange } from './billing.js';
|
|
1
2
|
|
|
2
3
|
import { priceRecord, catalog, } from './pricing/index.js';
|
|
3
4
|
import { fullCalendarMonths, shiftCalendarDays } from './calendar.js';
|
|
@@ -88,6 +89,8 @@ export function dateRange(
|
|
|
88
89
|
return { ...range, calendarMonths: fullCalendarMonths(range) };
|
|
89
90
|
}
|
|
90
91
|
|
|
92
|
+
|
|
93
|
+
|
|
91
94
|
|
|
92
95
|
|
|
93
96
|
|
|
@@ -118,6 +121,7 @@ export function subscriptionForRange(monthly , range )
|
|
|
118
121
|
|
|
119
122
|
|
|
120
123
|
|
|
124
|
+
|
|
121
125
|
|
|
122
126
|
|
|
123
127
|
|
|
@@ -199,52 +203,46 @@ export function summarize(
|
|
|
199
203
|
const historyPartial = !demo; // Local transcripts cannot prove that all historical requests were retained.
|
|
200
204
|
const completePricing = rows.length > 0 && priced.length === rows.length;
|
|
201
205
|
const apiEquivalent = completePricing ? subtotal : null;
|
|
202
|
-
const billing = settings
|
|
203
|
-
const subscription =
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
: subscription === null
|
|
219
|
-
? 'Subscription price not set'
|
|
220
|
-
: !completePricing
|
|
221
|
-
? 'Pricing unavailable'
|
|
222
|
-
: 'Partial history',
|
|
223
|
-
subscription,
|
|
224
|
-
);
|
|
225
|
-
if (completePricing && subscription !== null) {
|
|
226
|
-
const value = apiEquivalent - subscription;
|
|
206
|
+
const billing = billingForRange(settings, provider, range);
|
|
207
|
+
const { subscription, apiSpend, paid } = billing;
|
|
208
|
+
let comparison = {
|
|
209
|
+
...neutral(
|
|
210
|
+
fullCalendarMonths(range) === null
|
|
211
|
+
? 'Choose a full month'
|
|
212
|
+
: paid === null
|
|
213
|
+
? 'Expenses not set'
|
|
214
|
+
: 'Partial history',
|
|
215
|
+
subscription,
|
|
216
|
+
),
|
|
217
|
+
apiSpend,
|
|
218
|
+
paid,
|
|
219
|
+
};
|
|
220
|
+
if (completePricing && paid !== null) {
|
|
221
|
+
const value = apiEquivalent - paid;
|
|
227
222
|
let accumulated = 0;
|
|
228
223
|
let breakEven = null;
|
|
229
224
|
for (const d of daily) {
|
|
230
225
|
accumulated += d[provider] ?? 0;
|
|
231
|
-
if (accumulated >=
|
|
226
|
+
if (accumulated >= paid) {
|
|
232
227
|
breakEven = d.date;
|
|
233
228
|
break;
|
|
234
229
|
}
|
|
235
230
|
}
|
|
236
231
|
comparison = {
|
|
237
232
|
subscription,
|
|
233
|
+
apiSpend,
|
|
234
|
+
paid,
|
|
238
235
|
value,
|
|
239
|
-
roi:
|
|
236
|
+
roi: paid > 0 ? apiEquivalent / paid : null,
|
|
240
237
|
breakEven: historyPartial ? null : breakEven,
|
|
241
|
-
outcome:
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
238
|
+
outcome:
|
|
239
|
+
historyPartial || subscription === 0
|
|
240
|
+
? 'neutral'
|
|
241
|
+
: value > 0
|
|
242
|
+
? 'positive'
|
|
243
|
+
: value < 0
|
|
244
|
+
? 'negative'
|
|
245
|
+
: 'neutral',
|
|
248
246
|
reason: historyPartial ? 'Observed usage · Partial history' : null,
|
|
249
247
|
};
|
|
250
248
|
}
|
|
@@ -299,6 +297,7 @@ export function summarize(
|
|
|
299
297
|
comparison,
|
|
300
298
|
mode: billing.mode,
|
|
301
299
|
monthly: billing.monthly,
|
|
300
|
+
needsBilling: billing.needsBilling,
|
|
302
301
|
first: timestamps[0] ?? null,
|
|
303
302
|
last: timestamps.at(-1) ?? null,
|
|
304
303
|
days: new Set(timestamps.map((t) => t.slice(0, 10))).size,
|
|
@@ -311,43 +310,43 @@ export function summarize(
|
|
|
311
310
|
const known = active.filter((p) => p.knownSubtotal !== null);
|
|
312
311
|
const knownSubtotal = known.length ? known.reduce((n, p) => n + p.knownSubtotal , 0) : null;
|
|
313
312
|
const apiEquivalent = count > 0 && pricedRecords === count ? knownSubtotal : null;
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
active.
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
accumulated += (d.codex ?? 0) + (d.claude ?? 0);
|
|
332
|
-
if (accumulated >= subscription) {
|
|
333
|
-
breakEven = d.date;
|
|
334
|
-
break;
|
|
313
|
+
let comparison = neutral('Expenses not set');
|
|
314
|
+
if (active.length && active.every((p) => p.comparison.paid != null)) {
|
|
315
|
+
const subscription = active.reduce((n, p) => n + p.comparison.subscription , 0),
|
|
316
|
+
apiSpend = active.reduce((n, p) => n + p.comparison.apiSpend , 0),
|
|
317
|
+
paid = subscription + apiSpend;
|
|
318
|
+
comparison = { ...neutral('Partial history', subscription), apiSpend, paid };
|
|
319
|
+
if (apiEquivalent !== null) {
|
|
320
|
+
const value = apiEquivalent - paid;
|
|
321
|
+
let accumulated = 0,
|
|
322
|
+
breakEven = null;
|
|
323
|
+
if (demo)
|
|
324
|
+
for (const d of daily) {
|
|
325
|
+
accumulated += (d.codex ?? 0) + (d.claude ?? 0);
|
|
326
|
+
if (accumulated >= paid) {
|
|
327
|
+
breakEven = d.date;
|
|
328
|
+
break;
|
|
329
|
+
}
|
|
335
330
|
}
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
331
|
+
comparison = {
|
|
332
|
+
subscription,
|
|
333
|
+
apiSpend,
|
|
334
|
+
paid,
|
|
335
|
+
value,
|
|
336
|
+
roi: paid > 0 ? apiEquivalent / paid : null,
|
|
337
|
+
breakEven,
|
|
338
|
+
outcome:
|
|
339
|
+
demo && subscription > 0
|
|
340
|
+
? value > 0
|
|
341
|
+
? 'positive'
|
|
342
|
+
: value < 0
|
|
343
|
+
? 'negative'
|
|
344
|
+
: 'neutral'
|
|
345
|
+
: 'neutral',
|
|
346
|
+
reason: demo ? null : 'Observed usage · Partial history',
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
}
|
|
351
350
|
const confidence = active.length
|
|
352
351
|
? ['INCOMPLETE', 'LOW', 'MEDIUM', 'HIGH'].find((q) => active.some((p) => p.confidence === q))
|
|
353
352
|
: 'NO DATA';
|