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
44 changes: 44 additions & 0 deletions src/services/api/verbooCheckout.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
confirmCardlessTrial,
createCheckoutSession,
getWhatsAppProfile,
hasGroupSubscriptionEntitlement,
isWooviSubscriptionActive,
startCardlessTrial,
} from './verbooCheckout.js'
Expand All @@ -15,6 +16,7 @@ const GROUP_ID = '11111111-1111-4111-8111-111111111111'
const OTHER_GROUP_ID = '22222222-2222-4222-8222-222222222222'
const VERIFICATION_ID = '33333333-3333-4333-8333-333333333333'
const SUBSCRIPTION_ID = '44444444-4444-4444-8444-444444444444'
const ATTEMPT_ID = '55555555-5555-4555-8555-555555555555'

afterEach(() => {
axios.post = originalPost
Expand All @@ -26,6 +28,7 @@ test('sends the explicit Woovi method and payer data to checkout', async () => {
data: {
data: {
mode: 'woovi' as const,
attemptId: ATTEMPT_ID,
wooviQrCode: '000201',
wooviSubscriptionId: 'woovi-subscription',
},
Expand All @@ -40,6 +43,7 @@ test('sends the explicit Woovi method and payer data to checkout', async () => {
}),
).resolves.toEqual({
mode: 'woovi',
attemptId: ATTEMPT_ID,
wooviQrCode: '000201',
wooviSubscriptionId: 'woovi-subscription',
})
Expand All @@ -58,6 +62,46 @@ test('sends the explicit Woovi method and payer data to checkout', async () => {
)
})

test('paid entitlement never treats an active trial as a completed purchase', () => {
const trial = {
id: SUBSCRIPTION_ID,
groupId: GROUP_ID,
source: 'stripe_trial',
status: 'trialing',
cancelAtPeriodEnd: false,
}
expect(hasGroupSubscriptionEntitlement([trial], GROUP_ID, 'access')).toBe(true)
expect(hasGroupSubscriptionEntitlement([trial], GROUP_ID, 'paid')).toBe(false)
expect(
hasGroupSubscriptionEntitlement(
[{ ...trial, source: 'stripe', status: 'active' }],
GROUP_ID,
'paid',
),
).toBe(true)
expect(
hasGroupSubscriptionEntitlement(
[{ ...trial, source: 'stripe_trial', status: 'active' }],
GROUP_ID,
'paid',
),
).toBe(true)
expect(
hasGroupSubscriptionEntitlement(
[{ ...trial, source: 'woovi', status: 'active' }],
GROUP_ID,
'paid',
),
).toBe(true)
expect(
hasGroupSubscriptionEntitlement(
[{ ...trial, source: 'manual', status: 'active' }],
GROUP_ID,
'paid',
),
).toBe(false)
})

test('confirms only the Woovi subscription that became active', async () => {
const get = mock(async () => ({
data: {
Expand Down
42 changes: 36 additions & 6 deletions src/services/api/verbooCheckout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,17 @@ const httpUrlSchema = z
)

const checkoutResultSchema = z.discriminatedUnion('mode', [
z.object({ mode: z.literal('stripe'), url: httpUrlSchema }).passthrough(),
z
.object({
mode: z.literal('stripe'),
attemptId: z.string().uuid(),
url: httpUrlSchema,
})
.passthrough(),
z
.object({
mode: z.literal('woovi'),
attemptId: z.string().uuid(),
wooviQrCode: z.string().min(1),
wooviSubscriptionId: z.string().min(1),
})
Expand Down Expand Up @@ -120,6 +127,7 @@ export type CheckoutInput = {
export type WhatsAppProfile = z.infer<typeof whatsappProfileSchema>
export type CardlessTrialInput = z.input<typeof cardlessTrialInputSchema>
export type CardlessTrialResult = z.infer<typeof cardlessTrialResultSchema>
export type GroupEntitlementRequirement = 'access' | 'paid'

function authHeaders(accessToken: string): Record<string, string> {
return {
Expand Down Expand Up @@ -258,12 +266,34 @@ export async function resendCardlessTrialCode(
export async function isGroupSubscriptionActive(
accessToken: string,
groupId: string,
opts: { signal?: AbortSignal } = {},
opts: {
signal?: AbortSignal
requirement?: GroupEntitlementRequirement
} = {},
): Promise<boolean> {
const subscriptions = await fetchSubscriptions(accessToken, opts)
return subscriptions.some(
(sub) =>
sub.groupId === groupId &&
(sub.status === 'active' || sub.status === 'trialing'),
return hasGroupSubscriptionEntitlement(
subscriptions,
groupId,
opts.requirement ?? 'access',
)
}

export function hasGroupSubscriptionEntitlement(
subscriptions: Awaited<ReturnType<typeof fetchSubscriptions>>,
groupId: string,
requirement: GroupEntitlementRequirement,
): boolean {
return subscriptions.some((subscription) => {
if (subscription.groupId !== groupId) return false
if (requirement === 'access') {
return subscription.status === 'active' || subscription.status === 'trialing'
}
return (
subscription.status === 'active' &&
(subscription.source === 'stripe' ||
subscription.source === 'stripe_trial' ||
subscription.source === 'woovi')
)
})
}
24 changes: 24 additions & 0 deletions src/services/oauth/purchaseFlow.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
filterCliPurchasablePlans,
getPlanColumnCount,
getPlanDetailOptions,
getStripeTrialConversionUrl,
movePlanFocus,
} from './purchaseFlow.js'
import { isValidCPF, onlyDigits } from './purchaseValidation.js'
Expand Down Expand Up @@ -85,6 +86,18 @@ test('offers both free trial and immediate paid purchase when eligible', () => {
])
})

test('never advertises a trial for an annual offer', () => {
const annual = plan({
billingInterval: 'year',
trialDays: 7,
trialEligible: true,
})
expect(getPlanDetailOptions(annual).map((option) => option.value)).toEqual([
'buy',
'back',
])
})

test('keeps an active local trial available for paid conversion even when full', () => {
const trialPlan = plan({
isMember: true,
Expand All @@ -99,6 +112,17 @@ test('keeps an active local trial available for paid conversion even when full',
).toEqual([trialPlan])
})

test('routes a Stripe trial to the dedicated full-price conversion journey', () => {
expect(
getStripeTrialConversionUrl(
'22222222-2222-4222-8222-222222222222',
'year',
),
).toBe(
'https://code.verboo.ai/pt/settings/billing?subscription=22222222-2222-4222-8222-222222222222&action=convert&billingInterval=year',
)
})

test('keeps legacy local trials available for paid conversion', () => {
const trialPlan = plan({ isMember: true })
expect(
Expand Down
Loading
Loading