Skip to content

Commit bcb4331

Browse files
[feat] referrals system (#17)
1 parent c1eb56b commit bcb4331

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

49 files changed

+2064
-249
lines changed

.env.example

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,16 @@ NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY='your publishable key'
1818
DATABASE_URL='postgresql://postgres:mysecretpassword@localhost:5432/postgres'
1919

2020
# Web
21-
APP_URL='https://localhost:3000'
21+
NEXT_PUBLIC_APP_URL='https://localhost:3000'
2222
GOOGLE_SITE_VERIFICATION_ID='your google verification id'
2323
GITHUB_ID='your github client ID'
2424
GITHUB_SECRET='your github secret ID'
2525
NEXTAUTH_SECRET='your next-auth secret'
2626
NEXTAUTH_URL='http://localhost:3000'
2727

28+
# All
29+
NEXT_PUBLIC_SUPPORT_EMAIL='support@manicode.ai'
30+
2831
#################################################################
2932
# PLEASE SEE JAMES OR BRANDON FOR THE REAL ENVIRONMENT VARIABLES!
3033
#################################################################

authentication.knowledge.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ Manicode implements a secure authentication flow that involves the npm-app (CLI)
1616
a. `fingerprintId` (to link the current session with user credentials)
1717
b. Timestamp (5 minutes in the future, for link expiration)
1818
c. Hash of the above + a secret value (for request verification)
19-
- Backend appends this auth code to the login URL: `${APP_URL}/login?auth_code=<token-goes-here>`
19+
- Backend appends this auth code to the login URL: `${NEXT_PUBLIC_APP_URL}/login?auth_code=<token-goes-here>`
2020

2121
3. User Login:
2222

backend/knowledge.md

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,31 @@ The backend handles file operations for the Manicode project:
7070
- **Reading Files**: The `read_files` tool allows the AI to access project file contents.
7171
- **Applying Changes**: The `applyChanges` function in `prompts.ts` processes and applies file modifications suggested by the AI.
7272

73+
## Code Organization and Best Practices
74+
75+
1. **Centralize Shared Logic**: When implementing functionality that's used in multiple places (e.g., web API and backend), create shared functions to promote code reuse and consistency. This is particularly important for business logic like referral handling and usage calculations.
76+
77+
2. **Shared Function Location**: Place shared functions in a common directory accessible to both the web API and backend. Consider creating a `common/src/utils` directory for such shared functionality.
78+
79+
3. **DRY Principle**: Always look for opportunities to refactor repeated code into shared, reusable functions. This not only reduces code duplication but also makes maintenance and updates easier.
80+
81+
4. **Consistent API**: When creating shared functions, ensure they have a consistent API that can be easily used by different parts of the application.
82+
83+
5. **Testing Shared Functions**: Implement unit tests for shared functions to ensure they work correctly in all contexts where they are used.
84+
85+
6. **Documentation**: Document shared functions clearly, including their purpose, inputs, outputs, and any side effects, so that other developers can use them effectively.
86+
87+
7. **Version Control**: When making changes to shared functions, consider the impact on all parts of the application that use them, and test thoroughly.
88+
89+
8. **Dependency Injection**: Prefer pulling common dependencies (like database connections or environment variables) from centralized locations rather than passing them as parameters. This reduces function complexity and improves maintainability.
90+
91+
9. **Single Responsibility Principle**: Design functions to have a single, well-defined purpose. For example, separate the logic of determining eligibility for a referral code from the generation of the full referral link.
92+
93+
10. **Abstraction Refinement**: Be prepared to refine initial implementations as the system's needs become clearer. This might involve changing function signatures, splitting functions, or adjusting their purposes to better fit the overall architecture.
94+
95+
96+
97+
7398
## Development Guidelines
7499

75100
1. **Type Safety**: Utilize TypeScript's type system to ensure code reliability and catch errors early.
@@ -145,6 +170,29 @@ export const logger = pino({
145170
4. Create a robust testing suite for backend components.
146171
5. Optimize the file diff generation process for better reliability and performance.
147172

173+
## Referral System
174+
175+
The referral system is an important feature of our application. Here are key points to remember:
176+
177+
1. **Referral Limit**: Users are limited to a maximum number of successful referrals (currently set to 5).
178+
179+
2. **Limit Enforcement**: The referral limit must be enforced during the redemption process (POST request), not just when displaying referral information (GET request).
180+
181+
3. **Centralized Logic**: The `hasMaxedReferrals` function in `common/src/util/referral.ts` is used to check if a user has reached their referral limit. This function should be used consistently across the application to ensure uniform enforcement of the referral limit.
182+
183+
4. **Redemption Process**: When redeeming a referral code (in the POST request handler), always check if the referrer has maxed out their referrals before processing the redemption. This ensures that users cannot exceed their referral limit even if they distribute their referral code widely.
184+
185+
5. **Error Handling**: Provide clear error messages when a referral code cannot be redeemed due to the referrer reaching their limit. This helps maintain a good user experience.
186+
187+
Remember to keep the referral system logic consistent between the backend API and the websocket server to ensure uniform behavior across different parts of the application.
188+
189+
190+
191+
192+
193+
194+
195+
148196
## Debugging Docker Issues
149197

150198
- When encountering "Cannot find module" errors in a Docker container, it's important to verify the contents of the container itself, not just the local build.
@@ -194,3 +242,5 @@ This project uses Bun for testing instead of Jest. When writing tests, keep the
194242
- Bun's test API is similar to Jest's, but there are some differences in implementation.
195243
- When mocking methods, use `mock(object.method)` instead of Jest's `jest.spyOn(object, 'method')`.
196244
- Bun's `mock` function expects 0-1 arguments, not 2 like Jest's `spyOn`.
245+
246+
Remember to keep this knowledge file updated as the application evolves or new features are added.

backend/src/env.mjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ export const env = createEnv({
2424
STRIPE_SUBSCRIPTION_PRICE_ID: z.string().min(1),
2525
PORT: z.coerce.number().min(1000),
2626
ENVIRONMENT: z.string().min(1),
27-
APP_URL: z.string().min(1),
27+
NEXT_PUBLIC_APP_URL: z.string().min(1),
2828
NEXTAUTH_SECRET: z.string().min(1),
2929
},
3030
runtimeEnv: process.env,

backend/src/websockets/middleware.ts

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -82,9 +82,6 @@ protec.use(async (action, _clientSessionId, _) => {
8282
logger.debug(`Protecting action of type: '${action.type}'`)
8383
})
8484
protec.use(async (action, _clientSessionId, ws) => {
85-
if (env.ENVIRONMENT === 'local') {
86-
return
87-
}
8885
return match(action)
8986
.with(
9087
{

backend/src/websockets/websocket-action.ts

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,14 @@ import { getSearchSystemPrompt } from '../system-prompt'
1111
import { promptClaude } from '../claude'
1212
import { env } from '../env.mjs'
1313
import db from 'common/db'
14-
import * as schema from 'common/db/schema'
1514
import { genAuthCode } from 'common/util/credentials'
15+
import * as schema from 'common/db/schema'
1616
import { claudeModels } from 'common/constants'
1717
import { protec } from './middleware'
1818
import { getQuotaManager } from '@/billing/quota-manager'
1919
import { logger, withLoggerContext } from '@/util/logger'
2020
import { generateCommitMessage } from '@/generate-commit-message'
21+
import { hasMaxedReferrals } from 'common/util/server/referral'
2122

2223
export const sendAction = (ws: WebSocket, action: ServerAction) => {
2324
sendMessage(ws, {
@@ -51,10 +52,7 @@ async function calculateUsage(fingerprintId: string, userId?: string) {
5152
userId ? 'authenticated' : 'anonymous',
5253
userId ?? fingerprintId
5354
)
54-
const { creditsUsed, quota } = await quotaManager.checkQuota()
55-
if (env.ENVIRONMENT === 'local') {
56-
return { usage: creditsUsed, limit: Infinity }
57-
}
55+
const { creditsUsed, quota } = await quotaManager.updateQuota()
5856
return { usage: creditsUsed, limit: quota }
5957
}
6058

@@ -180,7 +178,10 @@ const onClearAuthTokenRequest = async (
180178
}
181179

182180
const onLoginCodeRequest = (
183-
{ fingerprintId }: Extract<ClientAction, { type: 'login-code-request' }>,
181+
{
182+
fingerprintId,
183+
referralCode,
184+
}: Extract<ClientAction, { type: 'login-code-request' }>,
184185
_clientSessionId: string,
185186
ws: WebSocket
186187
): void => {
@@ -191,7 +192,7 @@ const onLoginCodeRequest = (
191192
expiresAt.toString(),
192193
env.NEXTAUTH_SECRET
193194
)
194-
const loginUrl = `${env.APP_URL}/login?auth_code=${fingerprintId}.${expiresAt}.${fingerprintHash}`
195+
const loginUrl = `${env.NEXT_PUBLIC_APP_URL}/login?auth_code=${fingerprintId}.${expiresAt}.${fingerprintHash}${referralCode ? `&referral_code=${referralCode}` : ''}`
195196

196197
sendAction(ws, {
197198
type: 'login-code-response',
@@ -323,10 +324,20 @@ const onUsageRequest = async (
323324
const { usage, limit } = await calculateUsage(fingerprintId, userId)
324325
await withLoggerContext({ fingerprintId, userId, usage, limit }, async () => {
325326
logger.info('Sending usage info')
327+
328+
let referralLink: string | undefined = undefined
329+
if (userId) {
330+
const shouldGenerateReferralLink = await hasMaxedReferrals(userId)
331+
if (shouldGenerateReferralLink.reason === undefined) {
332+
referralLink = shouldGenerateReferralLink.referralLink
333+
}
334+
}
335+
326336
sendAction(ws, {
327337
type: 'usage-response',
328338
usage,
329339
limit,
340+
referralLink,
330341
})
331342
})
332343
}
@@ -414,7 +425,7 @@ export const onWebsocketAction = async (
414425
}
415426

416427
subscribeToAction('user-input', protec.run(onUserInput))
417-
subscribeToAction('init', () => protec.run(onInit))
428+
subscribeToAction('init', protec.run(onInit))
418429

419430
subscribeToAction('clear-auth-token', onClearAuthTokenRequest)
420431
subscribeToAction('login-code-request', onLoginCodeRequest)

bun.lockb

1.3 KB
Binary file not shown.

common/src/actions.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@ export const CLIENT_ACTION_SCHEMA = z.discriminatedUnion('type', [
9191
z.object({
9292
type: z.literal('login-code-request'),
9393
fingerprintId: z.string(),
94+
referralCode: z.string().optional(),
9495
}),
9596
z.object({
9697
type: z.literal('login-status-request'),
@@ -167,6 +168,7 @@ export const SERVER_ACTION_SCHEMA = z.discriminatedUnion('type', [
167168
type: z.literal('usage-response'),
168169
usage: z.number(),
169170
limit: z.number(),
171+
referralLink: z.string().optional(),
170172
}),
171173
z.object({
172174
type: z.literal('quota-exceeded'),

common/src/constants.ts

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
// import { env } from './env.mjs'
2+
13
export const STOP_MARKER = '[' + 'END]'
24
export const FIND_FILES_MARKER = '[' + 'FIND_FILES_PLEASE]'
35
export const TOOL_RESULT_MARKER = '[' + 'TOOL_RESULT]'
@@ -39,11 +41,20 @@ export const SKIPPED_TERMINAL_COMMANDS = [
3941

4042
export const MAX_DATE = new Date(86399999999999)
4143

42-
export const CREDITS_USAGE_LIMITS = {
43-
ANON: 1_000,
44-
FREE: 2_500,
45-
PAID: 50_000,
46-
}
44+
export type UsageLimits = 'ANON' | 'FREE' | 'PAID'
45+
export const CREDITS_USAGE_LIMITS: Record<UsageLimits, number> =
46+
process.env.NEXT_PUBLIC_ENVIRONMENT === 'local'
47+
? {
48+
ANON: 1_000_000,
49+
FREE: 2_500_000,
50+
PAID: 50_000_000,
51+
}
52+
: {
53+
ANON: 1_000,
54+
FREE: 2_500,
55+
PAID: 50_000,
56+
}
57+
export const CREDITS_REFERRAL_BONUS = 500
4758

4859
export const claudeModels = {
4960
sonnet: 'claude-3-5-sonnet-20240620',
@@ -61,3 +72,5 @@ export const models = {
6172
}
6273

6374
export const TEST_USER_ID = 'test-user-id'
75+
76+
export const MAX_REFERRALS = 5
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
DO $$ BEGIN
2+
CREATE TYPE "public"."referral_status" AS ENUM('pending', 'completed');
3+
EXCEPTION
4+
WHEN duplicate_object THEN null;
5+
END $$;
6+
--> statement-breakpoint
7+
CREATE TABLE IF NOT EXISTS "referral" (
8+
"referrer_id" text NOT NULL,
9+
"referred_id" text NOT NULL,
10+
"status" "referral_status" DEFAULT 'pending' NOT NULL,
11+
"credits" integer NOT NULL,
12+
"created_at" timestamp DEFAULT now() NOT NULL,
13+
"completed_at" timestamp,
14+
CONSTRAINT "referral_referrer_id_referred_id_pk" PRIMARY KEY("referrer_id","referred_id")
15+
);
16+
--> statement-breakpoint
17+
ALTER TABLE "user" ADD COLUMN "referral_code" text DEFAULT 'ref-' || gen_random_uuid();--> statement-breakpoint
18+
DO $$ BEGIN
19+
ALTER TABLE "referral" ADD CONSTRAINT "referral_referrer_id_user_id_fk" FOREIGN KEY ("referrer_id") REFERENCES "public"."user"("id") ON DELETE no action ON UPDATE no action;
20+
EXCEPTION
21+
WHEN duplicate_object THEN null;
22+
END $$;
23+
--> statement-breakpoint
24+
DO $$ BEGIN
25+
ALTER TABLE "referral" ADD CONSTRAINT "referral_referred_id_user_id_fk" FOREIGN KEY ("referred_id") REFERENCES "public"."user"("id") ON DELETE no action ON UPDATE no action;
26+
EXCEPTION
27+
WHEN duplicate_object THEN null;
28+
END $$;
29+
--> statement-breakpoint
30+
ALTER TABLE "user" ADD CONSTRAINT "user_referral_code_unique" UNIQUE("referral_code");

0 commit comments

Comments
 (0)