Onboarding

This commit is contained in:
2026-05-08 13:00:30 +02:00
parent d37b49f1f6
commit 9386ae1be7
37 changed files with 5606 additions and 2275 deletions

View File

@@ -1,5 +1,6 @@
import AsyncStorage from '@react-native-async-storage/async-storage';
import { mockBackendService } from '../../services/backend/mockBackendService';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { mockBackendService } from '../../services/backend/mockBackendService';
import { openAiScanService } from '../../services/backend/openAiScanService';
jest.mock('@react-native-async-storage/async-storage', () => ({
getItem: jest.fn(),
@@ -11,7 +12,7 @@ const asyncStorageMemory: Record<string, string> = {};
const mockedAsyncStorage = AsyncStorage as jest.Mocked<typeof AsyncStorage>;
const runScan = async (userId: string, idempotencyKey: string) => {
const runScan = async (userId: string, idempotencyKey: string) => {
const settledPromise = mockBackendService.scanPlant({
userId,
idempotencyKey,
@@ -26,7 +27,33 @@ const runScan = async (userId: string, idempotencyKey: string) => {
const settled = await settledPromise;
if (!settled.ok) throw settled.error;
return settled.value;
};
};
const runHealthCheck = async (userId: string, idempotencyKey: string) => {
const settledPromise = mockBackendService.healthCheck({
userId,
idempotencyKey,
imageUri: `data:image/jpeg;base64,${idempotencyKey}`,
language: 'en',
plantContext: {
name: 'Monstera',
botanicalName: 'Monstera deliciosa',
careInfo: {
waterIntervalDays: 7,
light: 'Bright indirect light',
temp: '18-24C',
},
},
}).then(
value => ({ ok: true as const, value }),
error => ({ ok: false as const, error }),
);
await Promise.resolve();
await jest.runAllTimersAsync();
const settled = await settledPromise;
if (!settled.ok) throw settled.error;
return settled.value;
};
describe('mockBackendService billing simulation', () => {
beforeEach(() => {
@@ -48,10 +75,11 @@ describe('mockBackendService billing simulation', () => {
});
});
afterEach(() => {
jest.useRealTimers();
jest.clearAllMocks();
});
afterEach(() => {
jest.useRealTimers();
jest.restoreAllMocks();
jest.clearAllMocks();
});
it('keeps simulatePurchase idempotent for same idempotency key', async () => {
const userId = 'test-user-idempotency';
@@ -142,6 +170,40 @@ describe('mockBackendService billing simulation', () => {
expect(second.billing.credits.available).toBe(first.billing.credits.available);
});
it('charges one credit for a normal scan after a two-credit health check', async () => {
const userId = 'test-user-health-then-scan-cost';
jest.spyOn(openAiScanService, 'isConfigured').mockReturnValue(true);
jest.spyOn(openAiScanService, 'analyzePlantHealth').mockResolvedValue({
overallHealthScore: 72,
status: 'watch',
analysisSummary: 'Mild stress signs are visible.',
likelyIssues: [
{
title: 'Watering stress',
confidence: 0.62,
details: 'The leaf texture suggests inconsistent watering.',
},
],
actionsNow: ['Check soil moisture before watering.'],
plan7Days: ['Take a comparison photo in one week.'],
});
await mockBackendService.simulatePurchase({
userId,
idempotencyKey: 'sub-health-then-scan-cost',
productId: 'monthly_pro',
});
const healthCheck = await runHealthCheck(userId, 'health-cost-1');
expect(healthCheck.creditsCharged).toBe(2);
expect(healthCheck.billing.credits.usedThisCycle).toBe(2);
const scan = await runScan(userId, 'scan-after-health-cost-1');
expect(scan.modelPath).toContain('mock-review');
expect(scan.creditsCharged).toBe(1);
expect(scan.billing.credits.usedThisCycle).toBe(3);
});
it('blocks free users from real scans', async () => {
const userId = 'test-user-credit-limit';
let successfulScans = 0;