Skip to content

Commit ca70e7f

Browse files
committed
up
1 parent 5f915e1 commit ca70e7f

11 files changed

Lines changed: 9134 additions & 6480 deletions

File tree

package-lock.json

Lines changed: 8807 additions & 5962 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@
104104
"autoprefixer": "^10.4.20",
105105
"eslint": "^8.57.1",
106106
"eslint-config-next": "^14.2.15",
107+
"jest-mock-extended": "^4.0.0",
107108
"jsdom": "^25.0.0",
108109
"postcss": "^8.5.6",
109110
"prettier": "^3.3.0",

src/components/analytics/top-products.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -351,7 +351,7 @@ export function TopProducts({
351351
outerRadius={120}
352352
fill="#8884d8"
353353
dataKey="value"
354-
label={({ name, percent }: { name: string; percent: number }) => `${name}: ${(percent * 100).toFixed(1)}%`}
354+
label={(props: any) => `${props.name}: ${(props.percent * 100).toFixed(1)}%`}
355355
labelLine={false}
356356
>
357357
{getPieData().map((entry, index) => (

src/hooks/use-toast.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
// Minimal useToast hook used by components in tests/dev
2+
// Provides a no-op toast in server/test environments and a simple wrapper in client
3+
4+
export default function useToast() {
5+
function toast({ title, description }: { title?: string; description?: string }) {
6+
if (typeof window !== 'undefined' && (window as any).console) {
7+
// Lightweight: log to console in dev; real app should replace with Radix toast
8+
// eslint-disable-next-line no-console
9+
console.info('[toast]', title, description);
10+
}
11+
}
12+
13+
return { toast };
14+
}

src/lib/plan-enforcement.ts

Lines changed: 101 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -344,4 +344,104 @@ export default {
344344
getUpgradeRecommendations,
345345
PLAN_ENFORCEMENT_ERRORS,
346346
isPlanEnforcementError,
347-
};
347+
};
348+
349+
// Compatibility implementations for legacy test/api usage
350+
// Legacy helpers used by older tests and route handlers
351+
export async function checkProductCreationLimit(request: NextRequest): Promise<null | NextResponse> {
352+
try {
353+
const session = await getSessionFromRequest(request);
354+
if (!session) {
355+
return NextResponse.json({ error: { code: 'UNAUTHORIZED', message: 'Authentication required' } }, { status: 401 });
356+
}
357+
358+
const storeId = (session as any).storeId;
359+
if (!storeId) {
360+
return NextResponse.json({ error: { code: 'MISSING_STORE_ID', message: 'Store ID is required' } }, { status: 400 });
361+
}
362+
363+
try {
364+
const result = await SubscriptionService.canCreateProduct(storeId);
365+
if (result && (result as any).allowed) {
366+
return null;
367+
}
368+
369+
return NextResponse.json({ error: { code: 'PLAN_LIMIT_EXCEEDED', message: 'Product creation limit exceeded for your current plan' } }, { status: 403 });
370+
} catch (err) {
371+
console.error('checkProductCreationLimit error', err);
372+
return NextResponse.json({ error: { code: 'PLAN_CHECK_FAILED', message: 'Failed to check plan limits' } }, { status: 500 });
373+
}
374+
} catch (err) {
375+
console.error('checkProductCreationLimit unexpected error', err);
376+
return NextResponse.json({ error: { code: 'ENFORCEMENT_ERROR', message: 'Failed to check plan limits' } }, { status: 500 });
377+
}
378+
}
379+
380+
export async function checkOrderCreationLimit(request: NextRequest): Promise<null | NextResponse> {
381+
try {
382+
const session = await getSessionFromRequest(request);
383+
if (!session) {
384+
return NextResponse.json({ error: { code: 'UNAUTHORIZED', message: 'Authentication required' } }, { status: 401 });
385+
}
386+
387+
const storeId = (session as any).storeId;
388+
if (!storeId) {
389+
return NextResponse.json({ error: { code: 'MISSING_STORE_ID', message: 'Store ID is required' } }, { status: 400 });
390+
}
391+
392+
try {
393+
const result = await SubscriptionService.canCreateOrder(storeId);
394+
if (result && (result as any).allowed) {
395+
return null;
396+
}
397+
398+
return NextResponse.json({ error: { code: 'PLAN_LIMIT_EXCEEDED', message: 'Order creation limit exceeded for your current plan' } }, { status: 403 });
399+
} catch (err) {
400+
console.error('checkOrderCreationLimit error', err);
401+
return NextResponse.json({ error: { code: 'PLAN_CHECK_FAILED', message: 'Failed to check plan limits' } }, { status: 500 });
402+
}
403+
} catch (err) {
404+
console.error('checkOrderCreationLimit unexpected error', err);
405+
return NextResponse.json({ error: { code: 'ENFORCEMENT_ERROR', message: 'Failed to check plan limits' } }, { status: 500 });
406+
}
407+
}
408+
409+
export async function enforcePlanLimits(
410+
request: NextRequest,
411+
handler: (request: NextRequest, context?: any) => Promise<NextResponse>,
412+
resource: 'product' | 'order'
413+
): Promise<NextResponse> {
414+
// Validate resource
415+
if (resource !== 'product' && resource !== 'order') {
416+
return NextResponse.json({ error: { code: 'INVALID_RESOURCE_TYPE', message: 'Invalid resource type for plan enforcement' } }, { status: 400 });
417+
}
418+
419+
// Allow safe methods without enforcement
420+
const method = (request && (request as any).method) || 'GET';
421+
if (['GET', 'PUT', 'PATCH', 'DELETE'].includes(method)) {
422+
return await handler(request);
423+
}
424+
425+
// Run appropriate check
426+
if (resource === 'product') {
427+
const check = await checkProductCreationLimit(request);
428+
if (check) return check as NextResponse;
429+
} else {
430+
const check = await checkOrderCreationLimit(request);
431+
if (check) return check as NextResponse;
432+
}
433+
434+
// Allowed, call handler
435+
return await handler(request);
436+
}
437+
438+
export function withPlanLimits(
439+
handler: (request: NextRequest, context?: any) => Promise<NextResponse>,
440+
resource: 'product' | 'order'
441+
) {
442+
return async (request: NextRequest, context?: any) => {
443+
// Delegate to enforcePlanLimits which will call the handler when allowed
444+
const result = await enforcePlanLimits(request, (req) => handler(req, context), resource);
445+
return result;
446+
};
447+
}

src/lib/sanitize.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ export function sanitizeHtml(
101101
ALLOW_ARIA_ATTR: true, // Allow ARIA for accessibility
102102
RETURN_DOM: false, // Return string (not DOM)
103103
RETURN_DOM_FRAGMENT: false,
104-
});
104+
} as any);
105105
}
106106

