@unedio/types 0.0.1

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.
@@ -0,0 +1,57 @@
1
+ import { SupabaseClient } from '@supabase/supabase-js';
2
+
3
+ const CURRICULUM_REFERENCE_PREFIX = 'curriculums:';
4
+ const SIGNED_URL_TTL_SECONDS = 60 * 60;
5
+
6
+ export function buildCurriculumReference(filePath: string): string {
7
+ return `${CURRICULUM_REFERENCE_PREFIX}${filePath}`;
8
+ }
9
+
10
+ export async function resolveCurriculumUrl(
11
+ supabase: SupabaseClient,
12
+ cvUrl: string | null,
13
+ expiresIn = SIGNED_URL_TTL_SECONDS,
14
+ ): Promise<string | null> {
15
+ if (!cvUrl) {
16
+ return null;
17
+ }
18
+
19
+ if (/^https?:\/\//i.test(cvUrl)) {
20
+ return cvUrl;
21
+ }
22
+
23
+ const filePath = cvUrl.startsWith(CURRICULUM_REFERENCE_PREFIX)
24
+ ? cvUrl.slice(CURRICULUM_REFERENCE_PREFIX.length)
25
+ : cvUrl;
26
+
27
+ if (!filePath) {
28
+ return null;
29
+ }
30
+
31
+ const { data, error } = await supabase.storage
32
+ .from('curriculums')
33
+ .createSignedUrl(filePath, expiresIn);
34
+
35
+ if (error) {
36
+ console.error('Failed to create signed curriculum URL:', error);
37
+ return null;
38
+ }
39
+
40
+ return data.signedUrl;
41
+ }
42
+
43
+ export async function deleteCurriculum(supabase: SupabaseClient, cvUrl: string): Promise<void> {
44
+ const filePath = cvUrl.startsWith(CURRICULUM_REFERENCE_PREFIX)
45
+ ? cvUrl.slice(CURRICULUM_REFERENCE_PREFIX.length)
46
+ : cvUrl;
47
+
48
+ if (!filePath || /^https?:\/\//i.test(filePath)) {
49
+ return;
50
+ }
51
+
52
+ const { error } = await supabase.storage.from('curriculums').remove([filePath]);
53
+
54
+ if (error) {
55
+ console.error('Failed to delete curriculum from storage:', error);
56
+ }
57
+ }
@@ -0,0 +1,240 @@
1
+ /**
2
+ * Stripe utilities for payment processing
3
+ */
4
+
5
+ // Note: Stripe integration requires environment variables and Stripe SDK
6
+ // This is a basic structure - needs Stripe API key configuration
7
+
8
+ export interface StripeConfig {
9
+ apiKey: string;
10
+ webhookSecret: string;
11
+ }
12
+
13
+ export interface CreateCheckoutSessionParams {
14
+ customerId?: string;
15
+ priceId: string;
16
+ successUrl: string;
17
+ cancelUrl: string;
18
+ metadata?: Record<string, string>;
19
+ }
20
+
21
+ export interface CreateCustomerParams {
22
+ email: string;
23
+ name?: string;
24
+ metadata?: Record<string, string>;
25
+ }
26
+
27
+ export type PlanSlug = 'trial' | 'solo' | 'team' | 'business' | 'enterprise';
28
+
29
+ export interface BillingPlanDetails {
30
+ slug: PlanSlug;
31
+ name: string;
32
+ monthly_price: number | null;
33
+ max_seats: number | null;
34
+ stripe_price_id: string | null;
35
+ is_trial: boolean;
36
+ includes_branding: boolean;
37
+ includes_custom_domain: boolean;
38
+ limits: {
39
+ max_jobs: number;
40
+ max_applications_per_job: number;
41
+ };
42
+ }
43
+
44
+ /**
45
+ * Stripe service wrapper
46
+ * Note: Actual implementation requires @stripe/stripe-js or stripe npm package
47
+ */
48
+ export class StripeService {
49
+ private apiKey: string;
50
+
51
+ constructor(apiKey: string) {
52
+ this.apiKey = apiKey;
53
+ }
54
+
55
+ /**
56
+ * Create a Stripe customer
57
+ */
58
+ async createCustomer(params: CreateCustomerParams): Promise<any> {
59
+ // TODO: Implement with actual Stripe SDK
60
+ // const stripe = new Stripe(this.apiKey);
61
+ // return await stripe.customers.create(params);
62
+ void params;
63
+ throw new Error('Stripe integration not yet implemented');
64
+ }
65
+
66
+ /**
67
+ * Create a checkout session
68
+ */
69
+ async createCheckoutSession(params: CreateCheckoutSessionParams): Promise<any> {
70
+ // TODO: Implement with actual Stripe SDK
71
+ // const stripe = new Stripe(this.apiKey);
72
+ // return await stripe.checkout.sessions.create({
73
+ // customer: params.customerId,
74
+ // line_items: [{ price: params.priceId, quantity: 1 }],
75
+ // mode: 'subscription',
76
+ // success_url: params.successUrl,
77
+ // cancel_url: params.cancelUrl,
78
+ // metadata: params.metadata,
79
+ // });
80
+ void params;
81
+ throw new Error('Stripe integration not yet implemented');
82
+ }
83
+
84
+ /**
85
+ * Create a billing portal session
86
+ */
87
+ async createPortalSession(customerId: string, returnUrl: string): Promise<any> {
88
+ // TODO: Implement with actual Stripe SDK
89
+ // const stripe = new Stripe(this.apiKey);
90
+ // return await stripe.billingPortal.sessions.create({
91
+ // customer: customerId,
92
+ // return_url: returnUrl,
93
+ // });
94
+ void customerId;
95
+ void returnUrl;
96
+ throw new Error('Stripe integration not yet implemented');
97
+ }
98
+
99
+ /**
100
+ * Verify webhook signature
101
+ */
102
+ verifyWebhookSignature(payload: string, signature: string, webhookSecret: string): any {
103
+ // TODO: Implement with actual Stripe SDK
104
+ // const stripe = new Stripe(this.apiKey);
105
+ // return stripe.webhooks.constructEvent(payload, signature, webhookSecret);
106
+ void payload;
107
+ void signature;
108
+ void webhookSecret;
109
+ throw new Error('Stripe integration not yet implemented');
110
+ }
111
+
112
+ /**
113
+ * Cancel subscription
114
+ */
115
+ async cancelSubscription(subscriptionId: string): Promise<any> {
116
+ // TODO: Implement with actual Stripe SDK
117
+ // const stripe = new Stripe(this.apiKey);
118
+ // return await stripe.subscriptions.cancel(subscriptionId);
119
+ void subscriptionId;
120
+ throw new Error('Stripe integration not yet implemented');
121
+ }
122
+
123
+ /**
124
+ * Update subscription
125
+ */
126
+ async updateSubscription(
127
+ subscriptionId: string,
128
+ params: { priceId?: string; metadata?: Record<string, string> },
129
+ ): Promise<any> {
130
+ // TODO: Implement with actual Stripe SDK
131
+ // const stripe = new Stripe(this.apiKey);
132
+ // const updateData: any = {};
133
+ // if (params.priceId) {
134
+ // updateData.items = [{ price: params.priceId }];
135
+ // }
136
+ // if (params.metadata) {
137
+ // updateData.metadata = params.metadata;
138
+ // }
139
+ // return await stripe.subscriptions.update(subscriptionId, updateData);
140
+ void subscriptionId;
141
+ void params;
142
+ throw new Error('Stripe integration not yet implemented');
143
+ }
144
+ }
145
+
146
+ export const TRIAL_PERIOD_DAYS = 15;
147
+
148
+ // Subscription plan definitions aligned with the current UNEDIO business model.
149
+ export const SUBSCRIPTION_PLANS: Record<PlanSlug, BillingPlanDetails> = {
150
+ trial: {
151
+ slug: 'trial',
152
+ name: 'Trial',
153
+ monthly_price: null,
154
+ max_seats: 1,
155
+ stripe_price_id: null,
156
+ is_trial: true,
157
+ includes_branding: true,
158
+ includes_custom_domain: false,
159
+ limits: {
160
+ max_jobs: -1,
161
+ max_applications_per_job: -1,
162
+ },
163
+ },
164
+ solo: {
165
+ slug: 'solo',
166
+ name: 'Solo',
167
+ monthly_price: 29,
168
+ max_seats: 1,
169
+ stripe_price_id: 'price_solo_monthly',
170
+ is_trial: false,
171
+ includes_branding: true,
172
+ includes_custom_domain: true,
173
+ limits: {
174
+ max_jobs: -1,
175
+ max_applications_per_job: -1,
176
+ },
177
+ },
178
+ team: {
179
+ slug: 'team',
180
+ name: 'Team',
181
+ monthly_price: 99,
182
+ max_seats: 10,
183
+ stripe_price_id: 'price_team_monthly',
184
+ is_trial: false,
185
+ includes_branding: true,
186
+ includes_custom_domain: true,
187
+ limits: {
188
+ max_jobs: -1,
189
+ max_applications_per_job: -1,
190
+ },
191
+ },
192
+ business: {
193
+ slug: 'business',
194
+ name: 'Business',
195
+ monthly_price: 249,
196
+ max_seats: 25,
197
+ stripe_price_id: 'price_business_monthly',
198
+ is_trial: false,
199
+ includes_branding: true,
200
+ includes_custom_domain: true,
201
+ limits: {
202
+ max_jobs: -1,
203
+ max_applications_per_job: -1,
204
+ },
205
+ },
206
+ enterprise: {
207
+ slug: 'enterprise',
208
+ name: 'Enterprise',
209
+ monthly_price: null,
210
+ max_seats: null,
211
+ stripe_price_id: null,
212
+ is_trial: false,
213
+ includes_branding: true,
214
+ includes_custom_domain: true,
215
+ limits: {
216
+ max_jobs: -1,
217
+ max_applications_per_job: -1,
218
+ },
219
+ },
220
+ };
221
+
222
+ export function resolvePlanSlug(plan: string | null | undefined): PlanSlug | null {
223
+ if (!plan) return null;
224
+
225
+ const normalized = plan.trim().toLowerCase();
226
+ if (normalized in SUBSCRIPTION_PLANS) {
227
+ return normalized as PlanSlug;
228
+ }
229
+
230
+ return null;
231
+ }
232
+
233
+ export function getSubscriptionPlan(plan: string | null | undefined): BillingPlanDetails | null {
234
+ const slug = resolvePlanSlug(plan);
235
+ return slug ? SUBSCRIPTION_PLANS[slug] : null;
236
+ }
237
+
238
+ export function listSubscriptionPlans(): BillingPlanDetails[] {
239
+ return Object.values(SUBSCRIPTION_PLANS);
240
+ }
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Validation schemas for application-related operations
3
+ */
4
+
5
+ import { z } from '@hono/zod-openapi';
6
+
7
+ // Create application (apply to job)
8
+ export const createApplicationSchema = z.object({
9
+ source: z.string().max(50).optional().default('direct'),
10
+ });
11
+
12
+ // Update application
13
+ export const updateApplicationSchema = z.object({
14
+ status_id: z.string().uuid('Invalid status ID').optional(),
15
+ });
16
+
17
+ // List applications query params
18
+ export const listApplicationsSchema = z.object({
19
+ job_id: z.string().uuid('Invalid job ID').optional(),
20
+ status_id: z.string().uuid('Invalid status ID').optional(),
21
+ search: z.string().max(255).optional(),
22
+ page: z.coerce.number().min(1).optional().default(1),
23
+ limit: z.coerce.number().min(1).max(100).optional().default(20),
24
+ });
25
+
26
+ // Create comment
27
+ export const createCommentSchema = z.object({
28
+ comment_text: z.string().min(1, 'Comment cannot be empty').max(2000),
29
+ });
30
+
31
+ // Create application status
32
+ export const createStatusSchema = z.object({
33
+ status_name: z.string().min(1, 'Status name is required').max(100),
34
+ order: z.number().min(0).optional().default(0),
35
+ });
36
+
37
+ // Update application status
38
+ export const updateStatusSchema = z.object({
39
+ status_name: z.string().min(1, 'Status name cannot be empty').max(100).optional(),
40
+ order: z.number().min(0).optional(),
41
+ });
42
+
43
+ // Reorder statuses
44
+ export const reorderStatusesSchema = z.object({
45
+ statuses: z
46
+ .array(
47
+ z.object({
48
+ id: z.string().uuid('Invalid status ID'),
49
+ order: z.number().min(0),
50
+ }),
51
+ )
52
+ .min(1, 'At least one status must be provided'),
53
+ });
54
+
55
+ export type CreateApplicationInput = z.infer<typeof createApplicationSchema>;
56
+ export type UpdateApplicationInput = z.infer<typeof updateApplicationSchema>;
57
+ export type ListApplicationsInput = z.infer<typeof listApplicationsSchema>;
58
+ export type CreateCommentInput = z.infer<typeof createCommentSchema>;
59
+ export type CreateStatusInput = z.infer<typeof createStatusSchema>;
60
+ export type UpdateStatusInput = z.infer<typeof updateStatusSchema>;
61
+ export type ReorderStatusesInput = z.infer<typeof reorderStatusesSchema>;
@@ -0,0 +1,52 @@
1
+ import { z } from '@hono/zod-openapi';
2
+ import { isValidEmail } from '../utils/email';
3
+
4
+ const candidateEmailSchema = z.string().refine((value) => isValidEmail(value), {
5
+ message: 'Invalid email format',
6
+ });
7
+
8
+ export const candidateExperienceSchema = z.object({
9
+ company: z.string().max(200),
10
+ position: z.string().max(200),
11
+ description: z.string().max(2000).optional().nullable().default(''),
12
+ start_date: z.string().max(20).optional().default(''),
13
+ end_date: z.string().max(20).optional().nullable().default(null),
14
+ });
15
+
16
+ export const candidateEducationSchema = z.object({
17
+ institution: z.string().max(200),
18
+ degree: z.string().max(200),
19
+ field: z.string().max(200).optional().nullable().default(null),
20
+ start_date: z.string().max(20).optional().default(''),
21
+ end_date: z.string().max(20).optional().nullable().default(null),
22
+ });
23
+
24
+ export const createCandidateSchema = z.object({
25
+ email: candidateEmailSchema,
26
+ name: z.string().max(100),
27
+ phone: z.string().max(50).optional(),
28
+ linkedin_url: z.string().url().optional().nullable(),
29
+ city: z.string().max(100).optional(),
30
+ country: z.string().max(100).optional(),
31
+ summary: z.string().max(4000).optional(),
32
+ skills: z.array(z.string().max(100)).optional(),
33
+ experience_years: z.number().min(0).max(100).optional(),
34
+ education: z.array(candidateEducationSchema).optional(),
35
+ experience: z.array(candidateExperienceSchema).optional(),
36
+ cv_url: z.string().max(1000).optional().nullable(),
37
+ });
38
+
39
+ export const updateCandidateSchema = z.object({
40
+ full_name: z.string().max(100).optional(),
41
+ email: candidateEmailSchema.optional(),
42
+ phone: z.string().max(50).optional().nullable(),
43
+ linkedin_url: z.string().url().optional().nullable(),
44
+ city: z.string().max(100).optional().nullable(),
45
+ country: z.string().max(100).optional().nullable(),
46
+ summary: z.string().max(4000).optional().nullable(),
47
+ skills: z.array(z.string().max(100)).optional().nullable(),
48
+ experience_years: z.number().min(0).max(100).optional().nullable(),
49
+ education: z.array(candidateEducationSchema).optional().nullable(),
50
+ experience: z.array(candidateExperienceSchema).optional().nullable(),
51
+ cv_url: z.string().max(1000).optional().nullable(),
52
+ });
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Validation schemas for client-related operations
3
+ */
4
+
5
+ import { z } from '@hono/zod-openapi';
6
+ import { paginationSchema } from './common.validator';
7
+
8
+ /**
9
+ * List clients query params
10
+ */
11
+ export const listClientsSchema = paginationSchema;
12
+
13
+ /**
14
+ * Create client
15
+ */
16
+ export const createClientSchema = z.object({
17
+ name: z.string().min(1, 'Client name is required').max(255),
18
+ website: z.string().url('Invalid website URL').optional().or(z.literal('')),
19
+ industry: z.string().max(100).optional(),
20
+ });
21
+
22
+ /**
23
+ * Update client
24
+ */
25
+ export const updateClientSchema = z.object({
26
+ name: z.string().min(1, 'Client name cannot be empty').max(255).optional(),
27
+ website: z.string().url('Invalid website URL').optional().or(z.literal('')),
28
+ industry: z.string().max(100).optional(),
29
+ });
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Common validation schemas
3
+ */
4
+
5
+ import { z } from '@hono/zod-openapi';
6
+
7
+ /**
8
+ * Common pagination and search schema
9
+ */
10
+ export const paginationSchema = z.object({
11
+ page: z.coerce.number().min(1).optional().default(1),
12
+ limit: z.coerce.number().min(1).max(100).optional().default(20),
13
+ search: z.string().max(255).optional(),
14
+ });
15
+
16
+ export type PaginationInput = z.infer<typeof paginationSchema>;
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Validation schemas for contact-related operations
3
+ */
4
+
5
+ import { z } from '@hono/zod-openapi';
6
+
7
+ /**
8
+ * Create contact
9
+ */
10
+ export const createContactSchema = z.object({
11
+ client_id: z.string().uuid('Invalid client ID'),
12
+ full_name: z.string().min(1, 'Full name is required').max(255),
13
+ email: z.string().email('Invalid email').optional().or(z.literal('')),
14
+ phone: z.string().max(50).optional(),
15
+ role: z.string().max(100).optional(),
16
+ is_primary: z.boolean().optional(),
17
+ });
18
+
19
+ /**
20
+ * Update contact
21
+ */
22
+ export const updateContactSchema = z.object({
23
+ client_id: z.string().uuid('Invalid client ID').optional(),
24
+ full_name: z.string().min(1, 'Full name cannot be empty').max(255).optional(),
25
+ email: z.string().email('Invalid email').optional().or(z.literal('')),
26
+ phone: z.string().max(50).optional(),
27
+ role: z.string().max(100).optional(),
28
+ is_primary: z.boolean().optional(),
29
+ });
@@ -0,0 +1,28 @@
1
+ import { z } from '@hono/zod-openapi';
2
+
3
+ /**
4
+ * Email Template validators
5
+ */
6
+ export const createTemplateSchema = z.object({
7
+ template_type: z.enum(
8
+ [
9
+ 'application_received',
10
+ 'application_rejected',
11
+ 'interview_scheduled',
12
+ 'offer_extended',
13
+ 'custom',
14
+ ],
15
+ { message: 'Invalid template type' },
16
+ ),
17
+ subject: z.string().min(1).max(200, { message: 'Subject must be between 1 and 200 characters' }),
18
+ body: z.string().min(1).max(5000, { message: 'Body must be between 1 and 5000 characters' }),
19
+ });
20
+
21
+ export const updateTemplateSchema = z.object({
22
+ subject: z.string().min(1).max(200).optional(),
23
+ body: z.string().min(1).max(5000).optional(),
24
+ });
25
+
26
+ export const renderTemplateSchema = z.object({
27
+ variables: z.record(z.string(), z.string()),
28
+ });
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Validation schemas for job-related operations
3
+ */
4
+
5
+ import { z } from '@hono/zod-openapi';
6
+
7
+ // Job status validation
8
+ const jobStatusSchema = z.enum(['draft', 'published', 'archived'], {
9
+ message: 'Invalid job status',
10
+ });
11
+
12
+ // Work modality validation
13
+ const workModalitySchema = z.enum(['remote', 'hybrid', 'office'], {
14
+ message: 'Invalid work modality',
15
+ });
16
+
17
+ // Job type validation
18
+ const jobTypeSchema = z.enum(['full-time', 'part-time', 'contractor', 'internship'], {
19
+ message: 'Invalid job type',
20
+ });
21
+
22
+ // Create job
23
+ export const createJobSchema = z.object({
24
+ title: z.string().min(1, 'Job title is required').max(255),
25
+ description: z.string().min(10, 'Description must be at least 10 characters'),
26
+ work_modality: workModalitySchema.optional().nullable(),
27
+ job_type: jobTypeSchema.optional().nullable(),
28
+ location: z.string().max(255).optional().nullable(),
29
+ salary_range: z.string().max(100).optional().nullable(),
30
+ status: jobStatusSchema.optional().default('draft'),
31
+ client_id: z.string().uuid().optional().nullable(),
32
+ contact_id: z.string().uuid().optional().nullable(),
33
+ });
34
+
35
+ // Update job
36
+ export const updateJobSchema = z.object({
37
+ title: z.string().min(1, 'Job title cannot be empty').max(255).optional(),
38
+ description: z.string().min(10, 'Description must be at least 10 characters').optional(),
39
+ work_modality: workModalitySchema.optional().nullable(),
40
+ job_type: jobTypeSchema.optional().nullable(),
41
+ location: z.string().max(255).optional().nullable(),
42
+ salary_range: z.string().max(100).optional().nullable(),
43
+ status: jobStatusSchema.optional(),
44
+ client_id: z.string().uuid().optional().nullable(),
45
+ contact_id: z.string().uuid().optional().nullable(),
46
+ });
47
+
48
+ import { paginationSchema } from './common.validator';
49
+
50
+ // ... (other code)
51
+
52
+ // List jobs query params
53
+ export const listJobsSchema = paginationSchema.extend({
54
+ status: jobStatusSchema.optional(),
55
+ work_modality: workModalitySchema.optional(),
56
+ job_type: jobTypeSchema.optional(),
57
+ });
58
+
59
+ export type CreateJobInput = z.infer<typeof createJobSchema>;
60
+ export type UpdateJobInput = z.infer<typeof updateJobSchema>;
61
+ export type ListJobsInput = z.infer<typeof listJobsSchema>;
@@ -0,0 +1,28 @@
1
+ import { z } from '@hono/zod-openapi';
2
+
3
+ /**
4
+ * Parse CV validators
5
+ */
6
+
7
+ // Allowed languages for CV parsing
8
+ const ALLOWED_LANGUAGES = ['english', 'spanish', 'french', 'german', 'portuguese'] as const;
9
+
10
+ export const parseCVSchema = z.object({
11
+ file: z
12
+ .instanceof(File, { message: 'File is required' })
13
+ .refine((file) => file.size > 0, { message: 'File cannot be empty' })
14
+ .refine((file) => file.size <= 10 * 1024 * 1024, {
15
+ message: 'File too large. Maximum size is 10MB',
16
+ })
17
+ .refine((file) => file.type === 'application/pdf', {
18
+ message: 'Only PDF files are supported',
19
+ }),
20
+ lang: z
21
+ .enum(ALLOWED_LANGUAGES)
22
+ .default('english')
23
+ .refine((lang) => ALLOWED_LANGUAGES.includes(lang), {
24
+ message: `Invalid language. Allowed: ${ALLOWED_LANGUAGES.join(', ')}`,
25
+ }),
26
+ });
27
+
28
+ export type ParseCVInput = z.infer<typeof parseCVSchema>;
@@ -0,0 +1,27 @@
1
+ import { z } from '@hono/zod-openapi';
2
+ import { paginationSchema } from './common.validator';
3
+
4
+ /**
5
+ * List saved candidates query params
6
+ */
7
+ export const listSavedCandidatesSchema = paginationSchema;
8
+
9
+ /**
10
+ * Saved Candidates validators
11
+ */
12
+
13
+ export const saveCandidateSchema = z.object({
14
+ seeker_id: z.string().uuid({ message: 'Invalid seeker ID' }),
15
+ });
16
+
17
+ export const createManualCandidateSchema = z.object({
18
+ email: z.string().email(),
19
+ name: z.string().max(100).optional(),
20
+ linkedin_url: z.string().url().optional().nullable(),
21
+ city: z.string().max(100).optional(),
22
+ country: z.string().max(100).optional(),
23
+ skills: z.array(z.string()).optional(),
24
+ experience_years: z.number().int().min(0).max(100).optional(),
25
+ education: z.array(z.any()).optional(),
26
+ experience: z.array(z.any()).optional(),
27
+ });
@@ -0,0 +1,29 @@
1
+ import { z } from '@hono/zod-openapi';
2
+ import { paginationSchema } from './common.validator';
3
+
4
+ /**
5
+ * Seeker Profile validators
6
+ */
7
+ export const updateSeekerProfileSchema = z.object({
8
+ headline: z.string().max(200).optional(),
9
+ bio: z.string().max(1000).optional(),
10
+ skills: z.array(z.string()).optional(),
11
+ experience_years: z.number().int().min(0).max(100).optional(),
12
+ education: z.string().max(500).optional(),
13
+ city: z.string().max(100).optional(),
14
+ country: z.string().max(100).optional(),
15
+ linkedin_url: z.string().url().optional().nullable(),
16
+ github_url: z.string().url().optional().nullable(),
17
+ portfolio_url: z.string().url().optional().nullable(),
18
+ });
19
+
20
+ export const searchSeekersSchema = paginationSchema.extend({
21
+ skills: z
22
+ .array(z.string())
23
+ .optional()
24
+ .or(z.string().transform((s) => s.split(','))),
25
+ city: z.string().optional(),
26
+ country: z.string().optional(),
27
+ min_experience: z.coerce.number().int().min(0).optional(),
28
+ max_experience: z.coerce.number().int().min(0).optional(),
29
+ });
@@ -0,0 +1,21 @@
1
+ import { z } from '@hono/zod-openapi';
2
+
3
+ /**
4
+ * Subscription validators
5
+ */
6
+ export const updateSubscriptionSchema = z.object({
7
+ plan: z.enum(['solo', 'team', 'business', 'enterprise'], { message: 'Invalid plan' }),
8
+ });
9
+
10
+ export const createCheckoutSessionSchema = z.object({
11
+ plan: z.enum(['solo', 'team', 'business'], { message: 'Invalid plan for checkout' }),
12
+ success_url: z.string().url({ message: 'Invalid success URL' }),
13
+ cancel_url: z.string().url({ message: 'Invalid cancel URL' }),
14
+ });
15
+
16
+ export const stripeWebhookSchema = z.object({
17
+ type: z.string(),
18
+ data: z.object({
19
+ object: z.any(),
20
+ }),
21
+ });