/* * Procurement Approval Hub - vendors, contracts, purchase requests, approvals, and invoices. * * pa_vendor__c <- vendor profile and risk tier * pa_contract__c <- vendor agreement and spend ceiling * pa_purchase_request__c <- request tied to a vendor and optional contract * pa_approval_step__c <- approval routing line for a request * pa_invoice__c <- invoice received against a contract * * Run: * yarn tsn examples/manifest/procurement-approval-hub/procurement-approval-hub.ts \ * > examples/manifest/procurement-approval-hub/manifest.json */ import { Category, CustomObject, CustomObjectFieldSection, CustomObjectPageLayout, DateField, EmailField, FormulaField, ListViewDef, LongTextField, LookupField, ManifestBuilder, ManifestFunction, NumberField, ParentChildField, Rule, RadioField, SummaryField, TextField, Validation, Workflow, } from '@rippling/rippling-sdk/lib/manifest'; const manifest = new ManifestBuilder({ key: 'procurement_approval_hub', name: 'Procurement Approval Hub', description: 'Vendor contracts, purchase requests, approval routing, and invoice tracking', }); const category = new Category(manifest, { apiName: 'procurement__c', name: 'Procurement', description: 'Procurement workflow and vendor governance records', }); const vendorObj = new CustomObject(manifest, { apiName: 'pa_vendor__c', name: 'Vendor', pluralLabel: 'Vendors', category, description: 'Vendor profile, risk tier, and compliance owner', }); const vendorSection = new CustomObjectFieldSection(vendorObj, { name: 'Vendor', sectionId: 'sec_pa_vendor', }); const vendorLegalNameField = new TextField(vendorObj, { apiName: 'legal_name__c', displayName: 'Legal name', description: 'Registered legal vendor name', required: true, maxLength: 200, section: vendorSection, }); const vendorCategoryField = new RadioField(vendorObj, { apiName: 'vendor_category__c', displayName: 'Vendor category', description: 'Primary spend category', options: ['Software', 'Facilities', 'Professional services', 'Marketing', 'Hardware', 'Other'], required: true, section: vendorSection, }); const vendorRiskField = new RadioField(vendorObj, { apiName: 'risk_tier__c', displayName: 'Risk tier', description: 'Risk tier from diligence review', options: ['Low', 'Medium', 'High', 'Critical'], required: true, section: vendorSection, }); const vendorContactEmailField = new EmailField(vendorObj, { apiName: 'contact_email__c', displayName: 'Contact email', description: 'Primary vendor contact email', section: vendorSection, }); const contractObj = new CustomObject(manifest, { apiName: 'pa_contract__c', name: 'Contract', pluralLabel: 'Contracts', category, description: 'Vendor contract with dates, status, and spend ceiling', }); const contractSection = new CustomObjectFieldSection(contractObj, { name: 'Contract', sectionId: 'sec_pa_contract', }); const contractVendorRef = new ParentChildField(contractObj, { apiName: 'vendor_ref__c', displayName: 'Vendor', target: vendorObj, relatedDataLabel: 'Contracts', parentChildType: 'PRIMARY', enableParentOwnerInheritance: false, description: 'Vendor this contract belongs to', section: contractSection, }); const contractStatusField = new RadioField(contractObj, { apiName: 'status__c', displayName: 'Status', description: 'Contract lifecycle status', options: ['Draft', 'In review', 'Active', 'Expired', 'Terminated'], required: true, section: contractSection, }); const contractStartDateField = new DateField(contractObj, { apiName: 'start_date__c', displayName: 'Start date', description: 'Contract effective date', section: contractSection, }); const contractEndDateField = new DateField(contractObj, { apiName: 'end_date__c', displayName: 'End date', description: 'Contract expiration date', section: contractSection, }); const contractCeilingCentsField = new NumberField(contractObj, { apiName: 'spend_ceiling_cents__c', displayName: 'Spend ceiling (cents)', description: 'Approved maximum spend in cents', decimalPlaces: 0, section: contractSection, }); const contractTermsField = new LongTextField(contractObj, { apiName: 'key_terms__c', displayName: 'Key terms', description: 'Renewal, data processing, and termination notes', maxLength: 6000, section: contractSection, }); const requestObj = new CustomObject(manifest, { apiName: 'pa_purchase_request__c', name: 'Purchase request', pluralLabel: 'Purchase requests', category, description: 'Purchase request with business justification and approval state', }); const requestSection = new CustomObjectFieldSection(requestObj, { name: 'Purchase request', sectionId: 'sec_pa_request', }); const requestVendorRef = new LookupField(requestObj, { apiName: 'vendor_ref__c', displayName: 'Vendor', target: vendorObj, relatedDataLabel: 'Purchase requests', description: 'Preferred or selected vendor', required: true, section: requestSection, }); const requestContractRef = new LookupField(requestObj, { apiName: 'contract_ref__c', displayName: 'Contract', target: contractObj, relatedDataLabel: 'Purchase requests', description: 'Contract used for this request, if any', section: requestSection, }); const requestAmountCentsField = new NumberField(requestObj, { apiName: 'amount_cents__c', displayName: 'Amount (cents)', description: 'Requested spend amount in cents', decimalPlaces: 0, required: true, section: requestSection, }); const requestStatusField = new RadioField(requestObj, { apiName: 'status__c', displayName: 'Status', description: 'Approval status', options: ['Draft', 'Submitted', 'In approval', 'Approved', 'Rejected', 'Ordered'], required: true, section: requestSection, }); const requestNeedByField = new DateField(requestObj, { apiName: 'need_by__c', displayName: 'Need by', description: 'Requested delivery or purchase date', section: requestSection, }); const requestJustificationField = new LongTextField(requestObj, { apiName: 'business_justification__c', displayName: 'Business justification', description: 'Why the purchase is needed', maxLength: 6000, section: requestSection, }); const approvalObj = new CustomObject(manifest, { apiName: 'pa_approval_step__c', name: 'Approval step', pluralLabel: 'Approval steps', category, description: 'Approval routing line tied to a purchase request', }); const approvalSection = new CustomObjectFieldSection(approvalObj, { name: 'Approval step', sectionId: 'sec_pa_approval', }); const approvalRequestRef = new ParentChildField(approvalObj, { apiName: 'purchase_request_ref__c', displayName: 'Purchase request', target: requestObj, relatedDataLabel: 'Approval steps', parentChildType: 'PRIMARY', enableParentOwnerInheritance: true, description: 'Purchase request this approval belongs to', section: approvalSection, }); const approvalSequenceField = new NumberField(approvalObj, { apiName: 'sequence__c', displayName: 'Sequence', description: 'Approval sequence number', decimalPlaces: 0, required: true, section: approvalSection, }); const approvalRoleField = new RadioField(approvalObj, { apiName: 'approval_role__c', displayName: 'Approval role', description: 'Approval lane', options: ['Budget owner', 'Finance', 'Security', 'Legal', 'Procurement'], required: true, section: approvalSection, }); const approvalStatusField = new RadioField(approvalObj, { apiName: 'status__c', displayName: 'Status', description: 'Approval step status', options: ['Pending', 'Approved', 'Rejected', 'Skipped'], required: true, section: approvalSection, }); const invoiceObj = new CustomObject(manifest, { apiName: 'pa_invoice__c', name: 'Invoice', pluralLabel: 'Invoices', category, description: 'Invoice received against a vendor contract', }); const invoiceSection = new CustomObjectFieldSection(invoiceObj, { name: 'Invoice', sectionId: 'sec_pa_invoice', }); const invoiceContractRef = new ParentChildField(invoiceObj, { apiName: 'contract_ref__c', displayName: 'Contract', target: contractObj, relatedDataLabel: 'Invoices', parentChildType: 'PRIMARY', enableParentOwnerInheritance: false, description: 'Contract this invoice is billed against', section: invoiceSection, }); const invoiceNumberField = new TextField(invoiceObj, { apiName: 'invoice_number__c', displayName: 'Invoice number', description: 'Vendor invoice number', maxLength: 120, required: true, section: invoiceSection, }); const invoiceAmountCentsField = new NumberField(invoiceObj, { apiName: 'amount_cents__c', displayName: 'Amount (cents)', description: 'Invoice amount in cents', decimalPlaces: 0, required: true, section: invoiceSection, }); const invoiceStatusField = new RadioField(invoiceObj, { apiName: 'status__c', displayName: 'Status', description: 'Invoice processing status', options: ['Received', 'Matched', 'Approved', 'Paid', 'Exception'], required: true, section: invoiceSection, }); const contractCountOnVendor = new SummaryField(vendorObj, { apiName: 'contract_count__c', displayName: 'Contracts', aggregates: contractStatusField, using: 'COUNT', lookup: contractVendorRef, description: 'Contracts for this vendor', section: vendorSection, }); const approvalCountOnRequest = new SummaryField(requestObj, { apiName: 'approval_step_count__c', displayName: 'Approval steps', aggregates: approvalRoleField, using: 'COUNT', lookup: approvalRequestRef, description: 'Approval steps attached to this request', section: requestSection, }); const invoicedAmountOnContract = new SummaryField(contractObj, { apiName: 'invoiced_amount_cents__c', displayName: 'Invoiced amount (cents)', aggregates: invoiceAmountCentsField, using: 'SUM', lookup: invoiceContractRef, description: 'Total invoice amount billed against this contract', section: contractSection, }); const requestLabelField = new FormulaField(requestObj, { apiName: 'approval_label__c', displayName: 'Approval label', description: 'Status and amount for approval queues', fieldType: 'TEXT', maxLength: 240, formula: "CONCATENATE(status__c, ' - ', amount_cents__c)", section: requestSection, }); new Validation(contractObj, { ruleId: 'pa_contract_end_after_start', displayName: 'Contract end must be after start', rqlFormula: '(end_date__c <= start_date__c)', errorMessage: 'Contract end date must be after start date.', ruleDescription: 'Prevents inverted contract terms', state: 'ACTIVE', level: 'FIELD', fieldRef: contractEndDateField, }); new Validation(requestObj, { ruleId: 'pa_request_amount_positive', displayName: 'Request amount must be positive', rqlFormula: '(amount_cents__c <= 0)', errorMessage: 'Amount must be greater than zero.', ruleDescription: 'Blocks zero-value purchase requests', state: 'ACTIVE', level: 'FIELD', fieldRef: requestAmountCentsField, }); new Rule(requestObj, { flowId: 'pa_default_request_status', flowName: 'Default purchase request status', triggerType: 'CREATE', description: 'New purchase requests start as Draft', actions: [{ targetField: requestStatusField, fixedValue: 'Draft' }], }); new Rule(approvalObj, { flowId: 'pa_default_approval_status', flowName: 'Default approval step status', triggerType: 'CREATE', description: 'New approval steps start Pending', actions: [{ targetField: approvalStatusField, fixedValue: 'Pending' }], }); new ListViewDef(vendorObj, { viewId: 'view_pa_vendors', name: 'Vendors', description: 'Vendor directory with risk and contract count', fields: [ 'name', vendorLegalNameField, vendorCategoryField, vendorRiskField, vendorContactEmailField, contractCountOnVendor, ], orderBy: vendorLegalNameField, sortOrder: 'ASC', isPublic: true, objectScope: 'all', }); new ListViewDef(contractObj, { viewId: 'view_pa_contracts', name: 'Contracts', description: 'Contract inventory with dates and invoiced amount', fields: [ 'name', contractVendorRef, contractStatusField, contractStartDateField, contractEndDateField, contractCeilingCentsField, invoicedAmountOnContract, ], orderBy: contractEndDateField, sortOrder: 'ASC', isPublic: true, objectScope: 'all', }); new ListViewDef(requestObj, { viewId: 'view_pa_purchase_requests', name: 'Purchase requests', description: 'Approval queue for purchase requests', fields: [ 'name', requestVendorRef, requestContractRef, requestAmountCentsField, requestStatusField, requestNeedByField, approvalCountOnRequest, requestLabelField, ], orderBy: requestNeedByField, sortOrder: 'ASC', isPublic: true, objectScope: 'all', }); new ListViewDef(approvalObj, { viewId: 'view_pa_approval_steps', name: 'Approval steps', description: 'Approval routing lines by request', fields: ['name', approvalRequestRef, approvalSequenceField, approvalRoleField, approvalStatusField], orderBy: approvalSequenceField, sortOrder: 'ASC', isPublic: true, objectScope: 'all', }); new ListViewDef(invoiceObj, { viewId: 'view_pa_invoices', name: 'Invoices', description: 'Invoices received against contracts', fields: ['name', invoiceContractRef, invoiceNumberField, invoiceAmountCentsField, invoiceStatusField], orderBy: invoiceContractRef, sortOrder: 'ASC', isPublic: true, objectScope: 'all', }); CustomObjectPageLayout.basic(vendorObj, { section: vendorSection, fields: [ 'name', vendorLegalNameField, vendorCategoryField, vendorRiskField, vendorContactEmailField, contractCountOnVendor, ], }); CustomObjectPageLayout.basic(contractObj, { section: contractSection, fields: [ 'name', contractVendorRef, contractStatusField, contractStartDateField, contractEndDateField, contractCeilingCentsField, invoicedAmountOnContract, contractTermsField, ], }); CustomObjectPageLayout.basic(requestObj, { section: requestSection, fields: [ 'name', requestVendorRef, requestContractRef, requestAmountCentsField, requestStatusField, requestNeedByField, approvalCountOnRequest, requestLabelField, requestJustificationField, ], }); CustomObjectPageLayout.basic(approvalObj, { section: approvalSection, fields: ['name', approvalRequestRef, approvalSequenceField, approvalRoleField, approvalStatusField], }); CustomObjectPageLayout.basic(invoiceObj, { section: invoiceSection, fields: ['name', invoiceContractRef, invoiceNumberField, invoiceAmountCentsField, invoiceStatusField], }); const routeApprovalFunction = new ManifestFunction(manifest, { apiName: 'pa_route_purchase_request_fn', name: 'Route purchase request approvals', description: 'Function invoked by workflow when a purchase request is submitted', }); new Workflow(manifest, { definitionId: 'pa_purchase_request_submitted_workflow', title: 'Purchase request submitted', description: 'Routes purchase requests into approval steps when request records are created', function: routeApprovalFunction, trigger: { automation_event_type: 'AUTOMATION_EVENT_TYPE_RECORD_CHANGE', og_module: 'custom_objects', base_model: requestObj.getApiName(), record_change_operation_type: 'RECORD_CHANGE_OPERATION_TYPE_CREATE', }, }); console.log(manifest.preview());