Skip to content
Merged

Cli #367

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 28 additions & 7 deletions app/api/_lib/polar/services/polar-webhook-processor.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@

import { UserRepository } from '@/app/api/_lib/polar/repositories/user.repository';
import { SubscriptionRepository } from '@/app/api/_lib/polar/repositories/subscription.repository';
import { PolarService } from '@/app/api/_lib/polar/services/polar.service';
import { PlanType, SubscriptionProcessResult } from '@/app/api/_lib/polar/types';
import { extractPlanPrice } from '@/app/api/_lib/polar/utils/price-extractor.util';

/**
* Servicio para procesar datos de webhooks de Polar
Expand All @@ -34,9 +36,28 @@ export class PolarWebhookProcessorService {
*/
async processSubscriptionWithData(subscriptionId: string, polarData: any): Promise<SubscriptionProcessResult> {
try {
const userId = polarData.customerExternalId || polarData.customer?.externalId;
// Manejar tanto el campo antiguo como el nuevo
const userId =
polarData.customerExternalId ||
polarData.customer?.externalId ||
polarData.customer?.external_customer_id ||
polarData.customer_external_id ||
polarData.external_customer_id;

if (!userId) {
// Si no encontramos el external_id, intentar obtenerlo del customer_id
let finalUserId = userId;
if (!finalUserId && polarData.customer_id) {
console.log(`external_id not found in webhook payload, fetching from customer_id: ${polarData.customer_id}`);
try {
const polarService = new PolarService(process.env.POLAR_ACCESS_TOKEN || '');
const customer = await polarService.getCustomer(polarData.customer_id);
finalUserId = customer?.externalId || '';
} catch (error) {
console.error(`Error fetching customer ${polarData.customer_id}:`, error);
}
}

if (!finalUserId) {
return {
success: false,
userId: '',
Expand All @@ -47,14 +68,14 @@ export class PolarWebhookProcessorService {

// Determinar acción basada en el estado de la suscripción
if (this.isSubscriptionActiveFromData(polarData)) {
return await this.activateSubscriptionWithData(userId, polarData);
return await this.activateSubscriptionWithData(finalUserId, polarData);
} else if (this.isSubscriptionCanceledFromData(polarData)) {
return await this.cancelSubscriptionWithData(userId, polarData);
return await this.cancelSubscriptionWithData(finalUserId, polarData);
}

return {
success: true,
userId,
userId: finalUserId,
plan: PlanType.FREE,
message: 'Subscription processed successfully',
};
Expand Down Expand Up @@ -110,7 +131,7 @@ export class PolarWebhookProcessorService {
const nextPaymentDate = polarData.currentPeriodEnd
? new Date(polarData.currentPeriodEnd).toISOString()
: undefined;
const planPrice = polarData.amount ? polarData.amount / 100 : undefined;
const planPrice = extractPlanPrice(polarData);

await this.subscriptionRepository.update(userId, {
nextPaymentDate,
Expand Down Expand Up @@ -235,7 +256,7 @@ export class PolarWebhookProcessorService {
*/
private extractSubscriptionDataFromPolar(userId: string, polarData: any, plan: PlanType): any {
const nextPaymentDate = polarData.currentPeriodEnd ? new Date(polarData.currentPeriodEnd).toISOString() : undefined;
const planPrice = polarData.amount ? polarData.amount / 100 : undefined; // Convertir de centavos a dólares
const planPrice = extractPlanPrice(polarData); // Convertir de centavos a dólares

return {
id: userId,
Expand Down
10 changes: 9 additions & 1 deletion app/api/_lib/polar/services/polar.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,11 +140,19 @@ export class PolarService {
* Mapea la respuesta de Polar a nuestro tipo PolarSubscription
*/
private mapToPolarSubscription(polarResponse: any): PolarSubscription {
// Manejar tanto el campo antiguo como el nuevo
const customerExternalId =
polarResponse.customer?.externalId ||
polarResponse.customer?.external_customer_id ||
polarResponse.customer_external_id ||
polarResponse.external_customer_id ||
'';

return {
id: polarResponse.id,
status: polarResponse.status as SubscriptionStatus,
customerId: polarResponse.customer?.id || '',
customerExternalId: polarResponse.customer?.externalId || '',
customerExternalId,
productId: polarResponse.productId || '',
amount: polarResponse.amount || 0,
currentPeriodEnd: polarResponse.currentPeriodEnd ? new Date(polarResponse.currentPeriodEnd) : new Date(),
Expand Down
5 changes: 3 additions & 2 deletions app/api/_lib/polar/services/subscription.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { UserRepository } from '@/app/api/_lib/polar/repositories/user.repositor
import { SubscriptionRepository } from '@/app/api/_lib/polar/repositories/subscription.repository';
import { PolarService } from '@/app/api/_lib/polar/services/polar.service';
import { PlanType, SubscriptionProcessResult, ProductConfig, ProductPlanMapping } from '@/app/api/_lib/polar/types';
import { extractPlanPrice } from '@/app/api/_lib/polar/utils/price-extractor.util';

/**
* Servicio de aplicación para lógica de negocio de suscripciones
Expand Down Expand Up @@ -123,7 +124,7 @@ export class SubscriptionService {
const nextPaymentDate = polarSubscription?.currentPeriodEnd
? new Date(polarSubscription.currentPeriodEnd).toISOString()
: undefined;
const planPrice = polarSubscription?.amount ? polarSubscription.amount / 100 : undefined;
const planPrice = extractPlanPrice(polarSubscription);

await this.subscriptionRepository.update(userId, {
nextPaymentDate,
Expand All @@ -147,7 +148,7 @@ export class SubscriptionService {
const nextPaymentDate = polarSubscription?.currentPeriodEnd
? new Date(polarSubscription.currentPeriodEnd).toISOString()
: undefined;
const planPrice = polarSubscription?.amount ? polarSubscription.amount / 100 : undefined;
const planPrice = extractPlanPrice(polarSubscription);

await this.subscriptionRepository.upsert({
id: userId,
Expand Down
34 changes: 34 additions & 0 deletions app/api/_lib/polar/utils/price-extractor.util.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/**
* Utilidad para extraer precios de los payloads de Polar
* Centraliza la lógica de extracción de precios para evitar duplicación
*/

/**
* Extrae el precio del plan desde los datos de Polar
* Busca en múltiples ubicaciones basándose en los payloads reales de Polar
*/
export function extractPlanPrice(polarData: any): number | undefined {
// Basándose en los payloads reales de Polar:
// - subscription.updated: price.price_amount
// - order.paid: product_price.price_amount
// - subscription.revoked: amount

const priceSources = [
polarData?.amount, // Campo directo amount (subscription.revoked)
polarData?.price?.price_amount, // Dentro del objeto price (subscription.updated)
polarData?.product_price?.price_amount, // Dentro de product_price (order.paid)
polarData?.prices?.[0]?.price_amount, // Primer elemento del array prices
];

// Buscar el primer valor válido (no null, undefined, o 0)
for (const price of priceSources) {
if (price && price > 0) {
const finalPrice = price / 100; // Convertir de centavos a dólares
console.log(`Found plan price: $${finalPrice} from source: ${price}`);
return finalPrice;
}
}

console.warn(`No valid price found in Polar data. Sources checked:`, priceSources);
return undefined;
}
5 changes: 1 addition & 4 deletions app/store/hooks/useSubscriptionLogic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,10 +58,7 @@ export function useSubscriptionLogic(userId?: string): UseSubscriptionLogicResul
subscription.subscriptionId && !subscription.subscriptionId.startsWith('trial-')
);

// Verificar que tiene planPrice mayor a 0
const hasValidPrice = (subscription.planPrice ?? 0) > 0;

return hasValidSubscriptionId && hasValidPrice;
return hasValidSubscriptionId;
}, [subscription]);

// Determinar si es plan pagado basado en el precio del plan
Expand Down
13 changes: 13 additions & 0 deletions jest.setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,19 @@ process.env.DEV_CACHE_ENABLED = 'true';

global.console.warn = jest.fn();

// Polyfill para Request y Response (necesario para el SDK de Polar)
global.Request =
global.Request ||
class Request {
constructor(_input: string | Request, _init?: RequestInit) {}
};

global.Response =
global.Response ||
class Response {
constructor(_body?: BodyInit | null, _init?: ResponseInit) {}
};

Object.defineProperty(window, 'matchMedia', {
writable: true,
value: jest.fn().mockImplementation((query) => ({
Expand Down
1 change: 1 addition & 0 deletions middlewares/store-access/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ export async function handleStoreMiddleware(request: NextRequest, response: Next
return authResponse; // Si hay redirección de auth, retornarla
}

// Obtener la sesión que ya fue validada en handleAuthenticationMiddleware
const session = await getSession(request, response);

const userId = (session as AuthSession).tokens?.idToken?.payload?.['cognito:username'];
Expand Down