Skip to content

Commit a4cd5e2

Browse files
authored
Merge pull request #699 from Merit-Systems/master
Merge Referral Fixes
2 parents 3573142 + 5af2387 commit a4cd5e2

11 files changed

Lines changed: 526 additions & 6 deletions

File tree

packages/app/control/src/app/(app)/app/[id]/(overview)/_components/header/buttons/index.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,11 @@ interface Props {
1212
}
1313

1414
export const HeaderButtons: React.FC<Props> = ({ appId }) => {
15-
const [isOwner] = api.apps.app.isOwner.useSuspenseQuery(appId);
15+
const { data: isOwner, isLoading } = api.apps.app.isOwner.useQuery(appId);
16+
17+
if (isLoading || isOwner === undefined) {
18+
return <LoadingHeaderButtons />;
19+
}
1620

1721
return (
1822
<div className="flex items-center gap-2">
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
'use client';
2+
3+
import { useEffect, useState } from 'react';
4+
import { useSearchParams } from 'next/navigation';
5+
import { api } from '@/trpc/client';
6+
7+
interface Props {
8+
appId: string;
9+
}
10+
11+
export const ReferralHandler: React.FC<Props> = ({ appId }) => {
12+
const searchParams = useSearchParams();
13+
const referralCode = searchParams.get('referral_code');
14+
const [processed, setProcessed] = useState(false);
15+
16+
const { mutateAsync: registerReferral } =
17+
api.apps.app.registerReferral.useMutation();
18+
19+
useEffect(() => {
20+
if (!referralCode || processed) return;
21+
22+
const processReferralCode = async () => {
23+
await registerReferral({
24+
appId,
25+
code: referralCode,
26+
}).catch(() => {
27+
// Silently fail - referral code may be invalid, expired, or user may already have a referrer
28+
});
29+
30+
setProcessed(true);
31+
};
32+
33+
void processReferralCode();
34+
}, [referralCode, appId, registerReferral, processed]);
35+
36+
return null;
37+
};

packages/app/control/src/app/(app)/app/[id]/(overview)/page.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { api, HydrateClient } from '@/trpc/server';
99
import { HeaderCard, LoadingHeaderCard } from './_components/header';
1010
import { Setup } from './_components/setup';
1111
import { Overview } from './_components/overview';
12+
import { ReferralHandler } from './_components/referral-handler';
1213
import { userOrRedirect } from '@/auth/user-or-redirect';
1314

1415
export default async function AppPage(props: PageProps<'/app/[id]'>) {
@@ -26,6 +27,7 @@ export default async function AppPage(props: PageProps<'/app/[id]'>) {
2627

2728
return (
2829
<HydrateClient>
30+
<ReferralHandler appId={id} />
2931
<Body className="gap-0 pt-0">
3032
<Suspense fallback={<LoadingHeaderCard />}>
3133
<HeaderCard appId={id} />

packages/app/control/src/app/api/v1/user/referral/route.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,54 @@ import { z } from 'zod';
33
import { appIdSchema } from '@/services/db/apps/lib/schemas';
44
import { authRoute } from '../../../../../lib/api/auth-route';
55
import { setAppMembershipReferrer } from '@/services/db/apps/membership';
6+
import {
7+
getUserAppReferralCode,
8+
createAppReferralCode,
9+
} from '@/services/db/apps/referral-code';
10+
11+
const getUserReferralCodeSchema = z.object({
12+
echoAppId: appIdSchema,
13+
});
614

715
const setUserReferrerForAppSchema = z.object({
816
echoAppId: appIdSchema,
917
code: z.string(),
1018
});
1119

20+
export const GET = authRoute
21+
.query(getUserReferralCodeSchema)
22+
.handler(async (_, context) => {
23+
const { echoAppId } = context.query;
24+
const userId = context.ctx.userId;
25+
26+
let referralCode = await getUserAppReferralCode(userId, echoAppId);
27+
28+
if (!referralCode) {
29+
referralCode = await createAppReferralCode(userId, {
30+
appId: echoAppId,
31+
expiresAt: new Date(Date.now() + 365 * 24 * 60 * 60 * 1000),
32+
});
33+
34+
if (!referralCode) {
35+
return NextResponse.json(
36+
{
37+
success: false,
38+
message: 'Failed to create referral code',
39+
},
40+
{ status: 500 }
41+
);
42+
}
43+
}
44+
45+
return NextResponse.json({
46+
success: true,
47+
message: 'Referral code retrieved successfully',
48+
code: referralCode.code,
49+
referralLinkUrl: referralCode.referralLinkUrl,
50+
expiresAt: referralCode.expiresAt,
51+
});
52+
});
53+
1254
export const POST = authRoute
1355
.body(setUserReferrerForAppSchema)
1456
.handler(async (_, context) => {

packages/app/control/src/services/db/apps/membership.ts

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -163,21 +163,30 @@ export async function setAppMembershipReferrer(
163163
echoAppId: string,
164164
code: string
165165
): Promise<boolean> {
166-
const appMembership = await db.appMembership.findUnique({
166+
// Get or create the app membership
167+
const appMembership = await db.appMembership.upsert({
167168
where: {
168169
userId_echoAppId: {
169170
userId,
170171
echoAppId,
171172
},
172-
referrerId: null,
173173
},
174+
create: {
175+
userId,
176+
echoAppId,
177+
role: AppRole.CUSTOMER,
178+
status: MembershipStatus.ACTIVE,
179+
totalSpent: 0,
180+
},
181+
update: {},
174182
});
175183

176-
if (appMembership) {
177-
// If the user already has a referrer, return false
184+
// Check if user already has a referrer
185+
if (appMembership.referrerId) {
178186
return false;
179187
}
180188

189+
// Validate the referral code exists
181190
const referralCode = await db.referralCode.findUnique({
182191
where: {
183192
code,

packages/app/control/src/trpc/routers/apps/index.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import {
2323
createAppMembershipSchema,
2424
updateAppMembershipReferrer,
2525
updateAppMembershipReferrerSchema,
26+
setAppMembershipReferrer,
2627
} from '@/services/db/apps/membership';
2728
import {
2829
listAppsSchema,
@@ -262,6 +263,26 @@ export const appsRouter = createTRPCRouter({
262263
}),
263264
},
264265

266+
registerReferral: protectedProcedure
267+
.input(z.object({ appId: appIdSchema, code: z.string() }))
268+
.mutation(async ({ input, ctx }) => {
269+
const success = await setAppMembershipReferrer(
270+
ctx.session.user.id,
271+
input.appId,
272+
input.code
273+
);
274+
275+
if (!success) {
276+
throw new TRPCError({
277+
code: 'BAD_REQUEST',
278+
message:
279+
'Referral code could not be applied. It may be invalid, expired, or you may already have a referrer for this app.',
280+
});
281+
}
282+
283+
return { success: true };
284+
}),
285+
265286
transactions: {
266287
list: paginatedProcedure
267288
.concat(protectedProcedure)
Lines changed: 82 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,82 @@
1-
# Integration test trigger
1+
# Integration Tests
2+
3+
This package contains integration tests for the Echo platform.
4+
5+
## Setup
6+
7+
1. Set up the test environment:
8+
```bash
9+
pnpm env:setup
10+
```
11+
12+
2. Seed the test database:
13+
```bash
14+
pnpm db:seed
15+
```
16+
17+
## Running Tests
18+
19+
Run all integration tests:
20+
```bash
21+
pnpm test:watch
22+
```
23+
24+
Run specific test suites:
25+
```bash
26+
# Echo Data Server tests
27+
pnpm test:echo-data-server
28+
29+
# OAuth Protocol tests
30+
pnpm test:oauth-protocol
31+
```
32+
33+
## Test Suites
34+
35+
### Echo Data Server Tests
36+
Located in `tests/echo-data-server/`:
37+
- `api-key.client.test.ts` - API key authentication and usage
38+
- `402-auth.client.test.ts` - Payment required (402) authentication flow
39+
- `echo-access-jwt.client.test.ts` - JWT token validation
40+
- `free-tier.client.test.ts` - Free tier functionality
41+
- `referral-code.client.test.ts` - Referral code creation and application
42+
- `in-flight-requests.test.ts` - Concurrent request handling
43+
44+
### OAuth Protocol Tests
45+
Located in `tests/oauth-protocol/`:
46+
- OAuth authorization flow
47+
- Token refresh and lifecycle
48+
- PKCE security
49+
- CSRF vulnerability testing
50+
51+
## Referral Code Tests
52+
53+
The referral code integration tests (`referral-code.client.test.ts`) cover:
54+
55+
1. **GET endpoint** - Retrieval of referral codes:
56+
- Retrieving an existing referral code for a user
57+
- Auto-creating a referral code for users who don't have one
58+
- Ensuring consistency across multiple requests
59+
60+
2. **POST endpoint** - Application of referral codes:
61+
- Successfully applying another user's referral code
62+
- Rejecting invalid referral codes
63+
- Preventing users from applying codes when they already have a referrer
64+
65+
### Test Data
66+
67+
Referral code test data is defined in `config/test-data.ts`:
68+
- Primary user has referral code: `TEST-REFERRAL-CODE-PRIMARY`
69+
- Secondary user has referral code: `TEST-REFERRAL-CODE-SECONDARY`
70+
- Tertiary user has no referral code (created during tests)
71+
72+
## Database Management
73+
74+
Reset and reseed the database:
75+
```bash
76+
pnpm db:reset-and-seed
77+
```
78+
79+
Reset only:
80+
```bash
81+
pnpm db:reset
82+
```

packages/tests/integration/config/test-data.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,26 @@ export const TEST_DATA = {
131131
},
132132
},
133133

134+
// Referral code configurations
135+
referralCodes: {
136+
primaryUserCode: {
137+
id: '88888888-8888-4888-8888-888888888888',
138+
code: 'TEST-REFERRAL-CODE-PRIMARY',
139+
userId: '11111111-1111-4111-8111-111111111111', // Primary test user
140+
expiresAt: new Date(Date.now() + 365 * 24 * 60 * 60 * 1000), // 1 year from now
141+
isArchived: false,
142+
usedAt: null,
143+
},
144+
secondaryUserCode: {
145+
id: 'aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa',
146+
code: 'TEST-REFERRAL-CODE-SECONDARY',
147+
userId: '33333333-3333-4333-8333-333333333333', // Secondary test user
148+
expiresAt: new Date(Date.now() + 365 * 24 * 60 * 60 * 1000), // 1 year from now
149+
isArchived: false,
150+
usedAt: null,
151+
},
152+
},
153+
134154
// Test timeouts and delays
135155
timeouts: {
136156
default: 30000,
@@ -234,6 +254,16 @@ export const TEST_SPEND_POOL_IDS = {
234254
primary: TEST_DATA.spendPools.primary.id,
235255
};
236256

257+
export const TEST_REFERRAL_CODE_IDS = {
258+
primary: TEST_DATA.referralCodes.primaryUserCode.id,
259+
secondary: TEST_DATA.referralCodes.secondaryUserCode.id,
260+
};
261+
262+
export const TEST_REFERRAL_CODES = {
263+
primary: TEST_DATA.referralCodes.primaryUserCode.code,
264+
secondary: TEST_DATA.referralCodes.secondaryUserCode.code,
265+
};
266+
237267
// Type definitions for test data
238268
export type TestData = typeof TEST_DATA;
239269
export type TestUser = typeof TEST_DATA.users.primary;
@@ -242,3 +272,4 @@ export type TestApiKey = typeof TEST_DATA.apiKeys.primary;
242272
export type TestSpendPool = typeof TEST_DATA.spendPools.primary;
243273
export type TestUserSpendPoolUsage =
244274
typeof TEST_DATA.userSpendPoolUsage.tertiaryUserPrimaryPool;
275+
export type TestReferralCode = typeof TEST_DATA.referralCodes.primaryUserCode;

packages/tests/integration/scripts/seed-integration-db.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ export async function seedIntegrationDatabase() {
2525
await prisma.spendPool.deleteMany();
2626
await prisma.apiKey.deleteMany();
2727
await prisma.appMembership.deleteMany();
28+
await prisma.referralCode.deleteMany();
2829
await prisma.echoApp.deleteMany();
2930
await prisma.user.deleteMany();
3031

@@ -171,6 +172,17 @@ export async function seedIntegrationDatabase() {
171172

172173
console.log('🤖 Created test LLM transaction');
173174

175+
// Create test referral codes
176+
await prisma.referralCode.create({
177+
data: TEST_DATA.referralCodes.primaryUserCode,
178+
});
179+
180+
await prisma.referralCode.create({
181+
data: TEST_DATA.referralCodes.secondaryUserCode,
182+
});
183+
184+
console.log('🎟️ Created test referral codes');
185+
174186
console.log('✅ Integration test database seeded successfully');
175187
console.log('\n📊 Summary:');
176188
console.log(` - Users: 3`);
@@ -181,6 +193,7 @@ export async function seedIntegrationDatabase() {
181193
console.log(` - User Spend Pool Usage: 1`);
182194
console.log(` - Payments: 1`);
183195
console.log(` - LLM Transactions: 1`);
196+
console.log(` - Referral Codes: 2`);
184197
} catch (error) {
185198
console.error('❌ Error seeding integration test database:', error);
186199
throw error;

0 commit comments

Comments
 (0)