Greenlens/services/backend/contracts.ts

157 lines
3.5 KiB
TypeScript

import { CareInfo, IdentificationResult, Language, PlantHealthCheck } from '../../types';
export type PlanId = 'free' | 'pro';
export type BillingProvider = 'mock' | 'revenuecat' | 'stripe';
export type PurchaseProductId = 'monthly_pro' | 'yearly_pro' | 'topup_small' | 'topup_medium' | 'topup_large';
export type SimulatedWebhookEvent =
| 'entitlement_granted'
| 'entitlement_revoked'
| 'topup_granted'
| 'credits_depleted';
export interface BackendDatabaseEntry extends IdentificationResult {
imageUri: string;
imageStatus?: 'ok' | 'missing' | 'invalid';
categories: string[];
}
export interface CreditState {
monthlyAllowance: number;
usedThisCycle: number;
topupBalance: number;
available: number;
cycleStartedAt: string;
cycleEndsAt: string;
}
export interface EntitlementState {
plan: PlanId;
provider: BillingProvider;
status: 'active' | 'inactive';
renewsAt: string | null;
}
export interface BillingSummary {
entitlement: EntitlementState;
credits: CreditState;
availableProducts: PurchaseProductId[];
}
export interface ScanPlantRequest {
userId: string;
idempotencyKey: string;
imageUri: string;
language: Language;
}
export interface ScanPlantResponse {
result: IdentificationResult;
creditsCharged: number;
modelPath: string[];
modelUsed?: string | null;
modelFallbackCount?: number;
billing: BillingSummary;
}
export interface SemanticSearchRequest {
userId: string;
idempotencyKey: string;
query: string;
language: Language;
}
export interface SemanticSearchResponse {
status: 'success' | 'no_results';
results: BackendDatabaseEntry[];
creditsCharged: number;
billing: BillingSummary;
}
export interface HealthCheckRequest {
userId: string;
idempotencyKey: string;
imageUri: string;
language: Language;
plantContext?: {
name: string;
botanicalName: string;
careInfo: CareInfo;
description?: string;
};
}
export interface HealthCheckResponse {
healthCheck: PlantHealthCheck;
creditsCharged: number;
modelUsed?: string | null;
modelFallbackCount?: number;
billing: BillingSummary;
}
export interface ServiceHealthResponse {
ok: boolean;
uptimeSec: number;
timestamp: string;
openAiConfigured: boolean;
dbReady?: boolean;
dbPath?: string;
stripeConfigured?: boolean;
scanModel?: string;
healthModel?: string;
}
export interface SimulatePurchaseRequest {
userId: string;
idempotencyKey: string;
productId: PurchaseProductId;
}
export interface SimulatePurchaseResponse {
appliedProduct: PurchaseProductId;
billing: BillingSummary;
}
export interface SimulateWebhookRequest {
userId: string;
idempotencyKey: string;
event: SimulatedWebhookEvent;
payload?: {
credits?: number;
};
}
export interface SimulateWebhookResponse {
event: SimulatedWebhookEvent;
billing: BillingSummary;
}
export type BackendErrorCode =
| 'INSUFFICIENT_CREDITS'
| 'UNAUTHORIZED'
| 'TIMEOUT'
| 'PROVIDER_ERROR'
| 'BAD_REQUEST';
export class BackendApiError extends Error {
public readonly code: BackendErrorCode;
public readonly status: number;
public readonly metadata?: Record<string, unknown>;
constructor(
code: BackendErrorCode,
message: string,
status = 500,
metadata?: Record<string, unknown>,
) {
super(message);
this.name = 'BackendApiError';
this.code = code;
this.status = status;
this.metadata = metadata;
}
}
export const isBackendApiError = (error: unknown): error is BackendApiError => {
return error instanceof BackendApiError;
};