50c74b9007
Build & Push / Pipeline Tests (push) Failing after 1m34s
Build & Push / Build & Push Docker Image (push) Has been skipped
Test / Type Check (all packages) (push) Successful in 56s
Test / API Unit Tests (push) Successful in 1m8s
Test / Homepage Unit Tests (push) Successful in 47s
Test / Carplace Unit Tests (push) Successful in 43s
Test / Admin Unit Tests (push) Successful in 46s
Test / Dashboard Unit Tests (push) Failing after 43s
Test / API Integration Tests (push) Successful in 1m8s
423 lines
16 KiB
TypeScript
423 lines
16 KiB
TypeScript
import { z } from 'zod'
|
|
import type {
|
|
CarplaceHomepageContent,
|
|
CarplaceHomepageHowItWorksStep,
|
|
CarplaceHomepageMetric,
|
|
CarplaceHomepagePillar,
|
|
CarplaceHomepageSectionType,
|
|
CarplaceHomepageStep,
|
|
CarplaceHomepageTestimonial,
|
|
} from '@rentaldrivego/types'
|
|
|
|
export const loginSchema = z.object({
|
|
email: z.string().email().max(255).trim().toLowerCase(),
|
|
password: z.string().max(128),
|
|
totpCode: z.string().length(6).optional(),
|
|
recoveryCode: z.string().min(8).max(32).optional(),
|
|
})
|
|
|
|
export const forgotPasswordSchema = z.object({
|
|
email: z.string().email().max(255).trim().toLowerCase(),
|
|
})
|
|
|
|
export const resetPasswordSchema = z.object({
|
|
token: z.string().min(1),
|
|
password: z.string().min(8).max(128),
|
|
})
|
|
|
|
export const totpVerifySchema = z.object({
|
|
code: z.string().length(6),
|
|
})
|
|
|
|
export const companiesQuerySchema = z.object({
|
|
q: z.string().optional(),
|
|
status: z.string().optional(),
|
|
plan: z.string().optional(),
|
|
page: z.coerce.number().int().min(1).default(1),
|
|
pageSize: z.coerce.number().int().min(1).max(100).default(20),
|
|
})
|
|
|
|
export const rentersQuerySchema = z.object({
|
|
q: z.string().optional(),
|
|
blocked: z.string().optional(),
|
|
page: z.coerce.number().int().min(1).default(1),
|
|
pageSize: z.coerce.number().int().min(1).max(100).default(20),
|
|
})
|
|
|
|
export const auditLogQuerySchema = z.object({
|
|
adminId: z.string().optional(),
|
|
action: z.string().optional(),
|
|
companyId: z.string().optional(),
|
|
entityId: z.string().optional(),
|
|
page: z.coerce.number().int().min(1).default(1),
|
|
pageSize: z.coerce.number().int().min(1).max(100).default(50),
|
|
})
|
|
|
|
const notificationChannelSchema = z.enum(['EMAIL', 'SMS', 'WHATSAPP', 'IN_APP', 'PUSH'])
|
|
const notificationStatusSchema = z.enum(['PENDING', 'QUEUED', 'SENT', 'DELIVERED', 'FAILED', 'SKIPPED', 'DEAD_LETTER', 'READ'])
|
|
|
|
export const notificationsQuerySchema = z.object({
|
|
channel: notificationChannelSchema.optional(),
|
|
status: notificationStatusSchema.optional(),
|
|
companyId: z.string().optional(),
|
|
page: z.coerce.number().int().min(1).default(1),
|
|
pageSize: z.coerce.number().int().min(1).max(200).default(50),
|
|
})
|
|
|
|
export const billingQuerySchema = z.object({
|
|
q: z.string().optional(),
|
|
status: z.string().optional(),
|
|
plan: z.string().optional(),
|
|
page: z.coerce.number().int().min(1).default(1),
|
|
pageSize: z.coerce.number().int().min(1).max(100).default(20),
|
|
})
|
|
|
|
export const invoicesQuerySchema = z.object({
|
|
page: z.coerce.number().int().min(1).default(1),
|
|
pageSize: z.coerce.number().int().min(1).max(100).default(20),
|
|
})
|
|
|
|
export const permissionSchema = z.object({
|
|
resource: z.enum(['COMPANIES', 'COMPANY_EMPLOYEES', 'SUBSCRIPTIONS', 'PAYMENTS', 'OFFERS', 'RENTERS', 'ADMIN_USERS', 'AUDIT_LOGS', 'PLATFORM_METRICS']),
|
|
actions: z.array(z.enum(['READ', 'CREATE', 'UPDATE', 'DELETE', 'SUSPEND', 'IMPERSONATE'])).min(1),
|
|
})
|
|
|
|
export const createAdminSchema = z.object({
|
|
email: z.string().email().trim().toLowerCase(),
|
|
firstName: z.string().min(1).max(100).trim(),
|
|
lastName: z.string().min(1).max(100).trim(),
|
|
role: z.enum(['SUPER_ADMIN', 'ADMIN', 'SUPPORT', 'FINANCE', 'VIEWER']),
|
|
password: z.string().min(8),
|
|
permissions: z.array(permissionSchema).optional(),
|
|
})
|
|
|
|
export const updateAdminSchema = z.object({
|
|
email: z.string().email().trim().toLowerCase().optional(),
|
|
firstName: z.string().min(1).max(100).trim().optional(),
|
|
lastName: z.string().min(1).max(100).trim().optional(),
|
|
role: z.enum(['SUPER_ADMIN', 'ADMIN', 'SUPPORT', 'FINANCE', 'VIEWER']).optional(),
|
|
password: z.string().min(8).optional(),
|
|
isActive: z.boolean().optional(),
|
|
})
|
|
|
|
export const adminRoleSchema = z.object({
|
|
role: z.enum(['SUPER_ADMIN', 'ADMIN', 'SUPPORT', 'FINANCE', 'VIEWER']),
|
|
})
|
|
|
|
export const adminPermissionsSchema = z.object({
|
|
permissions: z.array(permissionSchema),
|
|
})
|
|
|
|
export const companyStatusSchema = z.object({
|
|
status: z.enum(['ACTIVE', 'SUSPENDED', 'CANCELLED']),
|
|
reason: z.string().optional(),
|
|
})
|
|
|
|
const nullableString = z.union([z.string(), z.null()]).optional()
|
|
const nullableEmail = z.union([z.string().email(), z.null()]).optional()
|
|
const nullableUrl = z.union([z.string().url(), z.null()]).optional()
|
|
const nullableDate = z.union([z.string().datetime(), z.string().regex(/^\d{4}-\d{2}-\d{2}$/), z.null()]).optional()
|
|
|
|
export const adminCompanyUpdateSchema = z.object({
|
|
company: z.object({
|
|
name: z.string().min(1).optional(), slug: z.string().min(1).optional(),
|
|
email: z.string().email().optional(), phone: nullableString,
|
|
status: z.enum(['PENDING', 'TRIALING', 'ACTIVE', 'PAST_DUE', 'SUSPENDED', 'CANCELLED']).optional(),
|
|
subscriptionPaymentRef: nullableString,
|
|
address: z.object({
|
|
streetAddress: nullableString,
|
|
city: nullableString,
|
|
country: nullableString,
|
|
zipCode: nullableString,
|
|
legalName: nullableString,
|
|
legalForm: nullableString,
|
|
companyEmail: nullableEmail,
|
|
managerName: nullableString,
|
|
iceNumber: nullableString,
|
|
operatingLicenseNumber: nullableString,
|
|
operatingLicenseIssuedAt: nullableString,
|
|
operatingLicenseIssuedBy: nullableString,
|
|
fax: nullableString,
|
|
yearsActive: nullableString,
|
|
representativeName: nullableString,
|
|
representativeTitle: nullableString,
|
|
responsibleName: nullableString,
|
|
responsibleRole: nullableString,
|
|
responsibleIdentityNumber: nullableString,
|
|
responsibleQualification: nullableString,
|
|
responsiblePhone: nullableString,
|
|
responsibleEmail: nullableEmail,
|
|
}).optional(),
|
|
}).optional(),
|
|
subscription: z.object({
|
|
plan: z.enum(['STARTER', 'GROWTH', 'PRO']).optional(),
|
|
billingPeriod: z.enum(['MONTHLY', 'ANNUAL']).optional(),
|
|
status: z.enum(['TRIALING', 'ACTIVE', 'PAST_DUE', 'CANCELLED', 'UNPAID']).optional(),
|
|
currency: z.literal('MAD').optional(),
|
|
trialStartAt: nullableDate, trialEndAt: nullableDate,
|
|
currentPeriodStart: nullableDate, currentPeriodEnd: nullableDate,
|
|
cancelledAt: nullableDate, cancelAtPeriodEnd: z.boolean().optional(),
|
|
}).optional(),
|
|
brand: z.object({
|
|
displayName: z.string().min(1).optional(), tagline: nullableString,
|
|
subdomain: z.string().min(1).optional(), customDomain: nullableString,
|
|
publicEmail: nullableEmail, publicPhone: nullableString, publicAddress: nullableString,
|
|
publicCity: nullableString, publicCountry: nullableString, websiteUrl: nullableUrl,
|
|
whatsappNumber: nullableString, defaultLocale: z.string().min(2).optional(),
|
|
defaultCurrency: z.literal('MAD').optional(),
|
|
isListedOnCarplace: z.boolean().optional(),
|
|
homePageConfig: z.any().optional(),
|
|
menuConfig: z.any().optional(),
|
|
}).optional(),
|
|
contractSettings: z.object({
|
|
legalName: nullableString, registrationNumber: nullableString, taxId: nullableString,
|
|
terms: z.string().optional(),
|
|
fuelPolicyType: z.enum(['FULL_TO_FULL', 'FULL_TO_EMPTY', 'SAME_TO_SAME', 'PREPAID', 'FREE']).optional(),
|
|
lateFeePerHour: z.number().int().nullable().optional(), taxRate: z.number().nullable().optional(),
|
|
signatureRequired: z.boolean().optional(), showTax: z.boolean().optional(),
|
|
}).optional(),
|
|
accountingSettings: z.object({
|
|
reportingPeriod: z.enum(['WEEKLY', 'MONTHLY', 'QUARTERLY', 'ANNUAL']).optional(),
|
|
fiscalYearStart: z.number().int().min(1).max(12).optional(),
|
|
currency: z.literal('MAD').optional(),
|
|
accountantEmail: nullableEmail, accountantName: nullableString,
|
|
autoSendReport: z.boolean().optional(),
|
|
reportFormat: z.enum(['PDF', 'CSV', 'BOTH']).optional(),
|
|
}).optional(),
|
|
})
|
|
|
|
const carplaceMetricSchema: z.ZodType<CarplaceHomepageMetric> = z.object({ value: z.string().min(1), label: z.string().min(1) })
|
|
const carplacePillarSchema: z.ZodType<CarplaceHomepagePillar> = z.object({ title: z.string().min(1), body: z.string().min(1) })
|
|
const carplaceStepSchema: z.ZodType<CarplaceHomepageStep> = z.object({ step: z.string().min(1), title: z.string().min(1), body: z.string().min(1) })
|
|
const carplaceHowItWorksStepSchema: z.ZodType<CarplaceHomepageHowItWorksStep> = z.object({ number: z.string().min(1), title: z.string().min(1), description: z.string().min(1) })
|
|
const carplaceTestimonialSchema: z.ZodType<CarplaceHomepageTestimonial> = z.object({ quote: z.string().min(1), author: z.string().min(1), role: z.string().min(1), company: z.string().optional() })
|
|
const carplaceSectionSchema: z.ZodType<CarplaceHomepageSectionType> = z.enum(['hero', 'surface', 'pillars', 'audiences', 'features', 'howitworks', 'steps', 'testimonials', 'closing'])
|
|
|
|
const carplaceHomepageContentSchema: z.ZodType<CarplaceHomepageContent> = z.object({
|
|
sections: z.array(carplaceSectionSchema).min(1),
|
|
heroKicker: z.string().min(1), heroTitle: z.string().min(1), heroBody: z.string().min(1),
|
|
startTrial: z.string().min(1), exploreVehicles: z.string().min(1),
|
|
surfaceLabel: z.string().min(1), surfaceTitle: z.string().min(1), surfaceBody: z.string().min(1),
|
|
liveLabel: z.string().min(1), trustedFleets: z.string().min(1), brandedFlows: z.string().min(1), multiTenant: z.string().min(1),
|
|
companyKicker: z.string().min(1), companyTitle: z.string().min(1), companyBody: z.string().min(1),
|
|
renterKicker: z.string().min(1), renterTitle: z.string().min(1), renterBody: z.string().min(1),
|
|
pillars: z.array(carplacePillarSchema).length(3),
|
|
metrics: z.array(carplaceMetricSchema).length(3),
|
|
featureLabel: z.string().min(1), features: z.array(z.string().min(1)).min(1),
|
|
howitworksKicker: z.string().min(1), howitworksTitle: z.string().min(1), howitworksSteps: z.array(carplaceHowItWorksStepSchema).min(1),
|
|
stepsTitle: z.string().min(1), steps: z.array(carplaceStepSchema).length(3), stepLabel: z.string().min(1),
|
|
testimonialsKicker: z.string().min(1), testimonialsTitle: z.string().min(1), testimonials: z.array(carplaceTestimonialSchema).min(1),
|
|
readyKicker: z.string().min(1), readyTitle: z.string().min(1), readyBody: z.string().min(1),
|
|
viewPricing: z.string().min(1), createWorkspace: z.string().min(1),
|
|
})
|
|
|
|
export const carplaceHomepageConfigSchema = z.object({
|
|
en: carplaceHomepageContentSchema,
|
|
fr: carplaceHomepageContentSchema,
|
|
ar: carplaceHomepageContentSchema,
|
|
})
|
|
|
|
export const idParamSchema = z.object({
|
|
id: z.string().min(1),
|
|
})
|
|
|
|
export const companyIdParamSchema = z.object({
|
|
companyId: z.string().min(1),
|
|
})
|
|
|
|
export const billingAccountIdParamSchema = z.object({
|
|
billingAccountId: z.string().min(1),
|
|
})
|
|
|
|
export const invoiceIdParamSchema = z.object({
|
|
invoiceId: z.string().min(1),
|
|
})
|
|
|
|
export const billingAccountUpdateSchema = z.object({
|
|
legalName: z.string().min(1).max(255).optional(),
|
|
billingEmail: z.string().email().optional(),
|
|
billingAddress: z.any().optional(),
|
|
taxId: z.union([z.string().max(120), z.null()]).optional(),
|
|
taxExempt: z.boolean().optional(),
|
|
invoiceTerms: z.enum(['DUE_ON_RECEIPT', 'NET_7', 'NET_15', 'NET_30', 'NET_45', 'NET_60']).optional(),
|
|
netTermsDays: z.number().int().min(0).max(365).optional(),
|
|
})
|
|
|
|
export const billingLineItemInputSchema = z.object({
|
|
type: z.enum([
|
|
'SUBSCRIPTION_FEE',
|
|
'SEAT_FEE',
|
|
'USAGE_FEE',
|
|
'SETUP_FEE',
|
|
'DISCOUNT',
|
|
'TAX',
|
|
'CREDIT',
|
|
'PRORATION',
|
|
'REFUND_ADJUSTMENT',
|
|
'MANUAL_ADJUSTMENT',
|
|
'PROFESSIONAL_SERVICES',
|
|
'CANCELLATION_FEE',
|
|
]),
|
|
description: z.string().min(1).max(255),
|
|
quantity: z.number().int().positive().max(100000),
|
|
unitAmount: z.number().int(),
|
|
periodStart: nullableDate,
|
|
periodEnd: nullableDate,
|
|
})
|
|
|
|
export const createBillingInvoiceSchema = z.object({
|
|
subscriptionId: z.union([z.string(), z.null()]).optional(),
|
|
invoiceType: z.enum([
|
|
'SUBSCRIPTION_INITIAL',
|
|
'SUBSCRIPTION_RENEWAL',
|
|
'TRIAL_CONVERSION',
|
|
'SUBSCRIPTION_UPGRADE',
|
|
'SUBSCRIPTION_DOWNGRADE_CREDIT',
|
|
'USAGE_BASED',
|
|
'ONE_TIME',
|
|
'MANUAL',
|
|
'CORRECTION',
|
|
'CANCELLATION_FEE',
|
|
]),
|
|
currency: z.string().min(3).max(8).optional(),
|
|
dueAt: nullableDate,
|
|
isSubscriptionBlocking: z.boolean().optional(),
|
|
adminReason: z.union([z.string().max(500), z.null()]).optional(),
|
|
lineItems: z.array(billingLineItemInputSchema).min(1),
|
|
})
|
|
|
|
export const payBillingInvoiceSchema = z.object({
|
|
amount: z.number().int().positive().optional(),
|
|
paymentMethodId: z.union([z.string(), z.null()]).optional(),
|
|
providerPaymentId: z.union([z.string(), z.null()]).optional(),
|
|
})
|
|
|
|
export const retryBillingInvoiceSchema = z.object({
|
|
paymentMethodId: z.union([z.string(), z.null()]).optional(),
|
|
})
|
|
|
|
export const billingReasonSchema = z.object({
|
|
reason: z.string().min(1).max(500),
|
|
})
|
|
|
|
export const billingCreditNoteSchema = z.object({
|
|
amount: z.number().int().positive(),
|
|
reason: z.string().min(1).max(500),
|
|
})
|
|
|
|
export const billingRefundSchema = z.object({
|
|
amount: z.number().int().positive(),
|
|
reason: z.string().min(1).max(500),
|
|
})
|
|
|
|
export const homepageUpdateSchema = z.object({
|
|
homepage: carplaceHomepageConfigSchema,
|
|
})
|
|
|
|
export const pricingUpdateSchema = z.object({
|
|
entries: z.array(z.object({
|
|
plan: z.enum(['STARTER', 'GROWTH', 'PRO']),
|
|
billingPeriod: z.enum(['MONTHLY', 'ANNUAL']),
|
|
amount: z.number().int().positive(),
|
|
})).min(1),
|
|
})
|
|
|
|
const planFeatureSchema = z.object({
|
|
plan: z.enum(['STARTER', 'GROWTH', 'PRO']),
|
|
label: z.string().min(1).max(120),
|
|
sortOrder: z.number().int().min(0).default(0),
|
|
})
|
|
|
|
export const planFeatureCreateSchema = planFeatureSchema
|
|
export const planFeatureUpdateSchema = planFeatureSchema.partial()
|
|
export const promotionCreateSchema = z.object({
|
|
code: z.string().min(2).max(30).regex(/^[A-Z0-9_-]+$/, 'Code must be uppercase A-Z, 0-9, _ or -'),
|
|
name: z.string().min(1).max(100),
|
|
description: z.string().max(500).optional(),
|
|
discountType: z.enum(['PERCENTAGE', 'FIXED']),
|
|
discountValue: z.number().int().positive(),
|
|
plans: z.array(z.enum(['STARTER', 'GROWTH', 'PRO'])),
|
|
periods: z.array(z.enum(['MONTHLY', 'ANNUAL'])),
|
|
maxUses: z.number().int().positive().nullable().optional(),
|
|
validFrom: z.string().datetime(),
|
|
validUntil: z.string().datetime().nullable().optional(),
|
|
isActive: z.boolean().default(true),
|
|
})
|
|
|
|
export const promotionUpdateSchema = promotionCreateSchema.partial()
|
|
|
|
export const planFeatureIdParamSchema = z.object({ featureId: z.string().min(1) })
|
|
export const promotionIdParamSchema = z.object({ promotionId: z.string().min(1) })
|
|
|
|
const employeeRoleSchema = z.enum(['OWNER', 'MANAGER', 'AGENT'])
|
|
const planSchema = z.enum(['STARTER', 'GROWTH', 'PRO'])
|
|
const menuItemTypeSchema = z.enum(['INTERNAL_PAGE', 'EXTERNAL_LINK', 'PARENT_MENU', 'SECTION_LABEL', 'DIVIDER'])
|
|
|
|
export const menuItemSchema = z.object({
|
|
systemKey: z.string().trim().min(1).max(120).optional().nullable(),
|
|
label: z.string().trim().min(1).max(120),
|
|
itemType: menuItemTypeSchema,
|
|
routeOrUrl: z.string().trim().max(400).optional().nullable(),
|
|
icon: z.string().trim().max(120).optional().nullable(),
|
|
parentId: z.string().trim().min(1).optional().nullable(),
|
|
displayOrder: z.number().int().min(0).default(0),
|
|
openInNewTab: z.boolean().default(false),
|
|
isRequired: z.boolean().default(false),
|
|
isActive: z.boolean().default(true),
|
|
roles: z.array(employeeRoleSchema).default([]),
|
|
subscriptionPlans: z.array(z.object({
|
|
plan: planSchema,
|
|
displayOrder: z.number().int().min(0).optional(),
|
|
isActive: z.boolean().optional(),
|
|
})).default([]),
|
|
companyAssignments: z.array(z.object({
|
|
companyId: z.string().trim().min(1),
|
|
displayOrder: z.number().int().min(0).optional(),
|
|
isActive: z.boolean().optional(),
|
|
})).default([]),
|
|
})
|
|
|
|
export const menuItemStatusSchema = z.object({
|
|
isActive: z.boolean(),
|
|
})
|
|
|
|
export const menuPlanAssignmentsSchema = z.object({
|
|
assignments: z.array(z.object({
|
|
plan: planSchema,
|
|
displayOrder: z.number().int().min(0).optional(),
|
|
isActive: z.boolean().optional(),
|
|
})).min(1),
|
|
})
|
|
|
|
export const menuCompanyAssignmentsSchema = z.object({
|
|
assignments: z.array(z.object({
|
|
companyId: z.string().trim().min(1),
|
|
displayOrder: z.number().int().min(0).optional(),
|
|
isActive: z.boolean().optional(),
|
|
})).min(1),
|
|
})
|
|
|
|
export const menuPreviewSchema = z.object({
|
|
companyId: z.string().trim().min(1),
|
|
role: employeeRoleSchema,
|
|
})
|
|
|
|
export const menuAuditLogQuerySchema = z.object({
|
|
page: z.coerce.number().int().min(1).default(1),
|
|
pageSize: z.coerce.number().int().min(1).max(100).default(20),
|
|
})
|
|
|
|
export const menuItemIdParamSchema = z.object({
|
|
id: z.string().min(1),
|
|
})
|
|
|
|
export const menuPlanParamSchema = z.object({
|
|
id: z.string().min(1),
|
|
plan: planSchema,
|
|
})
|
|
|
|
export const menuCompanyParamSchema = z.object({
|
|
id: z.string().min(1),
|
|
companyId: z.string().min(1),
|
|
})
|