Skip to content
Merged
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
56 changes: 47 additions & 9 deletions amplify/data/models/user-subscription.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,28 +5,66 @@ export const userSubscriptionModel = a
id: a
.id()
.required()
.authorization((allow) => [allow.ownerDefinedIn('userId').to(['create', 'read', 'update', 'delete'])]),
.authorization((allow) => [
allow.ownerDefinedIn('userId').to(['create', 'read', 'update', 'delete']),
allow.publicApiKey().to(['read', 'create', 'update', 'delete']),
]),
userId: a
.string()
.required()
.authorization((allow) => [allow.ownerDefinedIn('userId').to(['create', 'read', 'delete'])]),
.authorization((allow) => [
allow.ownerDefinedIn('userId').to(['create', 'read', 'delete']),
allow.publicApiKey().to(['create', 'read', 'delete']),
]),
subscriptionId: a
.string()
.required()
.authorization((allow) => [allow.ownerDefinedIn('userId').to(['create', 'read', 'update', 'delete'])]),
.authorization((allow) => [
allow.ownerDefinedIn('userId').to(['create', 'read', 'update', 'delete']),
allow.publicApiKey().to(['read', 'create', 'update', 'delete']),
]),
planName: a
.string()
.required()
.authorization((allow) => [allow.ownerDefinedIn('userId').to(['create', 'read', 'update', 'delete'])]),
nextPaymentDate: a.datetime(),
pendingPlan: a.string(),
pendingStartDate: a.datetime(),
planPrice: a.float(),
lastFourDigits: a.integer(),
.authorization((allow) => [
allow.ownerDefinedIn('userId').to(['create', 'read', 'update', 'delete']),
allow.publicApiKey().to(['read', 'create', 'update', 'delete']),
]),
nextPaymentDate: a
.datetime()
.authorization((allow) => [
allow.ownerDefinedIn('userId').to(['create', 'read', 'update', 'delete']),
allow.publicApiKey().to(['read', 'create', 'update', 'delete']),
]),
pendingPlan: a
.string()
.authorization((allow) => [
allow.ownerDefinedIn('userId').to(['create', 'read', 'update', 'delete']),
allow.publicApiKey().to(['read', 'create', 'update', 'delete']),
]),
pendingStartDate: a
.datetime()
.authorization((allow) => [
allow.ownerDefinedIn('userId').to(['create', 'read', 'update', 'delete']),
allow.publicApiKey().to(['read', 'create', 'update', 'delete']),
]),
planPrice: a
.float()
.authorization((allow) => [
allow.ownerDefinedIn('userId').to(['create', 'read', 'update', 'delete']),
allow.publicApiKey().to(['read', 'create', 'update', 'delete']),
]),
lastFourDigits: a
.integer()
.authorization((allow) => [
allow.ownerDefinedIn('userId').to(['create', 'read', 'update', 'delete']),
allow.publicApiKey().to(['read', 'create', 'update', 'delete']),
]),
})
.identifier(['id'])
.secondaryIndexes((index) => [index('userId'), index('subscriptionId'), index('pendingPlan')])
.authorization((allow) => [
allow.ownerDefinedIn('userId').to(['read', 'update', 'delete', 'create']),
allow.authenticated().to(['create']),
allow.publicApiKey().to(['read', 'create', 'update', 'delete']),
]);
102 changes: 83 additions & 19 deletions amplify/functions/createSubscription/handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,38 @@ export const handler: APIGatewayProxyHandler = async (event) => {
// Inicializar el cliente de Polar
const polar = new Polar({
accessToken: env.POLAR_ACCESS_TOKEN,
server: process.env.NODE_ENV === 'production' ? 'production' : 'sandbox',
});

// Validación temprana del plan y producto
if (!plan || !plan.polarId || typeof plan.polarId !== 'string') {
return {
statusCode: 400,
headers: getCorsHeaders(origin),
body: JSON.stringify({
error: 'Invalid request',
details: 'plan.polarId is required',
}),
};
}

// Verificar que el producto exista en el entorno actual (sandbox/production)
try {
await polar.products.get({ id: plan.polarId });
} catch (productErr) {
return {
statusCode: 400,
headers: getCorsHeaders(origin),
body: JSON.stringify({
error: 'Invalid product',
details:
`The product ${plan.polarId} does not exist in the environment ` +
(process.env.NODE_ENV === 'production' ? 'production' : 'sandbox') +
'. Verify that the ID corresponds to the correct environment.',
}),
};
}

// Buscar o crear el cliente en Polar
let customer;
let checkoutUrl = '';
Expand Down Expand Up @@ -65,27 +95,61 @@ export const handler: APIGatewayProxyHandler = async (event) => {
}
}
} catch (error) {
// Si no existe, crear un nuevo cliente
customer = await polar.customers.create({
email: email,
name: name,
externalId: userId,
billingAddress: {
country: 'CO',
},
});
// Si no existe, intentar crear un nuevo cliente
try {
customer = await polar.customers.create({
email: email,
name: name,
externalId: userId,
billingAddress: {
country: 'CO',
},
});

// Crear checkout para la suscripción
const checkout = await polar.checkouts.create({
customerBillingAddress: {
country: 'CO',
},
customerId: customer.id,
successUrl: 'https://fasttify.com/first-steps',
products: [plan.polarId],
});
// Crear checkout para la suscripción
const checkout = await polar.checkouts.create({
customerBillingAddress: {
country: 'CO',
},
customerId: customer.id,
successUrl: 'https://fasttify.com/first-steps',
products: [plan.polarId],
});

checkoutUrl = checkout.url;
} catch (createErr) {
const message = createErr instanceof Error ? createErr.message : String(createErr);
const isDuplicate =
message.includes('already exists') ||
message.includes('value_error') ||
message.includes('PolarRequestValidationError');

if (!isDuplicate) {
throw createErr;
}

// Si ya existe (email/externalId duplicados), recuperar el cliente existente
customer = await polar.customers.getExternal({
externalId: userId,
});

// Igual que en el caso de cliente existente: verificar suscripción activa
const customerState = await polar.customers.getState({ id: customer.id });
const hasActiveSubscription = customerState.activeSubscriptions && customerState.activeSubscriptions.length > 0;

checkoutUrl = checkout.url;
if (!hasActiveSubscription) {
const customerCheckout = await polar.checkouts.create({
customerBillingAddress: { country: 'CO' },
customerId: customer.id,
products: [plan.polarId],
successUrl: 'https://fasttify.com/first-steps',
});
checkoutUrl = customerCheckout.url;
} else {
const result = await polar.customerSessions.create({ customerId: customer.id });
checkoutUrl = result.customerPortalUrl;
}
}
}