107107
/**
@@ -126,11 +126,11 @@ export function stripHtml(html: string): string {
126126
}
127127

128128
// Use DOMPurify with ALLOWED_TAGS: [] to strip everything
129-
return DOMPurify.sanitize(html, {
129+
return (DOMPurify.sanitize(html, {
130130
ALLOWED_TAGS: [], // No tags allowed
131131
ALLOWED_ATTR: [], // No attributes allowed
132132
KEEP_CONTENT: true, // Keep text content
133-
}).trim();
133+
} as any) as string).trim();
134134
}
135135

136136
/**

tests/__mocks__/mockPrisma.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { createMock } from 'jest-mock-extended';
2+
import { PrismaClient } from '@prisma/client';
3+
4+
// Provide a typed mocked Prisma client for tests to import
5+
export const mockedPrisma = createMock<PrismaClient>();
6+
7+
export default mockedPrisma;

tests/support/fixtures.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
// Test fixtures helpers used across unit tests
2+
import { Session, User } from '@/types';
3+
4+
export function buildUser(overrides?: Partial<User>): User {
5+
return {
6+
id: 'user_1',
7+
email: 'user@example.com',
8+
name: 'Test User',
9+
createdAt: new Date().toISOString(),
10+
updatedAt: new Date().toISOString(),
11+
...overrides,
12+
} as unknown as User;
13+
}
14+
15+
export function buildSession(overrides?: Partial<Session>): Session {
16+
return {
17+
id: 'sess_1',
18+
userId: 'user_1',
19+
storeId: 'store_1',
20+
createdAt: new Date().toISOString(),
21+
updatedAt: new Date().toISOString(),
22+
...overrides,
23+
} as unknown as Session;
24+
}

0 commit comments

Comments
 (0)