@cogito.ai/cli 0.4.3 → 0.4.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/dist/index.js +1 -1
  2. package/dist/templates/web-nextjs/apps/docs/.source/browser.ts +8 -18
  3. package/dist/templates/web-nextjs/apps/docs/.source/dynamic.ts +5 -11
  4. package/dist/templates/web-nextjs/apps/docs/.source/server.ts +26 -37
  5. package/dist/templates/web-nextjs/apps/docs/content/docs/meta.json +1 -1
  6. package/dist/templates/web-nextjs/apps/docs/content/docs/template/alipay.mdx +276 -0
  7. package/dist/templates/web-nextjs/apps/docs/content/docs/template/deployment.mdx +32 -0
  8. package/dist/templates/web-nextjs/apps/docs/content/docs/template/drizzle.mdx +18 -0
  9. package/dist/templates/web-nextjs/apps/docs/content/docs/template/getting-started.mdx +98 -0
  10. package/dist/templates/web-nextjs/apps/docs/content/docs/template/meta.json +13 -0
  11. package/dist/templates/web-nextjs/apps/docs/content/docs/template/stripe.mdx +18 -0
  12. package/dist/templates/web-nextjs/apps/docs/content/docs/template/supabase.mdx +130 -0
  13. package/dist/templates/web-nextjs/apps/docs/content/docs/template/troubleshooting.mdx +87 -0
  14. package/dist/templates/web-nextjs/apps/docs/content/docs/template/wechat-pay.mdx +318 -0
  15. package/dist/templates/web-nextjs/apps/web/.env.example +35 -0
  16. package/dist/templates/web-nextjs/apps/web/messages/en.json +70 -0
  17. package/dist/templates/web-nextjs/apps/web/messages/zh.json +71 -1
  18. package/dist/templates/web-nextjs/apps/web/next-env.d.ts +1 -1
  19. package/dist/templates/web-nextjs/apps/web/package.json +4 -0
  20. package/dist/templates/web-nextjs/apps/web/src/app/[locale]/(auth)/forgot-password/page.tsx +4 -10
  21. package/dist/templates/web-nextjs/apps/web/src/app/[locale]/(protected)/payment/[paymentId]/page.tsx +88 -0
  22. package/dist/templates/web-nextjs/apps/web/src/app/[locale]/(protected)/payment/checkout/page.tsx +170 -0
  23. package/dist/templates/web-nextjs/apps/web/src/app/[locale]/(protected)/pricing/page.tsx +120 -0
  24. package/dist/templates/web-nextjs/apps/web/src/app/[locale]/(protected)/settings/layout.tsx +48 -1
  25. package/dist/templates/web-nextjs/apps/web/src/app/[locale]/(protected)/settings/subscription/page.tsx +128 -0
  26. package/dist/templates/web-nextjs/apps/web/src/app/api/payments/alipay/create/route.ts +43 -0
  27. package/dist/templates/web-nextjs/apps/web/src/app/api/payments/alipay/notify/route.ts +105 -0
  28. package/dist/templates/web-nextjs/apps/web/src/app/api/payments/alipay/query/route.ts +54 -0
  29. package/dist/templates/web-nextjs/apps/web/src/app/api/payments/alipay/return/route.ts +20 -0
  30. package/dist/templates/web-nextjs/apps/web/src/app/api/payments/create/route.ts +52 -0
  31. package/dist/templates/web-nextjs/apps/web/src/app/api/payments/wechat/create/route.ts +58 -0
  32. package/dist/templates/web-nextjs/apps/web/src/app/api/payments/wechat/notify/route.ts +92 -0
  33. package/dist/templates/web-nextjs/apps/web/src/app/api/payments/wechat/query/route.ts +54 -0
  34. package/dist/templates/web-nextjs/apps/web/src/app/docs/page.tsx +5 -0
  35. package/dist/templates/web-nextjs/apps/web/src/features/subscription/alipay/client.ts +36 -0
  36. package/dist/templates/web-nextjs/apps/web/src/features/subscription/alipay/server.ts +83 -0
  37. package/dist/templates/web-nextjs/apps/web/src/features/subscription/components/index.ts +4 -0
  38. package/dist/templates/web-nextjs/apps/web/src/features/subscription/components/pro-badge.tsx +9 -0
  39. package/dist/templates/web-nextjs/apps/web/src/features/subscription/components/pro-feature-comparison.tsx +70 -0
  40. package/dist/templates/web-nextjs/apps/web/src/features/subscription/components/quota-warning-banner.tsx +37 -0
  41. package/dist/templates/web-nextjs/apps/web/src/features/subscription/components/upgrade-button.tsx +42 -0
  42. package/dist/templates/web-nextjs/apps/web/src/features/subscription/hooks.ts +141 -0
  43. package/dist/templates/web-nextjs/apps/web/src/features/subscription/payment-helpers.ts +27 -0
  44. package/dist/templates/web-nextjs/apps/web/src/features/subscription/payment-service.ts +56 -0
  45. package/dist/templates/web-nextjs/apps/web/src/features/subscription/service.ts +55 -0
  46. package/dist/templates/web-nextjs/apps/web/src/features/subscription/types.ts +73 -0
  47. package/dist/templates/web-nextjs/apps/web/src/features/subscription/wechat/client.ts +33 -0
  48. package/dist/templates/web-nextjs/apps/web/src/features/subscription/wechat/server.ts +147 -0
  49. package/dist/templates/web-nextjs/apps/web/src/lib/supabase/admin.ts +23 -0
  50. package/dist/templates/web-nextjs/apps/web/src/lib/wechat-pay/client.ts +66 -0
  51. package/dist/templates/web-nextjs/apps/web/src/lib/wechat-pay/crypto.ts +99 -0
  52. package/dist/templates/web-nextjs/apps/web/src/lib/wechat-pay/types.ts +10 -0
  53. package/dist/templates/web-nextjs/pnpm-lock.yaml +319 -0
  54. package/dist/templates/web-nextjs/pnpm-workspace.yaml +7 -8
  55. package/dist/templates/web-nextjs/supabase/migrations/20250608_add_subscription_tables.sql +125 -0
  56. package/package.json +1 -1
