diff --git a/packages/spec/src/data/validation.test.ts b/packages/spec/src/data/validation.test.ts index 12cfec874..159e4182c 100644 --- a/packages/spec/src/data/validation.test.ts +++ b/packages/spec/src/data/validation.test.ts @@ -346,6 +346,186 @@ describe('ValidationRuleSchema (Discriminated Union)', () => { expect(() => ValidationRuleSchema.parse(validation)).not.toThrow(); }); + + // Salesforce-style validation examples + it('should validate opportunity close date is after create date (Salesforce pattern)', () => { + const validation = { + type: 'cross_field' as const, + name: 'close_date_after_create', + message: 'Close Date must be greater than or equal to Create Date', + condition: 'close_date >= created_date', + fields: ['close_date', 'created_date'], + }; + + expect(() => ValidationRuleSchema.parse(validation)).not.toThrow(); + }); + + it('should validate amount is within min/max range (Salesforce pattern)', () => { + const validation = { + type: 'cross_field' as const, + name: 'amount_in_range', + message: 'Amount must be between Minimum and Maximum values', + condition: 'amount >= min_amount AND amount <= max_amount', + fields: ['amount', 'min_amount', 'max_amount'], + }; + + expect(() => ValidationRuleSchema.parse(validation)).not.toThrow(); + }); + + it('should validate discount does not exceed total amount', () => { + const validation = { + type: 'cross_field' as const, + name: 'discount_not_exceed_total', + message: 'Discount cannot exceed Total Amount', + condition: 'discount_amount <= total_amount', + fields: ['discount_amount', 'total_amount'], + }; + + expect(() => ValidationRuleSchema.parse(validation)).not.toThrow(); + }); + + it('should validate shipping date is after order date', () => { + const validation = { + type: 'cross_field' as const, + name: 'shipping_after_order', + message: 'Shipping Date must be after Order Date', + condition: 'shipping_date > order_date', + fields: ['shipping_date', 'order_date'], + }; + + expect(() => ValidationRuleSchema.parse(validation)).not.toThrow(); + }); + + it('should validate percentage fields sum to 100', () => { + const validation = { + type: 'cross_field' as const, + name: 'percentage_sum', + message: 'Percentages must sum to 100', + condition: 'percent_a + percent_b + percent_c = 100', + fields: ['percent_a', 'percent_b', 'percent_c'], + }; + + expect(() => ValidationRuleSchema.parse(validation)).not.toThrow(); + }); + + it('should validate quantity does not exceed available stock', () => { + const validation = { + type: 'cross_field' as const, + name: 'quantity_check', + message: 'Order quantity cannot exceed available stock', + condition: 'order_quantity <= stock_available', + fields: ['order_quantity', 'stock_available'], + }; + + expect(() => ValidationRuleSchema.parse(validation)).not.toThrow(); + }); + + it('should validate renewal date is after contract start date', () => { + const validation = { + type: 'cross_field' as const, + name: 'renewal_after_start', + message: 'Renewal Date must be after Contract Start Date', + condition: 'renewal_date > contract_start_date', + fields: ['renewal_date', 'contract_start_date'], + }; + + expect(() => ValidationRuleSchema.parse(validation)).not.toThrow(); + }); + + it('should validate credit limit is not exceeded by balance', () => { + const validation = { + type: 'cross_field' as const, + name: 'credit_limit_check', + message: 'Balance cannot exceed Credit Limit', + condition: 'balance <= credit_limit', + fields: ['balance', 'credit_limit'], + }; + + expect(() => ValidationRuleSchema.parse(validation)).not.toThrow(); + }); + + it('should validate hours worked does not exceed capacity', () => { + const validation = { + type: 'cross_field' as const, + name: 'hours_capacity_check', + message: 'Hours worked cannot exceed capacity', + condition: 'hours_worked <= capacity_hours', + fields: ['hours_worked', 'capacity_hours'], + }; + + expect(() => ValidationRuleSchema.parse(validation)).not.toThrow(); + }); + + it('should validate multi-field dependency with OR condition', () => { + const validation = { + type: 'cross_field' as const, + name: 'approval_required', + message: 'Approval required if amount exceeds threshold or is high risk', + condition: 'amount > approval_threshold OR risk_level = "high"', + fields: ['amount', 'approval_threshold', 'risk_level'], + }; + + expect(() => ValidationRuleSchema.parse(validation)).not.toThrow(); + }); + + it('should validate contract term aligns with billing period', () => { + const validation = { + type: 'cross_field' as const, + name: 'term_billing_alignment', + message: 'Contract term must be a multiple of billing period', + condition: 'contract_term_months % billing_period_months = 0', + fields: ['contract_term_months', 'billing_period_months'], + }; + + expect(() => ValidationRuleSchema.parse(validation)).not.toThrow(); + }); + + it('should validate payment terms with credit check', () => { + const validation = { + type: 'cross_field' as const, + name: 'payment_credit_check', + message: 'Credit terms require minimum credit score', + condition: 'payment_terms = "credit" AND credit_score >= 650', + fields: ['payment_terms', 'credit_score'], + }; + + expect(() => ValidationRuleSchema.parse(validation)).not.toThrow(); + }); + + it('should validate minimum margin requirement', () => { + const validation = { + type: 'cross_field' as const, + name: 'minimum_margin', + message: 'Selling price must maintain minimum 20% margin', + condition: '(selling_price - cost_price) / cost_price >= 0.20', + fields: ['selling_price', 'cost_price'], + }; + + expect(() => ValidationRuleSchema.parse(validation)).not.toThrow(); + }); + + it('should handle edge case with empty fields array', () => { + const validation = { + type: 'cross_field' as const, + name: 'edge_case_validation', + message: 'Validation failed', + condition: 'true', + fields: [], + }; + + expect(() => ValidationRuleSchema.parse(validation)).not.toThrow(); + }); + + it('should enforce required fields property', () => { + const invalidValidation = { + type: 'cross_field' as const, + name: 'invalid_validation', + message: 'Missing fields', + condition: 'field_a > field_b', + }; + + expect(() => ValidationRuleSchema.parse(invalidValidation)).toThrow(); + }); }); describe('AsyncValidationSchema', () => { @@ -390,6 +570,160 @@ describe('ValidationRuleSchema (Discriminated Union)', () => { expect(result.timeout).toBe(5000); } }); + + // Use Case: Email Uniqueness Check + it('should validate email uniqueness via API', () => { + const validation = { + type: 'async' as const, + name: 'unique_email_check', + message: 'This email address is already registered', + field: 'email', + validatorUrl: '/api/users/check-email', + timeout: 3000, + debounce: 500, + }; + + expect(() => ValidationRuleSchema.parse(validation)).not.toThrow(); + }); + + // Use Case: Username Availability Check + it('should validate username availability with debounce', () => { + const validation = { + type: 'async' as const, + name: 'username_availability', + message: 'This username is not available', + field: 'username', + validatorUrl: '/api/users/check-username', + debounce: 300, + }; + + const result = ValidationRuleSchema.parse(validation); + if (result.type === 'async') { + expect(result.debounce).toBe(300); + expect(result.timeout).toBe(5000); // default + } + }); + + // Use Case: Domain Name Availability + it('should check domain name availability', () => { + const validation = { + type: 'async' as const, + name: 'domain_available', + message: 'This domain is already taken or reserved', + field: 'domain_name', + validatorUrl: '/api/domains/check-availability', + timeout: 2000, + }; + + expect(() => ValidationRuleSchema.parse(validation)).not.toThrow(); + }); + + // Use Case: Tax ID Validation via Government API + it('should validate tax ID via external service', () => { + const validation = { + type: 'async' as const, + name: 'validate_tax_id', + message: 'Invalid Tax ID number', + field: 'tax_id', + validatorFunction: 'validateTaxIdWithIRS', + timeout: 10000, // Government APIs may be slow + params: { country: 'US' }, + }; + + expect(() => ValidationRuleSchema.parse(validation)).not.toThrow(); + }); + + // Use Case: Credit Card Validation with Payment Gateway + it('should validate credit card via payment gateway', () => { + const validation = { + type: 'async' as const, + name: 'validate_card', + message: 'Invalid credit card', + field: 'card_number', + validatorUrl: 'https://api.stripe.com/v1/tokens/validate', + timeout: 5000, + params: { mode: 'validate_only' }, + }; + + expect(() => ValidationRuleSchema.parse(validation)).not.toThrow(); + }); + + // Use Case: Address Validation + it('should validate address via geocoding service', () => { + const validation = { + type: 'async' as const, + name: 'validate_address', + message: 'Unable to verify address', + field: 'street_address', + validatorFunction: 'validateAddressWithGoogleMaps', + timeout: 4000, + params: { + includeFields: ['city', 'state', 'zip'], + strictMode: true + }, + }; + + expect(() => ValidationRuleSchema.parse(validation)).not.toThrow(); + }); + + // Use Case: Coupon Code Validation + it('should validate coupon code availability', () => { + const validation = { + type: 'async' as const, + name: 'check_coupon', + message: 'Invalid or expired coupon code', + field: 'coupon_code', + validatorUrl: '/api/coupons/validate', + timeout: 2000, + params: { checkExpiration: true, checkUsageLimit: true }, + }; + + expect(() => ValidationRuleSchema.parse(validation)).not.toThrow(); + }); + + it('should accept async validation with custom timeout', () => { + const validation = { + type: 'async' as const, + name: 'slow_api_check', + message: 'Validation failed', + field: 'data', + validatorUrl: 'https://slow-api.example.com/validate', + timeout: 15000, + }; + + const result = ValidationRuleSchema.parse(validation); + if (result.type === 'async') { + expect(result.timeout).toBe(15000); + } + }); + + it('should accept async validation with additional params', () => { + const validation = { + type: 'async' as const, + name: 'complex_check', + message: 'Complex validation failed', + field: 'complex_field', + validatorUrl: '/api/validate/complex', + params: { + threshold: 100, + mode: 'strict', + includeMetadata: true, + }, + }; + + expect(() => ValidationRuleSchema.parse(validation)).not.toThrow(); + }); + + it('should enforce required field property', () => { + const invalidValidation = { + type: 'async' as const, + name: 'invalid_async', + message: 'Missing field', + validatorUrl: '/api/validate', + }; + + expect(() => ValidationRuleSchema.parse(invalidValidation)).toThrow(); + }); }); describe('CustomValidatorSchema', () => { @@ -496,6 +830,206 @@ describe('ValidationRuleSchema (Discriminated Union)', () => { expect(() => ValidationRuleSchema.parse(validation)).not.toThrow(); }); + + it('should validate only if customer type is premium', () => { + const validation = { + type: 'conditional' as const, + name: 'premium_discount_check', + message: 'Premium customer validation', + when: 'customer_type = "premium"', + then: { + type: 'cross_field' as const, + name: 'premium_discount_limit', + message: 'Premium customers can have maximum 30% discount', + condition: 'discount_percent <= 30', + fields: ['discount_percent'], + }, + }; + + expect(() => ValidationRuleSchema.parse(validation)).not.toThrow(); + }); + + it('should validate shipping address only when shipping required', () => { + const validation = { + type: 'conditional' as const, + name: 'shipping_validation', + message: 'Shipping validation', + when: 'requires_shipping = true', + then: { + type: 'script' as const, + name: 'shipping_address_required', + message: 'Shipping address is required', + condition: 'shipping_address = null OR shipping_address = ""', + }, + }; + + expect(() => ValidationRuleSchema.parse(validation)).not.toThrow(); + }); + + it('should apply different validation based on order value', () => { + const validation = { + type: 'conditional' as const, + name: 'order_value_validation', + message: 'Order value validation', + when: 'order_total > 10000', + then: { + type: 'script' as const, + name: 'high_value_approval', + message: 'Orders over $10,000 require manager approval', + condition: 'manager_approval = null', + }, + otherwise: { + type: 'script' as const, + name: 'standard_validation', + message: 'Payment method required', + condition: 'payment_method = null', + }, + }; + + expect(() => ValidationRuleSchema.parse(validation)).not.toThrow(); + }); + + it('should validate tax fields only for taxable items', () => { + const validation = { + type: 'conditional' as const, + name: 'tax_validation', + message: 'Tax validation', + when: 'is_taxable = true', + then: { + type: 'script' as const, + name: 'tax_code_required', + message: 'Tax code is required for taxable items', + condition: 'tax_code = null', + }, + }; + + expect(() => ValidationRuleSchema.parse(validation)).not.toThrow(); + }); + + it('should conditionally require field based on another field value', () => { + const validation = { + type: 'conditional' as const, + name: 'conditional_required_field', + message: 'Conditional field requirement', + when: 'payment_method = "bank_transfer"', + then: { + type: 'script' as const, + name: 'bank_details_required', + message: 'Bank account details required for bank transfer', + condition: 'bank_account_number = null', + }, + }; + + expect(() => ValidationRuleSchema.parse(validation)).not.toThrow(); + }); + + it('should apply complex conditional with multiple field checks', () => { + const validation = { + type: 'conditional' as const, + name: 'subscription_validation', + message: 'Subscription validation', + when: 'subscription_type = "annual" AND customer_status = "active"', + then: { + type: 'cross_field' as const, + name: 'annual_discount', + message: 'Annual subscriptions get automatic 15% discount', + condition: 'discount_percent >= 15', + fields: ['discount_percent'], + }, + }; + + expect(() => ValidationRuleSchema.parse(validation)).not.toThrow(); + }); + + it('should validate insurance requirement based on product category', () => { + const validation = { + type: 'conditional' as const, + name: 'insurance_check', + message: 'Insurance validation', + when: 'product_category IN ("electronics", "jewelry", "artwork")', + then: { + type: 'script' as const, + name: 'insurance_required', + message: 'Insurance is required for high-value items', + condition: 'insurance_selected = false', + }, + }; + + expect(() => ValidationRuleSchema.parse(validation)).not.toThrow(); + }); + + it('should apply different validations for different regions', () => { + const validation = { + type: 'conditional' as const, + name: 'regional_validation', + message: 'Regional compliance validation', + when: 'region = "EU"', + then: { + type: 'script' as const, + name: 'gdpr_consent', + message: 'GDPR consent required for EU customers', + condition: 'gdpr_consent_given = false', + }, + otherwise: { + type: 'script' as const, + name: 'tos_acceptance', + message: 'Terms of Service acceptance required', + condition: 'tos_accepted = false', + }, + }; + + expect(() => ValidationRuleSchema.parse(validation)).not.toThrow(); + }); + + it('should apply validation based on user role', () => { + const validation = { + type: 'conditional' as const, + name: 'role_based_validation', + message: 'Role-based validation', + when: 'user_role = "manager"', + then: { + type: 'script' as const, + name: 'manager_approval_limit', + message: 'Managers can approve up to $50,000', + condition: 'approval_amount > 50000', + }, + }; + + expect(() => ValidationRuleSchema.parse(validation)).not.toThrow(); + }); + + it('should enforce required field property in when clause', () => { + const validation = { + type: 'conditional' as const, + name: 'valid_conditional', + message: 'Valid conditional', + when: 'status = "active"', + then: { + type: 'script' as const, + name: 'test_rule', + message: 'Test', + condition: 'true', + }, + }; + + expect(() => ValidationRuleSchema.parse(validation)).not.toThrow(); + }); + + it('should fail without when clause', () => { + const invalidValidation = { + type: 'conditional' as const, + name: 'invalid_conditional', + message: 'Missing when clause', + then: { + type: 'script' as const, + name: 'test_rule', + message: 'Test', + condition: 'true', + }, + }; + + expect(() => ValidationRuleSchema.parse(invalidValidation)).toThrow(); + }); }); describe('Advanced Validation Examples', () => { diff --git a/packages/spec/src/data/validation.zod.ts b/packages/spec/src/data/validation.zod.ts index bc9199c2e..a5f933f4e 100644 --- a/packages/spec/src/data/validation.zod.ts +++ b/packages/spec/src/data/validation.zod.ts @@ -1,7 +1,54 @@ import { z } from 'zod'; +/** + * # ObjectStack Validation Protocol + * + * This module defines the validation schema protocol for ObjectStack, providing a comprehensive + * type-safe validation system similar to Salesforce's validation rules but with enhanced capabilities. + * + * ## Overview + * + * Validation rules are applied at the data layer to ensure data integrity and enforce business logic. + * The system supports multiple validation types: + * + * 1. **Script Validation**: Formula-based validation using expressions + * 2. **Uniqueness Validation**: Enforce unique constraints across fields + * 3. **State Machine Validation**: Control allowed state transitions + * 4. **Format Validation**: Validate field formats (email, URL, regex, etc.) + * 5. **Cross-Field Validation**: Validate relationships between multiple fields + * 6. **Async Validation**: Remote validation via API calls + * 7. **Custom Validation**: User-defined validation functions + * 8. **Conditional Validation**: Apply validations based on conditions + * + * ## Salesforce Comparison + * + * ObjectStack validation rules are inspired by Salesforce validation rules but enhanced: + * - Salesforce: Formula-based validation with `Error Condition Formula` + * - ObjectStack: Multiple validation types with composable rules + * + * Example Salesforce validation rule: + * ``` + * Rule Name: Discount_Cannot_Exceed_40_Percent + * Error Condition Formula: Discount_Percent__c > 0.40 + * Error Message: Discount cannot exceed 40%. + * ``` + * + * Equivalent ObjectStack rule: + * ```typescript + * { + * type: 'script', + * name: 'discount_cannot_exceed_40_percent', + * condition: 'discount_percent > 0.40', + * message: 'Discount cannot exceed 40%', + * severity: 'error' + * } + * ``` + */ + /** * Base Validation Rule + * + * All validation rules extend from this base schema with common properties. */ const BaseValidationSchema = z.object({ name: z.string().regex(/^[a-z_][a-z0-9_]*$/).describe('Unique rule name'), @@ -54,6 +101,65 @@ export const FormatValidationSchema = BaseValidationSchema.extend({ /** * 5. Cross-Field Validation * Validates relationships between multiple fields. + * + * ## Use Cases + * - Date range validations (end_date > start_date) + * - Amount comparisons (discount < total) + * - Complex business rules involving multiple fields + * + * ## Salesforce Examples + * + * ### Example 1: Close Date Must Be In Current or Future Month + * **Salesforce Formula:** + * ``` + * MONTH(CloseDate) < MONTH(TODAY()) || + * YEAR(CloseDate) < YEAR(TODAY()) + * ``` + * + * **ObjectStack Equivalent:** + * ```typescript + * { + * type: 'cross_field', + * name: 'close_date_future', + * condition: 'MONTH(close_date) >= MONTH(TODAY()) AND YEAR(close_date) >= YEAR(TODAY())', + * fields: ['close_date'], + * message: 'Close Date must be in the current or a future month' + * } + * ``` + * + * ### Example 2: Discount Validation + * **Salesforce Formula:** + * ``` + * Discount__c > (Amount__c * 0.40) + * ``` + * + * **ObjectStack Equivalent:** + * ```typescript + * { + * type: 'cross_field', + * name: 'discount_limit', + * condition: 'discount > (amount * 0.40)', + * fields: ['discount', 'amount'], + * message: 'Discount cannot exceed 40% of the amount' + * } + * ``` + * + * ### Example 3: Opportunity Must Have Products + * **Salesforce Formula:** + * ``` + * ISBLANK(Products__c) && ISPICKVAL(StageName, "Closed Won") + * ``` + * + * **ObjectStack Equivalent:** + * ```typescript + * { + * type: 'cross_field', + * name: 'products_required_for_won', + * condition: 'products = null AND stage = "closed_won"', + * fields: ['products', 'stage'], + * message: 'Opportunity must have products to be marked as Closed Won' + * } + * ``` */ export const CrossFieldValidationSchema = BaseValidationSchema.extend({ type: z.literal('cross_field'), @@ -64,6 +170,124 @@ export const CrossFieldValidationSchema = BaseValidationSchema.extend({ /** * 6. Async Validation * Remote validation via API call or database query. + * + * ## Use Cases + * + * ### 1. Email Uniqueness Check + * Check if an email address is already registered in the system. + * ```typescript + * { + * type: 'async', + * name: 'unique_email', + * field: 'email', + * validatorUrl: '/api/users/check-email', + * message: 'This email address is already registered', + * debounce: 500, // Wait 500ms after user stops typing + * timeout: 3000 + * } + * ``` + * + * ### 2. Username Availability + * Verify username is available before form submission. + * ```typescript + * { + * type: 'async', + * name: 'username_available', + * field: 'username', + * validatorUrl: '/api/users/check-username', + * message: 'This username is already taken', + * debounce: 300, + * timeout: 2000 + * } + * ``` + * + * ### 3. Tax ID Validation + * Validate tax ID with government API (e.g., IRS, HMRC). + * ```typescript + * { + * type: 'async', + * name: 'validate_tax_id', + * field: 'tax_id', + * validatorFunction: 'validateTaxIdWithIRS', + * message: 'Invalid Tax ID number', + * timeout: 10000, // Government APIs may be slow + * params: { country: 'US', format: 'EIN' } + * } + * ``` + * + * ### 4. Credit Card Validation + * Verify credit card with payment gateway without charging. + * ```typescript + * { + * type: 'async', + * name: 'validate_card', + * field: 'card_number', + * validatorUrl: 'https://api.stripe.com/v1/tokens/validate', + * message: 'Invalid credit card number', + * timeout: 5000, + * params: { + * mode: 'validate_only', + * checkFunds: false + * } + * } + * ``` + * + * ### 5. Address Validation + * Validate and standardize addresses using geocoding services. + * ```typescript + * { + * type: 'async', + * name: 'validate_address', + * field: 'street_address', + * validatorFunction: 'validateAddressWithGoogleMaps', + * message: 'Unable to verify address', + * timeout: 4000, + * params: { + * includeFields: ['city', 'state', 'zip'], + * strictMode: true, + * country: 'US' + * } + * } + * ``` + * + * ### 6. Domain Name Availability + * Check if domain name is available for registration. + * ```typescript + * { + * type: 'async', + * name: 'domain_available', + * field: 'domain_name', + * validatorUrl: '/api/domains/check-availability', + * message: 'This domain is already taken or reserved', + * debounce: 500, + * timeout: 2000 + * } + * ``` + * + * ### 7. Coupon Code Validation + * Verify coupon code is valid and not expired. + * ```typescript + * { + * type: 'async', + * name: 'validate_coupon', + * field: 'coupon_code', + * validatorUrl: '/api/coupons/validate', + * message: 'Invalid or expired coupon code', + * timeout: 2000, + * params: { + * checkExpiration: true, + * checkUsageLimit: true, + * userId: '{{current_user_id}}' + * } + * } + * ``` + * + * ## Best Practices + * - Always set a reasonable `timeout` (default is 5000ms) + * - Use `debounce` for fields that are typed (300-500ms recommended) + * - Implement proper error handling on the server side + * - Cache validation results when appropriate + * - Consider rate limiting for external API calls */ export const AsyncValidationSchema = BaseValidationSchema.extend({ type: z.literal('async'), @@ -105,6 +329,178 @@ export const ValidationRuleSchema: z.ZodType = z.lazy(() => /** * 8. Conditional Validation * Validation that only applies when a condition is met. + * + * ## Overview + * Conditional validations follow the pattern: "Validate X only if Y is true" + * This allows for context-aware validation rules that adapt to different scenarios. + * + * ## Use Cases + * + * ### 1. Validate Based on Record Type + * Apply different validation rules based on the type of record. + * ```typescript + * { + * type: 'conditional', + * name: 'enterprise_approval_required', + * when: 'account_type = "enterprise"', + * message: 'Enterprise validation', + * then: { + * type: 'script', + * name: 'require_approval', + * message: 'Enterprise accounts require manager approval', + * condition: 'approval_status = null' + * } + * } + * ``` + * + * ### 2. Conditional Field Requirements + * Require certain fields only when specific conditions are met. + * ```typescript + * { + * type: 'conditional', + * name: 'shipping_address_when_required', + * when: 'requires_shipping = true', + * message: 'Shipping validation', + * then: { + * type: 'script', + * name: 'shipping_address_required', + * message: 'Shipping address is required for physical products', + * condition: 'shipping_address = null OR shipping_address = ""' + * } + * } + * ``` + * + * ### 3. Amount-Based Validation + * Apply different rules based on transaction amount. + * ```typescript + * { + * type: 'conditional', + * name: 'high_value_approval', + * when: 'order_total > 10000', + * message: 'High value order validation', + * then: { + * type: 'script', + * name: 'manager_approval_required', + * message: 'Orders over $10,000 require manager approval', + * condition: 'manager_approval_id = null' + * }, + * otherwise: { + * type: 'script', + * name: 'standard_validation', + * message: 'Payment method is required', + * condition: 'payment_method = null' + * } + * } + * ``` + * + * ### 4. Regional Compliance + * Apply region-specific validation rules. + * ```typescript + * { + * type: 'conditional', + * name: 'regional_compliance', + * when: 'region = "EU"', + * message: 'EU compliance validation', + * then: { + * type: 'script', + * name: 'gdpr_consent', + * message: 'GDPR consent is required for EU customers', + * condition: 'gdpr_consent_given = false' + * }, + * otherwise: { + * type: 'script', + * name: 'tos_acceptance', + * message: 'Terms of Service acceptance required', + * condition: 'tos_accepted = false' + * } + * } + * ``` + * + * ### 5. Nested Conditional Validation + * Create complex validation logic with nested conditions. + * ```typescript + * { + * type: 'conditional', + * name: 'country_state_validation', + * when: 'country = "US"', + * message: 'US-specific validation', + * then: { + * type: 'conditional', + * name: 'california_validation', + * when: 'state = "CA"', + * message: 'California-specific validation', + * then: { + * type: 'script', + * name: 'ca_tax_id_required', + * message: 'California requires a valid tax ID', + * condition: 'tax_id = null OR NOT(REGEX(tax_id, "^\\d{2}-\\d{7}$"))' + * } + * } + * } + * ``` + * + * ### 6. Tax Validation for Taxable Items + * Only validate tax fields when the item is taxable. + * ```typescript + * { + * type: 'conditional', + * name: 'tax_field_validation', + * when: 'is_taxable = true', + * message: 'Tax validation', + * then: { + * type: 'script', + * name: 'tax_code_required', + * message: 'Tax code is required for taxable items', + * condition: 'tax_code = null OR tax_code = ""' + * } + * } + * ``` + * + * ### 7. Role-Based Validation + * Apply validation based on user role. + * ```typescript + * { + * type: 'conditional', + * name: 'role_based_approval_limit', + * when: 'user_role = "manager"', + * message: 'Manager approval limits', + * then: { + * type: 'script', + * name: 'manager_limit', + * message: 'Managers can approve up to $50,000', + * condition: 'approval_amount > 50000' + * } + * } + * ``` + * + * ## Salesforce Pattern Comparison + * + * Salesforce doesn't have explicit "conditional validation" rules but achieves similar + * behavior using formula logic. ObjectStack makes this pattern explicit and composable. + * + * **Salesforce Approach:** + * ``` + * IF( + * ISPICKVAL(Type, "Enterprise"), + * AND(Amount > 100000, ISBLANK(Approval__c)), + * FALSE + * ) + * ``` + * + * **ObjectStack Approach:** + * ```typescript + * { + * type: 'conditional', + * name: 'enterprise_high_value', + * when: 'type = "enterprise"', + * then: { + * type: 'cross_field', + * name: 'amount_approval', + * condition: 'amount > 100000 AND approval = null', + * fields: ['amount', 'approval'] + * } + * } + * ``` */ export const ConditionalValidationSchema = BaseValidationSchema.extend({ type: z.literal('conditional'),