Add subcription feature 6
This commit is contained in:
@@ -83,8 +83,7 @@ class ApiClient {
|
||||
|
||||
// Endpoints that require authentication but not a tenant ID (user-level endpoints)
|
||||
const noTenantEndpoints = [
|
||||
'/auth/me/onboarding', // Onboarding endpoints - tenant is created during onboarding
|
||||
'/auth/me', // User profile endpoints
|
||||
'/auth/users/', // User profile endpoints - user-level, no tenant context
|
||||
'/auth/register', // Registration
|
||||
'/auth/login', // Login
|
||||
'/geocoding', // Geocoding/address search - utility service, no tenant context
|
||||
|
||||
@@ -30,7 +30,34 @@ import {
|
||||
export class AuthService {
|
||||
private readonly baseUrl = '/auth';
|
||||
|
||||
// User Profile (authenticated)
|
||||
// Backend: services/auth/app/api/users.py
|
||||
// ===================================================================
|
||||
|
||||
async getProfile(): Promise<UserResponse> {
|
||||
// Get current user ID from auth store
|
||||
const { useAuthStore } = await import('../../stores/auth.store');
|
||||
const user = useAuthStore.getState().user;
|
||||
|
||||
if (!user?.id) {
|
||||
throw new Error('User not authenticated or user ID not available');
|
||||
}
|
||||
|
||||
return apiClient.get<UserResponse>(`${this.baseUrl}/users/${user.id}`);
|
||||
}
|
||||
|
||||
async updateProfile(updateData: UserUpdate): Promise<UserResponse> {
|
||||
// Get current user ID from auth store
|
||||
const { useAuthStore } = await import('../../stores/auth.store');
|
||||
const user = useAuthStore.getState().user;
|
||||
|
||||
if (!user?.id) {
|
||||
throw new Error('User not authenticated or user ID not available');
|
||||
}
|
||||
|
||||
return apiClient.put<UserResponse>(`${this.baseUrl}/users/${user.id}`, updateData);
|
||||
}
|
||||
|
||||
// ATOMIC REGISTRATION: SetupIntent-First Approach
|
||||
// These methods implement the secure registration flow with 3DS support
|
||||
// ===================================================================
|
||||
@@ -104,19 +131,6 @@ export class AuthService {
|
||||
});
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
// User Profile (authenticated)
|
||||
// Backend: services/auth/app/api/auth_operations.py
|
||||
// ===================================================================
|
||||
|
||||
async getProfile(): Promise<UserResponse> {
|
||||
return apiClient.get<UserResponse>(`${this.baseUrl}/me`);
|
||||
}
|
||||
|
||||
async updateProfile(updateData: UserUpdate): Promise<UserResponse> {
|
||||
return apiClient.put<UserResponse>(`${this.baseUrl}/me`, updateData);
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
// OPERATIONS: Email Verification
|
||||
// Backend: services/auth/app/api/auth_operations.py
|
||||
|
||||
@@ -179,7 +179,7 @@ export class SubscriptionService {
|
||||
* Get current usage summary for a tenant
|
||||
*/
|
||||
async getUsageSummary(tenantId: string): Promise<UsageSummary> {
|
||||
return apiClient.get<UsageSummary>(`${this.baseUrl}/subscriptions/${tenantId}/usage`);
|
||||
return apiClient.get<UsageSummary>(`/tenants/${tenantId}/subscription/usage`);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -190,7 +190,7 @@ export class SubscriptionService {
|
||||
featureName: string
|
||||
): Promise<FeatureCheckResponse> {
|
||||
return apiClient.get<FeatureCheckResponse>(
|
||||
`${this.baseUrl}/subscriptions/${tenantId}/features/${featureName}/check`
|
||||
`/tenants/${tenantId}/subscription/features/${featureName}`
|
||||
);
|
||||
}
|
||||
|
||||
@@ -202,23 +202,30 @@ export class SubscriptionService {
|
||||
quotaType: string,
|
||||
requestedAmount?: number
|
||||
): Promise<QuotaCheckResponse> {
|
||||
// Map quotaType to the existing endpoints in tenant_operations.py
|
||||
// Map quotaType to the new subscription limit endpoints
|
||||
let endpoint: string;
|
||||
switch (quotaType) {
|
||||
case 'inventory_items':
|
||||
endpoint = 'can-add-product';
|
||||
case 'products':
|
||||
endpoint = 'products';
|
||||
break;
|
||||
case 'users':
|
||||
endpoint = 'can-add-user';
|
||||
endpoint = 'users';
|
||||
break;
|
||||
case 'locations':
|
||||
endpoint = 'can-add-location';
|
||||
endpoint = 'locations';
|
||||
break;
|
||||
case 'recipes':
|
||||
endpoint = 'recipes';
|
||||
break;
|
||||
case 'suppliers':
|
||||
endpoint = 'suppliers';
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unsupported quota type: ${quotaType}`);
|
||||
}
|
||||
|
||||
const url = `${this.baseUrl}/subscriptions/${tenantId}/${endpoint}`;
|
||||
const url = `/tenants/${tenantId}/subscription/limits/${endpoint}`;
|
||||
|
||||
// Get the response from the endpoint (returns different format than expected)
|
||||
const response = await apiClient.get<{
|
||||
@@ -242,27 +249,35 @@ export class SubscriptionService {
|
||||
}
|
||||
|
||||
async validatePlanUpgrade(tenantId: string, planKey: string): Promise<PlanUpgradeValidation> {
|
||||
return apiClient.get<PlanUpgradeValidation>(`${this.baseUrl}/subscriptions/${tenantId}/validate-upgrade/${planKey}`);
|
||||
return apiClient.get<PlanUpgradeValidation>(`/tenants/${tenantId}/subscription/validate-upgrade/${planKey}`);
|
||||
}
|
||||
|
||||
async upgradePlan(tenantId: string, planKey: string): Promise<PlanUpgradeResult> {
|
||||
return apiClient.post<PlanUpgradeResult>(`${this.baseUrl}/subscriptions/${tenantId}/upgrade?new_plan=${planKey}`, {});
|
||||
return apiClient.post<PlanUpgradeResult>(`/tenants/${tenantId}/subscription/upgrade`, { new_plan: planKey });
|
||||
}
|
||||
|
||||
async canAddLocation(tenantId: string): Promise<{ can_add: boolean; reason?: string; current_count?: number; max_allowed?: number }> {
|
||||
return apiClient.get(`${this.baseUrl}/subscriptions/${tenantId}/can-add-location`);
|
||||
return apiClient.get(`/tenants/${tenantId}/subscription/limits/locations`);
|
||||
}
|
||||
|
||||
async canAddProduct(tenantId: string): Promise<{ can_add: boolean; reason?: string; current_count?: number; max_allowed?: number }> {
|
||||
return apiClient.get(`${this.baseUrl}/subscriptions/${tenantId}/can-add-product`);
|
||||
return apiClient.get(`/tenants/${tenantId}/subscription/limits/products`);
|
||||
}
|
||||
|
||||
async canAddUser(tenantId: string): Promise<{ can_add: boolean; reason?: string; current_count?: number; max_allowed?: number }> {
|
||||
return apiClient.get(`${this.baseUrl}/subscriptions/${tenantId}/can-add-user`);
|
||||
return apiClient.get(`/tenants/${tenantId}/subscription/limits/users`);
|
||||
}
|
||||
|
||||
async canAddRecipe(tenantId: string): Promise<{ can_add: boolean; reason?: string; current_count?: number; max_allowed?: number }> {
|
||||
return apiClient.get(`/tenants/${tenantId}/subscription/limits/recipes`);
|
||||
}
|
||||
|
||||
async canAddSupplier(tenantId: string): Promise<{ can_add: boolean; reason?: string; current_count?: number; max_allowed?: number }> {
|
||||
return apiClient.get(`/tenants/${tenantId}/subscription/limits/suppliers`);
|
||||
}
|
||||
|
||||
async hasFeature(tenantId: string, featureName: string): Promise<{ has_feature: boolean; feature_value?: any; plan?: string; reason?: string }> {
|
||||
return apiClient.get(`${this.baseUrl}/subscriptions/${tenantId}/features/${featureName}`);
|
||||
return apiClient.get(`/tenants/${tenantId}/subscription/features/${featureName}`);
|
||||
}
|
||||
|
||||
formatPrice(amount: number): string {
|
||||
@@ -348,8 +363,7 @@ export class SubscriptionService {
|
||||
days_remaining: number;
|
||||
read_only_mode_starts: string;
|
||||
}> {
|
||||
return apiClient.post('/subscriptions/cancel', {
|
||||
tenant_id: tenantId,
|
||||
return apiClient.post(`/tenants/${tenantId}/subscription/cancel`, {
|
||||
reason: reason || ''
|
||||
});
|
||||
}
|
||||
@@ -364,8 +378,7 @@ export class SubscriptionService {
|
||||
plan: string;
|
||||
next_billing_date: string | null;
|
||||
}> {
|
||||
return apiClient.post('/subscriptions/reactivate', {
|
||||
tenant_id: tenantId,
|
||||
return apiClient.post(`/tenants/${tenantId}/subscription/reactivate`, {
|
||||
plan
|
||||
});
|
||||
}
|
||||
@@ -383,7 +396,7 @@ export class SubscriptionService {
|
||||
billing_cycle?: string;
|
||||
next_billing_date?: string;
|
||||
}> {
|
||||
return apiClient.get(`/subscriptions/${tenantId}/status`);
|
||||
return apiClient.get(`/tenants/${tenantId}/subscription/status`);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -399,7 +412,7 @@ export class SubscriptionService {
|
||||
invoice_pdf: string | null;
|
||||
hosted_invoice_url: string | null;
|
||||
}>> {
|
||||
return apiClient.get(`/subscriptions/${tenantId}/invoices`);
|
||||
return apiClient.get(`/tenants/${tenantId}/subscription/invoices`);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -414,7 +427,7 @@ export class SubscriptionService {
|
||||
exp_year?: number;
|
||||
} | null> {
|
||||
try {
|
||||
const response = await apiClient.get(`/subscriptions/${tenantId}/payment-method`);
|
||||
const response = await apiClient.get(`/tenants/${tenantId}/subscription/payment-method`);
|
||||
return response;
|
||||
} catch (error) {
|
||||
console.error('Failed to get current payment method:', error);
|
||||
@@ -440,51 +453,13 @@ export class SubscriptionService {
|
||||
client_secret?: string;
|
||||
payment_intent_status?: string;
|
||||
}> {
|
||||
return apiClient.post(`/subscriptions/${tenantId}/update-payment-method?payment_method_id=${paymentMethodId}`, {});
|
||||
}
|
||||
|
||||
/**
|
||||
* Complete subscription creation after SetupIntent confirmation
|
||||
*
|
||||
* This method is called after the frontend successfully confirms a SetupIntent
|
||||
* (with or without 3DS authentication). It verifies the SetupIntent and creates
|
||||
* the subscription with the verified payment method.
|
||||
*
|
||||
* @param setupIntentId - The SetupIntent ID that was confirmed by Stripe
|
||||
* @param subscriptionData - Data needed to complete subscription creation
|
||||
* @returns Promise with subscription creation result
|
||||
*/
|
||||
async completeSubscriptionAfterSetupIntent(
|
||||
setupIntentId: string,
|
||||
subscriptionData: {
|
||||
customer_id: string;
|
||||
plan_id: string;
|
||||
payment_method_id: string;
|
||||
trial_period_days?: number;
|
||||
user_id: string;
|
||||
billing_interval: string;
|
||||
}
|
||||
): Promise<{
|
||||
success: boolean;
|
||||
message: string;
|
||||
data: {
|
||||
subscription_id: string;
|
||||
customer_id: string;
|
||||
status: string;
|
||||
plan: string;
|
||||
billing_cycle: string;
|
||||
trial_period_days?: number;
|
||||
current_period_end: string;
|
||||
user_id: string;
|
||||
setup_intent_id: string;
|
||||
};
|
||||
}> {
|
||||
return apiClient.post('/subscriptions/complete-after-setup-intent', {
|
||||
setup_intent_id: setupIntentId,
|
||||
...subscriptionData
|
||||
return apiClient.post(`/tenants/${tenantId}/subscription/payment-method`, {
|
||||
payment_method_id: paymentMethodId
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
// ============================================================================
|
||||
// NEW METHODS - Usage Forecasting & Predictive Analytics
|
||||
// ============================================================================
|
||||
|
||||
@@ -47,7 +47,7 @@ export class TenantService {
|
||||
userId: string
|
||||
): Promise<SubscriptionLinkingResponse> {
|
||||
return apiClient.post<SubscriptionLinkingResponse>(
|
||||
`${this.baseUrl}/subscriptions/link`,
|
||||
`${this.baseUrl}/link-subscription`,
|
||||
{ tenant_id: tenantId, subscription_id: subscriptionId, user_id: userId }
|
||||
);
|
||||
}
|
||||
|
||||
@@ -9,7 +9,15 @@ export class UserService {
|
||||
private readonly baseUrl = '/users';
|
||||
|
||||
async getCurrentUser(): Promise<UserResponse> {
|
||||
return apiClient.get<UserResponse>(`${this.baseUrl}/me`);
|
||||
// Get current user ID from auth store
|
||||
const authStore = useAuthStore.getState();
|
||||
const userId = authStore.user?.id;
|
||||
|
||||
if (!userId) {
|
||||
throw new Error('No authenticated user found');
|
||||
}
|
||||
|
||||
return apiClient.get<UserResponse>(`${this.baseUrl}/${userId}`);
|
||||
}
|
||||
|
||||
async updateUser(userId: string, updateData: UserUpdate): Promise<UserResponse> {
|
||||
|
||||
@@ -83,7 +83,7 @@ export const LoginForm: React.FC<LoginFormProps> = ({
|
||||
});
|
||||
onSuccess?.();
|
||||
} catch (err) {
|
||||
showError(error || 'Email o contraseña incorrectos. Verifica tus credenciales.', {
|
||||
showToast.error(error || 'Email o contraseña incorrectos. Verifica tus credenciales.', {
|
||||
title: 'Error al iniciar sesión'
|
||||
});
|
||||
}
|
||||
|
||||
@@ -558,10 +558,15 @@ const SubscriptionPageRedesign: React.FC = () => {
|
||||
try {
|
||||
setInvoicesLoading(true);
|
||||
const fetchedInvoices = await subscriptionService.getInvoices(tenantId);
|
||||
setInvoices(fetchedInvoices);
|
||||
|
||||
// Ensure fetchedInvoices is an array before setting state
|
||||
const validatedInvoices = Array.isArray(fetchedInvoices) ? fetchedInvoices : [];
|
||||
setInvoices(validatedInvoices);
|
||||
setInvoicesLoaded(true);
|
||||
} catch (error) {
|
||||
console.error('Error loading invoices:', error);
|
||||
// Set invoices to empty array in case of error to prevent slice error
|
||||
setInvoices([]);
|
||||
if (invoicesLoaded) {
|
||||
showToast.error('Error al cargar las facturas');
|
||||
}
|
||||
@@ -920,7 +925,7 @@ const SubscriptionPageRedesign: React.FC = () => {
|
||||
<p className="text-[var(--text-secondary)]">Cargando facturas...</p>
|
||||
</div>
|
||||
</div>
|
||||
) : invoices.length === 0 ? (
|
||||
) : (!Array.isArray(invoices) || invoices.length === 0) ? (
|
||||
<div className="text-center py-8">
|
||||
<div className="flex flex-col items-center gap-3">
|
||||
<div className="p-4 bg-[var(--bg-secondary)] rounded-full">
|
||||
@@ -943,7 +948,7 @@ const SubscriptionPageRedesign: React.FC = () => {
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{invoices.slice(0, 5).map((invoice) => (
|
||||
{Array.isArray(invoices) && invoices.slice(0, 5).map((invoice) => (
|
||||
<tr key={invoice.id} className="border-b border-[var(--border-color)] hover:bg-[var(--bg-secondary)] transition-colors">
|
||||
<td className="py-3 px-4 text-[var(--text-primary)] font-medium">
|
||||
{new Date(invoice.date).toLocaleDateString('es-ES', {
|
||||
@@ -979,7 +984,7 @@ const SubscriptionPageRedesign: React.FC = () => {
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
{invoices.length > 5 && (
|
||||
{Array.isArray(invoices) && invoices.length > 5 && (
|
||||
<div className="mt-4 text-center">
|
||||
<Button variant="ghost" onClick={() => setShowBilling(true)}>
|
||||
Ver todas las facturas ({invoices.length})
|
||||
|
||||
Reference in New Issue
Block a user