@@ -0,0 +1,83 @@
1
+ import { AlipaySdk } from 'alipay-sdk'
2
+
3
+ const ALIPAY_APP_ID = process.env.ALIPAY_APP_ID
4
+ const ALIPAY_PRIVATE_KEY = process.env.ALIPAY_PRIVATE_KEY
5
+ const ALIPAY_PUBLIC_KEY = process.env.ALIPAY_PUBLIC_KEY
6
+ const ALIPAY_GATEWAY = process.env.ALIPAY_GATEWAY || 'https://openapi.alipay.com/gateway.do'
7
+
8
+ export interface AlipayPayParams {
9
+ outTradeNo: string
10
+ totalAmount: string
11
+ subject: string
12
+ body?: string
13
+ returnUrl: string
14
+ notifyUrl: string
15
+ }
16
+
17
+ export function getAlipaySDK(): AlipaySdk {
18
+ if (!ALIPAY_APP_ID || !ALIPAY_PRIVATE_KEY || !ALIPAY_PUBLIC_KEY) {
19
+ throw new Error(
20
+ 'Missing Alipay credentials. Ensure ALIPAY_APP_ID, ALIPAY_PRIVATE_KEY, and ALIPAY_PUBLIC_KEY are set.',
21
+ )
22
+ }
23
+
24
+ return new AlipaySdk({
25
+ appId: ALIPAY_APP_ID,
26
+ privateKey: ALIPAY_PRIVATE_KEY,
27
+ signType: 'RSA2',
28
+ alipayPublicKey: ALIPAY_PUBLIC_KEY,
29
+ gateway: ALIPAY_GATEWAY,
30
+ camelcase: true,
31
+ })
32
+ }
33
+
34
+ export async function createAlipayPagePay(params: AlipayPayParams) {
35
+ const alipay = getAlipaySDK()
36
+ const result = await alipay.exec('alipay.trade.page.pay', {
37
+ notify_url: params.notifyUrl,
38
+ return_url: params.returnUrl,
39
+ bizContent: {
40
+ out_trade_no: params.outTradeNo,
41
+ total_amount: params.totalAmount,
42
+ subject: params.subject,
43
+ body: params.body,
44
+ product_code: 'FAST_INSTANT_TRADE_PAY',
45
+ },
46
+ })
47
+
48
+ return { formHtml: result }
49
+ }
50
+
51
+ export async function createAlipayWapPay(params: AlipayPayParams) {
52
+ const alipay = getAlipaySDK()
53
+ const result = await alipay.exec('alipay.trade.wap.pay', {
54
+ notify_url: params.notifyUrl,
55
+ return_url: params.returnUrl,
56
+ bizContent: {
57
+ out_trade_no: params.outTradeNo,
58
+ total_amount: params.totalAmount,
59
+ subject: params.subject,
60
+ body: params.body,
61
+ product_code: 'QUICK_WAP_WAY',
62
+ },
63
+ })
64
+
65
+ return { redirectUrl: (result as unknown as { url: string }).url }
66
+ }
67
+
68
+ export async function queryAlipayOrder(outTradeNo: string) {
69
+ const alipay = getAlipaySDK()
70
+ const result = await alipay.exec('alipay.trade.query', {
71
+ bizContent: { out_trade_no: outTradeNo },
72
+ })
73
+
74
+ return result as unknown as {
75
+ tradeStatus: string
76
+ code: string
77
+ }
78
+ }
79
+
80
+ export async function verifyAlipayNotify(formData: Record<string, unknown>) {
81
+ const alipay = getAlipaySDK()
82
+ return alipay.checkNotifySign(formData)
83
+ }
@@ -0,0 +1,4 @@
1
+ export { UpgradeButton } from './upgrade-button'
2
+ export { ProBadge } from './pro-badge'
3
+ export { ProFeatureComparison } from './pro-feature-comparison'
4
+ export { QuotaWarningBanner } from './quota-warning-banner'
@@ -0,0 +1,9 @@
1
+ import { Badge } from '@/components/ui/badge'
2
+
3
+ export function ProBadge() {
4
+ return (
5
+ <Badge variant="default" className="bg-gradient-to-r from-amber-500 to-orange-500 text-white">
6
+ PRO
7
+ </Badge>
8
+ )
9
+ }
@@ -0,0 +1,70 @@
1
+ 'use client'
2
+
3
+ import { useTranslations } from 'next-intl'
4
+ import { Check, X } from 'lucide-react'
5
+
6
+ interface Feature {
7
+ name: string
8
+ free: boolean | string
9
+ pro: boolean | string
10
+ }
11
+
12
+ interface ProFeatureComparisonProps {
13
+ features?: Feature[]
14
+ }
15
+
16
+ export function ProFeatureComparison({ features }: ProFeatureComparisonProps) {
17
+ const t = useTranslations('subscription')
18
+
19
+ const defaultFeatures: Feature[] = [
20
+ { name: t('featureProjects'), free: '3', pro: '50' },
21
+ { name: t('featureApiCalls'), free: '1,000', pro: '100,000' },
22
+ { name: t('featureSupport'), free: false, pro: true },
23
+ { name: t('featureAnalytics'), free: false, pro: true },
24
+ ]
25
+
26
+ const displayFeatures = features || defaultFeatures
27
+
28
+ return (
29
+ <div className="w-full overflow-hidden rounded-lg border">
30
+ <table className="w-full text-sm">
31
+ <thead>
32
+ <tr className="border-b bg-muted/50">
33
+ <th className="px-4 py-3 text-left font-medium">{t('feature')}</th>
34
+ <th className="px-4 py-3 text-center font-medium">{t('free')}</th>
35
+ <th className="px-4 py-3 text-center font-medium">{t('pro')}</th>
36
+ </tr>
37
+ </thead>
38
+ <tbody>
39
+ {displayFeatures.map((feat, index) => (
40
+ <tr key={index} className="border-b last:border-0">
41
+ <td className="px-4 py-3 font-medium">{feat.name}</td>
42
+ <td className="px-4 py-3 text-center">
43
+ {typeof feat.free === 'boolean' ? (
44
+ feat.free ? (
45
+ <Check className="mx-auto h-4 w-4 text-green-500" />
46
+ ) : (
47
+ <X className="mx-auto h-4 w-4 text-muted-foreground" />
48
+ )
49
+ ) : (
50
+ <span>{feat.free}</span>
51
+ )}
52
+ </td>
53
+ <td className="px-4 py-3 text-center">
54
+ {typeof feat.pro === 'boolean' ? (
55
+ feat.pro ? (
56
+ <Check className="mx-auto h-4 w-4 text-green-500" />
57
+ ) : (
58
+ <X className="mx-auto h-4 w-4 text-muted-foreground" />
59
+ )
60
+ ) : (
61
+ <span className="font-medium">{feat.pro}</span>
62
+ )}
63
+ </td>
64
+ </tr>
65
+ ))}
66
+ </tbody>
67
+ </table>
68
+ </div>
69
+ )
70
+ }
@@ -0,0 +1,37 @@
1
+ 'use client'
2
+
3
+ import { useRouter } from 'next/navigation'
4
+ import { AlertTriangle } from 'lucide-react'
5
+ import { Button } from '@/components/ui/button'
6
+
7
+ interface QuotaWarningBannerProps {
8
+ quotaName?: string
9
+ currentUsage?: number
10
+ maxQuota?: number
11
+ }
12
+
13
+ export function QuotaWarningBanner({
14
+ quotaName = 'API calls',
15
+ currentUsage = 0,
16
+ maxQuota = 1000,
17
+ }: QuotaWarningBannerProps) {
18
+ const router = useRouter()
19
+ const percentage = (currentUsage / maxQuota) * 100
20
+ const isNearLimit = percentage >= 80
21
+
22
+ if (!isNearLimit) return null
23
+
24
+ return (
25
+ <div className="flex items-center gap-3 rounded-lg border border-amber-200 bg-amber-50 p-4 dark:border-amber-900 dark:bg-amber-950">
26
+ <AlertTriangle className="h-5 w-5 flex-shrink-0 text-amber-600 dark:text-amber-400" />
27
+ <div className="flex-1">
28
+ <p className="text-sm font-medium text-amber-900 dark:text-amber-200">
29
+ You are using {currentUsage} of {maxQuota} {quotaName} ({percentage.toFixed(0)}%)
30
+ </p>
31
+ </div>
32
+ <Button variant="outline" size="sm" onClick={() => router.push('/pricing')}>
33
+ Upgrade
34
+ </Button>
35
+ </div>
36
+ )
37
+ }
@@ -0,0 +1,42 @@
1
+ 'use client'
2
+
3
+ import { useRouter } from 'next/navigation'
4
+ import { Button } from '@/components/ui/button'
5
+ import { Zap } from 'lucide-react'
6
+
7
+ interface UpgradeButtonProps {
8
+ source?: string
9
+ feature?: string
10
+ trigger?: string
11
+ icon?: boolean
12
+ text?: string
13
+ highlight?: boolean
14
+ }
15
+
16
+ export function UpgradeButton({
17
+ source = 'default',
18
+ feature,
19
+ trigger,
20
+ icon = true,
21
+ text = 'Upgrade',
22
+ highlight = false,
23
+ }: UpgradeButtonProps) {
24
+ const router = useRouter()
25
+
26
+ const handleClick = () => {
27
+ const params = new URLSearchParams()
28
+ if (source) params.set('source', source)
29
+ if (feature) params.set('feature', feature)
30
+ if (trigger) params.set('trigger', trigger)
31
+
32
+ const queryString = params.toString()
33
+ router.push(`/pricing${queryString ? `?${queryString}` : ''}`)
34
+ }
35
+
36
+ return (
37
+ <Button onClick={handleClick} variant={highlight ? 'default' : 'outline'} size="sm">
38
+ {icon && <Zap className="mr-1 h-4 w-4" />}
39
+ {text}
40
+ </Button>
41
+ )
42
+ }
@@ -0,0 +1,141 @@
1
+ 'use client'
2
+
3
+ import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
4
+ import type { Payment, SubscriptionPlan, UserSubscription } from './types'
5
+
6
+ function createSupabaseClient() {
7
+ return import('@supabase/ssr').then(({ createBrowserClient }) =>
8
+ createBrowserClient(
9
+ process.env.NEXT_PUBLIC_SUPABASE_URL!,
10
+ process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
11
+ ),
12
+ )
13
+ }
14
+
15
+ export function useSubscriptionPlans() {
16
+ return useQuery({
17
+ queryKey: ['subscription-plans'],
18
+ queryFn: async (): Promise<SubscriptionPlan[]> => {
19
+ const supabase = await createSupabaseClient()
20
+ const { data, error } = await supabase
21
+ .from('subscription_plans')
22
+ .select('*')
23
+ .eq('is_active', true)
24
+ .order('display_order', { ascending: true })
25
+
26
+ if (error) throw error
27
+ return (data as SubscriptionPlan[]) ?? []
28
+ },
29
+ })
30
+ }
31
+
32
+ export function useSubscriptionPlan(planCode: string) {
33
+ return useQuery({
34
+ queryKey: ['subscription-plan', planCode],
35
+ queryFn: async (): Promise<SubscriptionPlan | null> => {
36
+ const supabase = await createSupabaseClient()
37
+ const { data, error } = await supabase
38
+ .from('subscription_plans')
39
+ .select('*')
40
+ .eq('plan_code', planCode)
41
+ .single()
42
+
43
+ if (error && error.code !== 'PGRST116') throw error
44
+ return (data as SubscriptionPlan) ?? null
45
+ },
46
+ })
47
+ }
48
+
49
+ export function useUserSubscription(userId: string) {
50
+ return useQuery({
51
+ queryKey: ['user-subscription', userId],
52
+ queryFn: async (): Promise<UserSubscription | null> => {
53
+ const supabase = await createSupabaseClient()
54
+ const { data, error } = await supabase
55
+ .from('user_subscriptions')
56
+ .select('*')
57
+ .eq('user_id', userId)
58
+ .in('status', ['trial', 'active'])
59
+ .order('created_at', { ascending: false })
60
+ .limit(1)
61
+ .single()
62
+
63
+ if (error && error.code !== 'PGRST116') throw error
64
+ return (data as UserSubscription) ?? null
65
+ },
66
+ staleTime: 2 * 60 * 1000, // 2 minutes
67
+ enabled: !!userId,
68
+ })
69
+ }
70
+
71
+ export function useUserPayments(userId: string) {
72
+ return useQuery({
73
+ queryKey: ['user-payments', userId],
74
+ queryFn: async (): Promise<Payment[]> => {
75
+ const supabase = await createSupabaseClient()
76
+ const { data, error } = await supabase
77
+ .from('payments')
78
+ .select('*')
79
+ .eq('user_id', userId)
80
+ .order('created_at', { ascending: false })
81
+
82
+ if (error) throw error
83
+ return (data as Payment[]) ?? []
84
+ },
85
+ enabled: !!userId,
86
+ })
87
+ }
88
+
89
+ interface PaymentStatusResult {
90
+ status: string
91
+ }
92
+
93
+ export function usePaymentStatus(
94
+ orderNo: string,
95
+ paymentMethod: string,
96
+ options?: { enabled?: boolean },
97
+ ) {
98
+ return useQuery({
99
+ queryKey: ['payment-status', orderNo],
100
+ queryFn: async (): Promise<PaymentStatusResult> => {
101
+ const endpoint =
102
+ paymentMethod === 'alipay' ? '/api/payments/alipay/query' : '/api/payments/wechat/query'
103
+ const res = await fetch(endpoint, {
104
+ method: 'POST',
105
+ headers: { 'Content-Type': 'application/json' },
106
+ body: JSON.stringify({ outTradeNo: orderNo }),
107
+ })
108
+ if (!res.ok) throw new Error('Failed to query payment status')
109
+ return res.json() as Promise<PaymentStatusResult>
110
+ },
111
+ refetchInterval: 3000,
112
+ enabled: options?.enabled ?? !!orderNo,
113
+ })
114
+ }
115
+
116
+ interface CreatePaymentParams {
117
+ userId: string
118
+ planId: number
119
+ amount: number
120
+ paymentMethod: string
121
+ billingCycle: string
122
+ }
123
+
124
+ export function useCreatePayment() {
125
+ const queryClient = useQueryClient()
126
+
127
+ return useMutation({
128
+ mutationFn: async (params: CreatePaymentParams) => {
129
+ const res = await fetch('/api/payments/create', {
130
+ method: 'POST',
131
+ headers: { 'Content-Type': 'application/json' },
132
+ body: JSON.stringify(params),
133
+ })
134
+ if (!res.ok) throw new Error('Failed to create payment')
135
+ return res.json()
136
+ },
137
+ onSuccess: () => {
138
+ queryClient.invalidateQueries({ queryKey: ['user-payments'] })
139
+ },
140
+ })
141
+ }
@@ -0,0 +1,27 @@
1
+ import crypto from 'crypto'
2
+
3
+ /**
4
+ * Generates a unique order number in the format WN{timestamp}{4-digit-random}
5
+ * WN = WebNextjs prefix to avoid conflicts with other projects
6
+ */
7
+ export function generateOrderNumber(): string {
8
+ const timestamp = Date.now()
9
+ const random = Math.floor(Math.random() * 10000)
10
+ .toString()
11
+ .padStart(4, '0')
12
+ return `WN${timestamp}${random}`
13
+ }
14
+
15
+ /**
16
+ * Formats a price in cents to a human-readable currency string (CNY)
17
+ */
18
+ export function formatPriceInCents(cents: number): string {
19
+ return `¥${(cents / 100).toFixed(2)}`
20
+ }
21
+
22
+ /**
23
+ * Checks if a subscription is currently active
24
+ */
25
+ export function isSubscriptionActive(status: string): boolean {
26
+ return status === 'trial' || status === 'active'
27
+ }
@@ -0,0 +1,56 @@
1
+ import { getServerClient } from '@/infra/db/client'
2
+ import type { Payment, CreatePaymentParams } from './types'
3
+
4
+ export async function createPaymentRecord(params: CreatePaymentParams): Promise<Payment> {
5
+ const supabase = await getServerClient()
6
+ const { data, error } = await supabase
7
+ .from('payments')
8
+ .insert({
9
+ user_id: params.user_id,
10
+ subscription_id: params.subscription_id ?? null,
11
+ order_no: params.order_no,
12
+ amount: params.amount,
13
+ currency: params.currency ?? 'CNY',
14
+ payment_method: params.payment_method,
15
+ payment_channel: params.payment_channel ?? null,
16
+ status: 'pending',
17
+ metadata: params.metadata ?? {},
18
+ })
19
+ .select()
20
+ .single()
21
+
22
+ if (error) throw error
23
+ return data as Payment
24
+ }
25
+
26
+ export async function getPaymentByOrderNo(orderNo: string): Promise<Payment | null> {
27
+ const supabase = await getServerClient()
28
+ const { data, error } = await supabase
29
+ .from('payments')
30
+ .select('*')
31
+ .eq('order_no', orderNo)
32
+ .single()
33
+
34
+ if (error && error.code !== 'PGRST116') throw error
35
+ return (data as Payment) ?? null
36
+ }
37
+
38
+ export async function getPaymentById(id: number): Promise<Payment | null> {
39
+ const supabase = await getServerClient()
40
+ const { data, error } = await supabase.from('payments').select('*').eq('id', id).single()
41
+
42
+ if (error && error.code !== 'PGRST116') throw error
43
+ return (data as Payment) ?? null
44
+ }
45
+
46
+ export async function getUserPayments(userId: string): Promise<Payment[]> {
47
+ const supabase = await getServerClient()
48
+ const { data, error } = await supabase
49
+ .from('payments')
50
+ .select('*')
51
+ .eq('user_id', userId)
52
+ .order('created_at', { ascending: false })
53
+
54
+ if (error) throw error
55
+ return (data as Payment[]) ?? []
56
+ }
@@ -0,0 +1,55 @@
1
+ import { getBrowserClient, getServerClient } from '@/infra/db/client'
2
+ import type { SubscriptionPlan, UserSubscription } from './types'
3
+
4
+ export async function getActiveSubscriptionPlans(): Promise<SubscriptionPlan[]> {
5
+ const supabase = await getServerClient()
6
+ const { data, error } = await supabase
7
+ .from('subscription_plans')
8
+ .select('*')
9
+ .eq('is_active', true)
10
+ .order('display_order', { ascending: true })
11
+
12
+ if (error) throw error
13
+ return (data as SubscriptionPlan[]) ?? []
14
+ }
15
+
16
+ export async function getSubscriptionPlanByCode(
17
+ planCode: string,
18
+ ): Promise<SubscriptionPlan | null> {
19
+ const supabase = await getServerClient()
20
+ const { data, error } = await supabase
21
+ .from('subscription_plans')
22
+ .select('*')
23
+ .eq('plan_code', planCode)
24
+ .single()
25
+
26
+ if (error && error.code !== 'PGRST116') throw error
27
+ return (data as SubscriptionPlan) ?? null
28
+ }
29
+
30
+ export async function getUserActiveSubscription(userId: string): Promise<UserSubscription | null> {
31
+ const supabase = await getServerClient()
32
+ const { data, error } = await supabase
33
+ .from('user_subscriptions')
34
+ .select('*')
35
+ .eq('user_id', userId)
36
+ .in('status', ['trial', 'active'])
37
+ .order('created_at', { ascending: false })
38
+ .limit(1)
39
+ .single()
40
+
41
+ if (error && error.code !== 'PGRST116') throw error
42
+ return (data as UserSubscription) ?? null
43
+ }
44
+
45
+ export async function getUserSubscriptionHistory(userId: string): Promise<UserSubscription[]> {
46
+ const supabase = await getServerClient()
47
+ const { data, error } = await supabase
48
+ .from('user_subscriptions')
49
+ .select('*')
50
+ .eq('user_id', userId)
51
+ .order('created_at', { ascending: false })
52
+
53
+ if (error) throw error
54
+ return (data as UserSubscription[]) ?? []
55
+ }
@@ -0,0 +1,73 @@
1
+ export type PlanCode = 'free' | 'pro' | 'enterprise'
2
+
3
+ export type BillingCycle = 'monthly' | 'yearly'
4
+
5
+ export type PaymentMethod = 'alipay' | 'wechat' | 'stripe'
6
+
7
+ export type PaymentStatus = 'pending' | 'paid' | 'failed' | 'refunded' | 'cancelled'
8
+
9
+ export type SubscriptionStatus = 'trial' | 'active' | 'expired' | 'cancelled' | 'pending'
10
+
11
+ export type PaymentChannel = 'alipay_page' | 'alipay_wap' | 'wechat_native' | 'wechat_h5'
12
+
13
+ export interface SubscriptionPlan {
14
+ id: number
15
+ plan_code: PlanCode
16
+ plan_name: string
17
+ description: string | null
18
+ price_monthly: number
19
+ price_yearly: number
20
+ features: Record<string, unknown>
21
+ display_order: number
22
+ is_active: boolean
23
+ is_featured: boolean
24
+ created_at: string
25
+ updated_at: string
26
+ }
27
+
28
+ export interface UserSubscription {
29
+ id: number
30
+ user_id: string
31
+ plan_id: number
32
+ status: SubscriptionStatus
33
+ billing_cycle: BillingCycle
34
+ current_period_start: string | null
35
+ current_period_end: string | null
36
+ cancelled_at: string | null
37
+ cancel_at_period_end: boolean
38
+ last_payment_id: number | null
39
+ next_billing_date: string | null
40
+ metadata: Record<string, unknown> | null
41
+ created_at: string
42
+ updated_at: string
43
+ }
44
+
45
+ export interface Payment {
46
+ id: number
47
+ user_id: string
48
+ subscription_id: number | null
49
+ order_no: string
50
+ amount: number
51
+ currency: string
52
+ payment_method: PaymentMethod
53
+ payment_channel: PaymentChannel | null
54
+ status: PaymentStatus
55
+ provider_trade_no: string | null
56
+ provider_response: Record<string, unknown> | null
57
+ paid_at: string | null
58
+ refunded_at: string | null
59
+ metadata: Record<string, unknown> | null
60
+ created_at: string
61
+ updated_at: string
62
+ }
63
+
64
+ export interface CreatePaymentParams {
65
+ user_id: string
66
+ subscription_id?: number
67
+ order_no: string
68
+ amount: number
69
+ currency?: string
70
+ payment_method: PaymentMethod
71
+ payment_channel?: PaymentChannel
72
+ metadata?: Record<string, unknown>
73
+ }
@@ -0,0 +1,33 @@
1
+ 'use client'
2
+
3
+ interface WechatPayResult {
4
+ type: 'qrcode' | 'redirect'
5
+ codeUrl?: string
6
+ h5Url?: string
7
+ }
8
+
9
+ export async function initiateWechatPayment(
10
+ paymentId: number,
11
+ userId: string,
12
+ ): Promise<WechatPayResult> {
13
+ const res = await fetch('/api/payments/wechat/create', {
14
+ method: 'POST',
15
+ headers: { 'Content-Type': 'application/json' },
16
+ body: JSON.stringify({ paymentId, userId }),
17
+ })
18
+
19
+ if (!res.ok) {
20
+ throw new Error('Failed to initiate WeChat payment')
21
+ }
22
+
23
+ const data = (await res.json()) as WechatPayResult
24
+
25
+ if (data.type === 'qrcode' && data.codeUrl) {
26
+ return { type: 'qrcode', codeUrl: data.codeUrl }
27
+ } else if (data.type === 'redirect' && data.h5Url) {
28
+ window.location.href = data.h5Url
29
+ return { type: 'redirect', h5Url: data.h5Url }
30
+ }
31
+
32
+ throw new Error('Unexpected WeChat payment response')
33
+ }