// Siempre devolver código 200 con la URL correspondiente
Expand Down
1 change: 1 addition & 0 deletions amplify/functions/webHookPlan/services/polar-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export class PolarApiService {
constructor(accessToken: string) {
this.polar = new Polar({
accessToken,
server: process.env.NODE_ENV === 'production' ? 'production' : 'sandbox',
});
}

Expand Down
25 changes: 19 additions & 6 deletions amplify/functions/webHookPlan/services/polar-payment-processor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,13 +53,26 @@ export class PolarPaymentProcessor implements PaymentProcessor {
* Determina el nombre del plan basado en el ID del producto
*/
private getPlanFromProductId(productId: string): string {
// Mapeo de product_id a nombres de planes
// Esto debe configurarse según tus productos en Polar
// IDs por entorno
const isProd = process.env && process.env.NODE_ENV === 'production';

const DEV_ROYAL_ID = 'd889915d-bb1a-4c54-badd-de697857e624';
const DEV_MAJESTIC_ID = '442aacda-1fa3-47cd-8fba-6ad028285218';
const DEV_IMPERIAL_ID = '21e675ee-db9d-4cd7-9902-0fead14a85f5';

const PROD_ROYAL_ID = 'e02f173f-1ca5-4f7b-a900-2e5c9413d8a6';
const PROD_MAJESTIC_ID = '149c6595-1611-477d-b0b4-61700d33c069';
const PROD_IMPERIAL_ID = '3a85e94a-7deb-4f94-8aa4-99a972406f0f';

const ROYAL_ID = isProd ? PROD_ROYAL_ID : DEV_ROYAL_ID;
const MAJESTIC_ID = isProd ? PROD_MAJESTIC_ID : DEV_MAJESTIC_ID;
const IMPERIAL_ID = isProd ? PROD_IMPERIAL_ID : DEV_IMPERIAL_ID;

// Mapeo de product_id a nombres de planes según entorno
const productMap: Record<string, string> = {
'e02f173f-1ca5-4f7b-a900-2e5c9413d8a6': 'Royal',
'149c6595-1611-477d-b0b4-61700d33c069': 'Majestic',
'3a85e94a-7deb-4f94-8aa4-99a972406f0f': 'Imperial',
// Añadir más mapeos según sea necesario
[ROYAL_ID]: 'Royal',
[MAJESTIC_ID]: 'Majestic',
[IMPERIAL_ID]: 'Imperial',
};

return productMap[productId];
Expand Down
4 changes: 1 addition & 3 deletions amplify/functions/webHookPlan/services/user-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -169,8 +169,6 @@ export class CognitoUserService implements UserService {
* Calculates pending plan based on remaining subscription time
*/
private calculatePendingPlan(subscription: any): string {
// Add your logic here to calculate pending plan
// based on subscription data and remaining time
return 'free'; // Simplified for now
}

Expand Down Expand Up @@ -201,7 +199,7 @@ export class CognitoUserService implements UserService {
planName: subscriptionData.planName,
nextPaymentDate: subscriptionData.nextPaymentDate,
lastFourDigits: subscriptionData.lastFourDigits,
pendingPlan: null, // Clear pending plan on successful payment
pendingPlan: null,
};

if (existingSubscription) {
Expand Down
71 changes: 68 additions & 3 deletions app/(www)/pricing/components/plans.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,21 @@
const isProd = typeof process !== 'undefined' && process.env && process.env.NODE_ENV === 'production';

// IDs por entorno
const DEV_ROYAL_ID = 'd889915d-bb1a-4c54-badd-de697857e624';
const DEV_MAJESTIC_ID = '442aacda-1fa3-47cd-8fba-6ad028285218';
const DEV_IMPERIAL_ID = '21e675ee-db9d-4cd7-9902-0fead14a85f5';

const PROD_ROYAL_ID = 'e02f173f-1ca5-4f7b-a900-2e5c9413d8a6';
const PROD_MAJESTIC_ID = '149c6595-1611-477d-b0b4-61700d33c069';
const PROD_IMPERIAL_ID = '3a85e94a-7deb-4f94-8aa4-99a972406f0f';

const ROYAL_ID = isProd ? PROD_ROYAL_ID : DEV_ROYAL_ID;
const MAJESTIC_ID = isProd ? PROD_MAJESTIC_ID : DEV_MAJESTIC_ID;
const IMPERIAL_ID = isProd ? PROD_IMPERIAL_ID : DEV_IMPERIAL_ID;

export const plans = [
{
polarId: 'e02f173f-1ca5-4f7b-a900-2e5c9413d8a6',
polarId: ROYAL_ID,
name: 'Royal',
title: '$55.000 COP/mes',
price: '55000',
Expand All @@ -18,7 +33,7 @@ export const plans = [
className: 'bg-white',
},
{
polarId: '149c6595-1611-477d-b0b4-61700d33c069',
polarId: MAJESTIC_ID,
name: 'Majestic',
title: '$75.000 COP/mes',
price: '75000',
Expand All @@ -37,7 +52,7 @@ export const plans = [
popular: true,
},
{
polarId: '3a85e94a-7deb-4f94-8aa4-99a972406f0f',
polarId: IMPERIAL_ID,
name: 'Imperial',
title: '$100.000 COP/mes',
price: '100000',
Expand All @@ -56,3 +71,53 @@ export const plans = [
className: 'bg-white',
},
];

export const planFaqs = [
{
question: '¿Qué es Fasttify y cómo funciona?',
paragraphs: [
'Fasttify es una plataforma integral de comercio para que emprendedores y empresas inicien, administren y hagan crecer su negocio en línea, en tienda física y en cualquier canal digital.',
'Estas son algunas de las cosas que puedes hacer con Fasttify:',
],
bullets: [
'Crear y personalizar una tienda online.',
'Vender en web, móvil, redes sociales y tienda física.',
'Gestionar productos, inventario, pagos y envíos.',
'Crear, ejecutar y analizar campañas de marketing.',
],
},
{
question: '¿Cuánto cuesta Fasttify?',
paragraphs: [
'Ofrecemos planes desde $55.000 COP/mes. Puedes elegir el plan que mejor se adapte a tu negocio y cambiar cuando lo necesites.',
],
},
{
question: '¿Cuál es la duración de los contratos?',
paragraphs: ['La suscripción es mensual y flexible; no hay contratos a largo plazo.'],
},
{
question: '¿Puedo cancelar mi cuenta en cualquier momento?',
paragraphs: ['Sí. Puedes cancelar o cambiar de plan cuando quieras desde la configuración de tu tienda.'],
},
{
question: '¿Puedo cambiar mi plan más adelante?',
paragraphs: ['Sí. Puedes escalar o reducir tu plan en cualquier momento según tus necesidades.'],
},
{
question: '¿Ofrecen descuentos?',
paragraphs: ['Periódicamente ofrecemos promociones. Si necesitas facturación anual o por volumen, contáctanos.'],
},
{
question: '¿En qué países puedo usar Fasttify?',
paragraphs: [
'Fasttify funciona en la mayoría de países. La disponibilidad de métodos de pago puede variar por región.',
],
},
{
question: '¿Fasttify es compatible con PCI o está certificado PCI?',
paragraphs: [
'Sí. Cumplimos con estándares de seguridad y buenas prácticas para el manejo de pagos y datos sensibles.',
],
},
];
Loading