Hard paywall
This commit is contained in:
@@ -1,17 +1,18 @@
|
||||
const { get, run } = require('./postgres');
|
||||
|
||||
const FREE_MONTHLY_CREDITS = 15;
|
||||
const PRO_MONTHLY_CREDITS = 250;
|
||||
const TOPUP_DEFAULT_CREDITS = 60;
|
||||
const FREE_MONTHLY_CREDITS = 0;
|
||||
const TRIAL_MONTHLY_CREDITS = 30;
|
||||
const PRO_MONTHLY_CREDITS = 100;
|
||||
const TOPUP_DEFAULT_CREDITS = 100;
|
||||
const REVENUECAT_PRO_ENTITLEMENT_ID = (process.env.REVENUECAT_PRO_ENTITLEMENT_ID || 'pro').trim() || 'pro';
|
||||
const SUPPORTED_SUBSCRIPTION_PRODUCTS = new Set(['monthly_pro', 'yearly_pro']);
|
||||
|
||||
const TOPUP_CREDITS_BY_PRODUCT = {
|
||||
monthly_pro: 0,
|
||||
yearly_pro: 0,
|
||||
topup_small: 25,
|
||||
topup_medium: 120,
|
||||
topup_large: 300,
|
||||
topup_small: 30,
|
||||
topup_medium: 100,
|
||||
topup_large: 250,
|
||||
};
|
||||
|
||||
const AVAILABLE_PRODUCTS = ['monthly_pro', 'yearly_pro', 'topup_small', 'topup_medium', 'topup_large'];
|
||||
@@ -64,6 +65,22 @@ const getMonthlyAllowanceForPlan = (plan) => {
|
||||
return plan === 'pro' ? PRO_MONTHLY_CREDITS : FREE_MONTHLY_CREDITS;
|
||||
};
|
||||
|
||||
const getRevenueCatPeriodType = (source) => {
|
||||
return String(source?.periodType || source?.period_type || '').trim().toLowerCase();
|
||||
};
|
||||
|
||||
const isRevenueCatTrial = (source) => {
|
||||
return getRevenueCatPeriodType(source) === 'trial';
|
||||
};
|
||||
|
||||
const isAllowedMonthlyAllowance = (account) => {
|
||||
if (account.plan === 'pro') {
|
||||
return account.monthlyAllowance === PRO_MONTHLY_CREDITS
|
||||
|| account.monthlyAllowance === TRIAL_MONTHLY_CREDITS;
|
||||
}
|
||||
return account.monthlyAllowance === FREE_MONTHLY_CREDITS;
|
||||
};
|
||||
|
||||
const createInsufficientCreditsError = (required, available) => {
|
||||
const error = new Error(`Insufficient credits. Required ${required}, available ${available}.`);
|
||||
error.code = 'INSUFFICIENT_CREDITS';
|
||||
@@ -130,7 +147,7 @@ const buildDefaultAccount = (userId, now) => {
|
||||
const alignAccountToCurrentCycle = (account, now) => {
|
||||
const next = { ...account };
|
||||
const expectedMonthlyAllowance = getMonthlyAllowanceForPlan(next.plan);
|
||||
if (next.monthlyAllowance !== expectedMonthlyAllowance) {
|
||||
if (!isAllowedMonthlyAllowance(next)) {
|
||||
next.monthlyAllowance = expectedMonthlyAllowance;
|
||||
}
|
||||
|
||||
@@ -238,6 +255,7 @@ const getOrCreateAccount = async (db, userId) => {
|
||||
};
|
||||
|
||||
const getAvailableCredits = (account) => {
|
||||
if (account.plan !== 'pro') return 0;
|
||||
const monthlyRemaining = Math.max(0, account.monthlyAllowance - account.usedThisCycle);
|
||||
return monthlyRemaining + Math.max(0, account.topupBalance);
|
||||
};
|
||||
@@ -326,14 +344,22 @@ const getValidProEntitlement = (customerInfo) => {
|
||||
|
||||
const applyRevenueCatEntitlementState = (account, options) => {
|
||||
const now = new Date();
|
||||
const previousPlan = account.plan;
|
||||
const previousMonthlyAllowance = account.monthlyAllowance;
|
||||
const nextPlan = options.active ? 'pro' : 'free';
|
||||
const planChanged = account.plan !== nextPlan;
|
||||
const nextMonthlyAllowance = options.active && options.isTrial
|
||||
? TRIAL_MONTHLY_CREDITS
|
||||
: getMonthlyAllowanceForPlan(nextPlan);
|
||||
const planChanged = previousPlan !== nextPlan;
|
||||
const trialConvertedToPaid = previousPlan === 'pro'
|
||||
&& previousMonthlyAllowance === TRIAL_MONTHLY_CREDITS
|
||||
&& nextMonthlyAllowance === PRO_MONTHLY_CREDITS;
|
||||
account.plan = nextPlan;
|
||||
account.provider = 'revenuecat';
|
||||
account.monthlyAllowance = getMonthlyAllowanceForPlan(account.plan);
|
||||
account.monthlyAllowance = nextMonthlyAllowance;
|
||||
account.renewsAt = options.active ? options.renewsAt || account.renewsAt || addDays(now, 30).toISOString() : null;
|
||||
|
||||
if (planChanged) {
|
||||
if (planChanged || trialConvertedToPaid) {
|
||||
const { cycleStartedAt, cycleEndsAt } = getCycleBounds(now);
|
||||
account.cycleStartedAt = cycleStartedAt.toISOString();
|
||||
account.cycleEndsAt = cycleEndsAt.toISOString();
|
||||
@@ -401,6 +427,7 @@ const syncRevenueCatCustomerInfo = async (db, userId, customerInfo, options = {}
|
||||
if (source !== 'topup_purchase') {
|
||||
applyRevenueCatEntitlementState(account, {
|
||||
active: Boolean(proEntitlement),
|
||||
isTrial: isRevenueCatTrial(proEntitlement),
|
||||
renewsAt: asIsoDate(proEntitlement?.expirationDate || proEntitlement?.expiresDate),
|
||||
});
|
||||
}
|
||||
@@ -490,6 +517,7 @@ const syncRevenueCatWebhookEvent = async (db, eventPayload) => {
|
||||
if (affectsProEntitlement && shouldGrantRevenueCatSubscription(eventType)) {
|
||||
applyRevenueCatEntitlementState(account, {
|
||||
active: true,
|
||||
isTrial: isRevenueCatTrial(eventPayload),
|
||||
renewsAt: asIsoDate(eventPayload?.expiration_at_ms || eventPayload?.expiration_at),
|
||||
});
|
||||
} else if (affectsProEntitlement && shouldRevokeRevenueCatSubscription(eventType)) {
|
||||
@@ -519,6 +547,9 @@ const syncRevenueCatWebhookEvent = async (db, eventPayload) => {
|
||||
|
||||
const consumeCredits = (account, cost) => {
|
||||
if (cost <= 0) return 0;
|
||||
if (account.plan !== 'pro') {
|
||||
throw createInsufficientCreditsError(cost, 0);
|
||||
}
|
||||
|
||||
const available = getAvailableCredits(account);
|
||||
if (available < cost) {
|
||||
@@ -567,12 +598,12 @@ const consumeCreditsWithIdempotency = async (db, userId, key, cost) => {
|
||||
const getBillingSummary = async (db, userId) => {
|
||||
if (userId === 'guest') {
|
||||
return {
|
||||
entitlement: { plan: 'free', provider: 'mock', status: 'active', renewsAt: null },
|
||||
entitlement: { plan: 'free', provider: 'mock', status: 'inactive', renewsAt: null },
|
||||
credits: {
|
||||
monthlyAllowance: 5,
|
||||
monthlyAllowance: 0,
|
||||
usedThisCycle: 0,
|
||||
topupBalance: 0,
|
||||
available: 5,
|
||||
available: 0,
|
||||
cycleStartedAt: nowIso(),
|
||||
cycleEndsAt: nowIso(),
|
||||
},
|
||||
@@ -595,7 +626,7 @@ const getAccountSnapshot = async (db, userId) => {
|
||||
provider: 'mock',
|
||||
cycleStartedAt: nowIso(),
|
||||
cycleEndsAt: nowIso(),
|
||||
monthlyAllowance: 5,
|
||||
monthlyAllowance: 0,
|
||||
usedThisCycle: 0,
|
||||
topupBalance: 0,
|
||||
renewsAt: null,
|
||||
|
||||
Reference in New Issue
Block a user