Files
chat_web_app/llm-fe/src/llm-fe/utils/finance.test.ts
T
westfarn ce86f56d37
Deploy Beta / unit-tests (push) Successful in 12s
Unit Tests / test (push) Successful in 11s
Deploy Beta / deploy-beta (push) Failing after 2m20s
RevenueCat Capacitor IAP; keep Stripe on web (#100) (#102)
## Summary
- Native Capacitor: RevenueCat purchase/restore (`@revenuecat/purchases-capacitor`)
- Web: Stripe checkout/portal unchanged
- Billing history shows store rows (`provider` / `revenuecat_store`)
- RC `logIn`/`logOut` on auth lifecycle; appUserID = user pk

Closes #100. Depends on backend [chat_backend#68](ai_ml_operations/chat_backend#68) / monetization PR.

## Test plan
- [x] Unit tests: BillingSection, revenueCat, finance helpers, SignIn/Up, AuthCallback, Header2, DeleteAccount (38 OK)
- [ ] Fill `REACT_APP_REVENUECAT_*` keys in `.env.mobile`
- [ ] Align RC offerings/products with plan slugs
- [ ] `npm run build:mobile` + `cap sync`; sandbox purchase on Android/iOS
- [ ] Confirm web Stripe checkout/portal still work
- [ ] After RC webhook, Account → Billing history shows store invoiceReviewed-on: #102
2026-08-04 03:43:10 -07:00

186 lines
5.2 KiB
TypeScript

import {
canOpenBillingPortal,
formatMoneyCents,
formatTokenCount,
humanizeStatus,
invoiceProviderLabel,
isComplimentarySubscription,
isStoreSubscription,
isStripeSubscription,
pickPrimaryInvoice,
planAllowsRag,
} from './finance';
import type { FinanceInvoice, SubscriptionMe, SubscriptionPlanInfo } from './finance';
const basePlan = (overrides: Partial<SubscriptionPlanInfo> = {}): SubscriptionPlanInfo => ({
slug: 'standard',
name: 'Standard',
description: '',
price_cents: 999,
currency: 'usd',
interval: 'month',
is_public: true,
is_selectable: true,
features: {
text_generation: true,
image_generation: false,
rag: false,
all_future_features: false,
},
prompt_quota_per_window: 100,
prompt_window_hours: 6,
monthly_token_quota: null,
sort_order: 1,
...overrides,
});
const baseUsage: SubscriptionMe['usage'] = {
prompts_in_window: 0,
prompt_quota: null,
prompts_remaining: null,
window_hours: 6,
tokens_in_period: null,
tokens_out_period: null,
tokens_total_period: null,
turns_missing_token_usage: 0,
monthly_token_quota: null,
tokens_remaining: null,
period_start: null,
period_end: null,
};
const baseInvoice = (overrides: Partial<FinanceInvoice> = {}): FinanceInvoice => ({
id: 1,
provider: 'stripe',
status: 'open',
currency: 'usd',
amount_due: 1000,
amount_paid: 0,
period_start: null,
period_end: null,
stripe_invoice_id: null,
stripe_checkout_session_id: 'cs_1',
stripe_subscription_id: null,
hosted_invoice_url: '',
description: 'Chat Subscription',
created: '2026-07-01T00:00:00Z',
last_modified: '2026-07-01T00:00:00Z',
...overrides,
});
describe('finance helpers', () => {
it('formats money in cents', () => {
expect(formatMoneyCents(1000, 'usd')).toMatch(/10/);
});
it('shows em dash for missing token usage', () => {
expect(formatTokenCount(null)).toBe('—');
expect(formatTokenCount(undefined)).toBe('—');
expect(formatTokenCount(0)).toBe('0');
expect(formatTokenCount(1200)).toMatch(/1/);
});
it('humanizes status labels', () => {
expect(humanizeStatus('past_due')).toBe('Past Due');
});
it('prefers subscription-backed invoices for plan summary', () => {
const invoices = [
baseInvoice({ id: 1, status: 'open' }),
baseInvoice({
id: 2,
status: 'paid',
stripe_subscription_id: 'sub_1',
}),
];
expect(pickPrimaryInvoice(invoices)?.id).toBe(2);
});
it('detects portal access from paid Stripe invoices only', () => {
expect(canOpenBillingPortal([baseInvoice({ status: 'open' })])).toBe(false);
expect(canOpenBillingPortal([baseInvoice({ status: 'paid' })])).toBe(true);
expect(
canOpenBillingPortal([
baseInvoice({
status: 'paid',
provider: 'revenuecat',
stripe_subscription_id: null,
revenuecat_store: 'PLAY_STORE',
}),
])
).toBe(false);
});
it('labels invoice providers for history', () => {
expect(invoiceProviderLabel(baseInvoice({ provider: 'stripe' }))).toBe('Stripe');
expect(
invoiceProviderLabel(
baseInvoice({ provider: 'revenuecat', revenuecat_store: 'PLAY_STORE' })
)
).toBe('Play Store');
expect(
invoiceProviderLabel(
baseInvoice({ provider: 'revenuecat', revenuecat_store: 'APP_STORE' })
)
).toBe('App Store');
});
it('classifies subscription sources', () => {
expect(isStoreSubscription('revenuecat')).toBe(true);
expect(isStripeSubscription('stripe')).toBe(true);
expect(
isComplimentarySubscription(
{ source: 'revenuecat', needs_checkout: false },
false
)
).toBe(false);
});
});
describe('planAllowsRag', () => {
it('is false when there is no plan/subscription', () => {
expect(planAllowsRag(null)).toBe(false);
expect(planAllowsRag(undefined)).toBe(false);
});
it('is false when the rag feature flag is off', () => {
expect(planAllowsRag(basePlan())).toBe(false);
});
it('is true when the rag feature flag is on', () => {
expect(
planAllowsRag(basePlan({ features: { text_generation: true, image_generation: false, rag: true, all_future_features: false } }))
).toBe(true);
});
it('is true when all_future_features unlocks it', () => {
expect(
planAllowsRag(basePlan({ features: { text_generation: true, image_generation: true, rag: false, all_future_features: true } }))
).toBe(true);
});
it('accepts a SubscriptionMe wrapper and reads its plan', () => {
const subscription: SubscriptionMe = {
plan: basePlan({ features: { text_generation: true, image_generation: false, rag: true, all_future_features: false } }),
status: 'active',
source: 'stripe',
needs_checkout: false,
stripe_subscription_id: 'sub_1',
usage: baseUsage,
};
expect(planAllowsRag(subscription)).toBe(true);
});
it('is false when a subscription has no plan yet', () => {
const subscription: SubscriptionMe = {
plan: null,
status: 'none',
source: 'none',
needs_checkout: true,
stripe_subscription_id: '',
usage: baseUsage,
};
expect(planAllowsRag(subscription)).toBe(false);
});
});