@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.
- package/package.json +18 -0
- package/src/types/api.types.ts +423 -0
- package/src/types/database.types.ts +375 -0
- package/src/types/enums.ts +62 -0
- package/src/types/hono.types.ts +32 -0
- package/src/utils/ai.ts +70 -0
- package/src/utils/email.ts +26 -0
- package/src/utils/errors.ts +60 -0
- package/src/utils/json.ts +26 -0
- package/src/utils/pagination.ts +44 -0
- package/src/utils/storage.ts +57 -0
- package/src/utils/stripe.ts +240 -0
- package/src/validators/application.validator.ts +61 -0
- package/src/validators/candidate.validator.ts +52 -0
- package/src/validators/client.validator.ts +29 -0
- package/src/validators/common.validator.ts +16 -0
- package/src/validators/contact.validator.ts +29 -0
- package/src/validators/email-template.validator.ts +28 -0
- package/src/validators/job.validator.ts +61 -0
- package/src/validators/parse-cv.validator.ts +28 -0
- package/src/validators/saved-candidate.validator.ts +27 -0
- package/src/validators/seeker.validator.ts +29 -0
- package/src/validators/subscription.validator.ts +21 -0
- package/src/validators/user.validator.ts +86 -0
- package/src/validators/waitlist.validator.ts +8 -0
- package/src/validators/workspace.validator.ts +93 -0
- package/tsconfig.json +14 -0
|
@@ -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
|
+
});
|