From 8d0184ac42d16b3d31f9724759d3d69ac5087bbe Mon Sep 17 00:00:00 2001 From: Andrew Fawcett Date: Tue, 25 Aug 2026 10:51:07 +0100 Subject: [PATCH 01/12] Refresh sample app --- .gitignore | 1 + README.md | 67 ++++- config/project-scratch-def.json | 10 + sfdx-project.json | 2 +- .../OpportunityOperations.agent | 131 +++++++++ .../OpportunityOperations.bundle-meta.xml | 5 + ...exEnterprisePatternsSampleApp.app-meta.xml | 18 +- .../main/classes/Application.cls | 83 ------ .../main/classes/InvoicingTargetsRegistry.cls | 114 ++++++++ ... => InvoicingTargetsRegistry.cls-meta.xml} | 2 +- .../{domains/IAccounts.cls => UnitOfWork.cls} | 40 ++- ...n.cls-meta.xml => UnitOfWork.cls-meta.xml} | 2 +- .../main/classes/actions/ApplyDiscount.cls | 88 ++++++ .../ApplyDiscount.cls-meta.xml} | 2 +- .../main/classes/actions/CreateInvoice.cls | 99 +++++++ .../CreateInvoice.cls-meta.xml} | 2 +- .../classes/batchjobs/CreateInvoicesJob.cls | 59 ++-- .../batchjobs/CreateInvoicesJob.cls-meta.xml | 2 +- .../OpportunityApplyDiscountController.cls | 70 ++--- ...tunityApplyDiscountController.cls-meta.xml | 2 +- ...> OpportunityApplyDiscountsController.cls} | 44 ++- ...unityApplyDiscountsController.cls-meta.xml | 5 + .../OpportunityCreateInvoiceController.cls | 80 ++--- ...tunityCreateInvoiceController.cls-meta.xml | 2 +- .../OpportunityCreateInvoicesController.cls | 64 ++++ ...unityCreateInvoicesController.cls-meta.xml | 5 + .../OpportunityInvoicingController.cls | 46 ++- ...pportunityInvoicingController.cls-meta.xml | 2 +- .../QuickOpportunityWizardController.cls | 261 +++++++++++------ ...ckOpportunityWizardController.cls-meta.xml | 2 +- .../classes/domains/AbstractChargeable.cls | 59 ++-- .../domains/AbstractChargeable.cls-meta.xml | 2 +- .../main/classes/domains/Accounts.cls | 60 ++-- .../classes/domains/Accounts.cls-meta.xml | 2 +- .../classes/domains/DeveloperWorkItems.cls | 103 +++++-- .../domains/DeveloperWorkItems.cls-meta.xml | 2 +- .../classes/domains/IDeveloperWorkItems.cls | 3 - .../domains/IDeveloperWorkItems.cls-meta.xml | 5 - .../main/classes/domains/IOpportunities.cls | 31 -- .../domains/IOpportunities.cls-meta.xml | 5 - .../classes/domains/IOpportunityLineItems.cls | 30 -- .../IOpportunityLineItems.cls-meta.xml | 5 - .../main/classes/domains/Opportunities.cls | 114 +++++--- .../domains/Opportunities.cls-meta.xml | 2 +- .../classes/domains/OpportunityLineItems.cls | 73 ++--- .../domains/OpportunityLineItems.cls-meta.xml | 2 +- .../classes/domains/TrainingWorkItems.cls | 110 +++++++ .../domains/TrainingWorkItems.cls-meta.xml | 5 + .../restapis/OpportunitiesResource.cls | 24 +- .../OpportunitiesResource.cls-meta.xml | 2 +- .../classes/selectors/AccountsSelector.cls | 52 +++- .../selectors/AccountsSelector.cls-meta.xml | 2 +- .../selectors/DeveloperWorkItemsSelector.cls | 92 ++++++ .../DeveloperWorkItemsSelector.cls-meta.xml | 5 + .../selectors/IAccountsSelector.cls-meta.xml | 5 - .../selectors/IOpportunitiesSelector.cls | 33 --- .../IOpportunitiesSelector.cls-meta.xml | 5 - .../IOpportunityLineItemsSelector.cls | 4 - ...IOpportunityLineItemsSelector.cls-meta.xml | 5 - .../selectors/IPricebookEntriesSelector.cls | 4 - .../IPricebookEntriesSelector.cls-meta.xml | 5 - .../classes/selectors/IPricebooksSelector.cls | 4 - .../IPricebooksSelector.cls-meta.xml | 5 - .../classes/selectors/IProductsSelector.cls | 4 - .../selectors/IProductsSelector.cls-meta.xml | 5 - .../main/classes/selectors/IUsersSelector.cls | 4 - .../selectors/IUsersSelector.cls-meta.xml | 5 - .../selectors/OpportunitiesSelector.cls | 136 ++++++--- .../OpportunitiesSelector.cls-meta.xml | 2 +- .../classes/selectors/OpportunityInfo.cls | 17 -- .../selectors/OpportunityInfo.cls-meta.xml | 5 - .../OpportunityLineItemsSelector.cls | 74 +++-- .../OpportunityLineItemsSelector.cls-meta.xml | 2 +- .../OpportunitySummary.cls} | 50 +++- .../selectors/OpportunitySummary.cls-meta.xml | 5 + .../selectors/PricebookEntriesSelector.cls | 37 ++- .../PricebookEntriesSelector.cls-meta.xml | 2 +- .../classes/selectors/PricebooksSelector.cls | 39 ++- .../selectors/PricebooksSelector.cls-meta.xml | 2 +- .../classes/selectors/ProductsSelector.cls | 79 ++++- .../selectors/ProductsSelector.cls-meta.xml | 2 +- .../selectors/TrainingWorkItemsSelector.cls | 91 ++++++ .../TrainingWorkItemsSelector.cls-meta.xml | 5 + .../main/classes/selectors/UsersSelector.cls | 49 +++- .../selectors/UsersSelector.cls-meta.xml | 2 +- .../main/classes/service/AccountsService.cls | 56 +++- .../service/AccountsService.cls-meta.xml | 2 +- .../classes/service/AccountsServiceImpl.cls | 44 --- .../service/AccountsServiceImpl.cls-meta.xml | 5 - .../main/classes/service/IAccountsService.cls | 29 -- .../service/IAccountsService.cls-meta.xml | 5 - .../classes/service/IInvoicingService.cls | 30 -- .../service/IInvoicingService.cls-meta.xml | 5 - .../IOpportunitiesService.cls-meta.xml | 5 - .../main/classes/service/InvoicingService.cls | 160 +++++++--- .../service/InvoicingService.cls-meta.xml | 2 +- .../service/InvoicingServiceImpl.cls-meta.xml | 5 - .../classes/service/OpportunitiesService.cls | 155 +++++++++- .../service/OpportunitiesService.cls-meta.xml | 2 +- .../service/OpportunitiesServiceImpl.cls | 114 -------- .../OpportunitiesServiceImpl.cls-meta.xml | 5 - .../OpportunitiesTriggerHandler.cls | 63 ++-- .../OpportunitiesTriggerHandler.cls-meta.xml | 2 +- ...voiceTargets.DeveloperWorkItem.md-meta.xml | 17 ++ .../InvoiceTargets.Opportunity.md-meta.xml | 17 ++ ...nvoiceTargets.TrainingWorkItem.md-meta.xml | 17 ++ .../Account-Account Layout.layout-meta.xml | 273 ++++++++++++++++++ ...mdt-Invoice Targets Layout.layout-meta.xml | 76 +++++ ...rtunity-Opportunity Layout.layout-meta.xml | 272 +++++++++++++++++ ...Order__c-Work Order Layout.layout-meta.xml | 4 + .../main/lwc/jsconfig.json | 20 ++ .../opportunityApplyDiscount.test.js | 81 ++++++ .../opportunityApplyDiscount.html | 39 +++ .../opportunityApplyDiscount.js | 69 +++++ .../opportunityApplyDiscount.js-meta.xml | 14 + .../opportunityCreateInvoice.test.js | 54 ++++ .../opportunityCreateInvoice.html | 39 +++ .../opportunityCreateInvoice.js | 76 +++++ .../opportunityCreateInvoice.js-meta.xml | 14 + .../__tests__/opportunityInvoiceJob.test.js | 54 ++++ .../opportunityInvoiceJob.html | 3 + .../opportunityInvoiceJob.js | 50 ++++ .../opportunityInvoiceJob.js-meta.xml | 13 + .../__tests__/quickOpportunityWizard.test.js | 46 +++ .../quickOpportunityWizard.html | 65 +++++ .../quickOpportunityWizard.js | 157 ++++++++++ .../quickOpportunityWizard.js-meta.xml | 14 + .../QuickOpportunity.webLink-meta.xml | 12 - .../InvoiceTargets__mdt.object-meta.xml | 7 + .../fields/DomainClass__c.field-meta.xml | 12 + .../fields/SObjectType__c.field-meta.xml | 13 + .../fields/SelectorClass__c.field-meta.xml | 12 + .../Opportunity/Opportunity.object-meta.xml | 7 + .../webLinks/ApplyDiscount.webLink-meta.xml | 12 - .../webLinks/BatchInvoicing.webLink-meta.xml | 13 - .../webLinks/CreateInvoice.webLink-meta.xml | 12 - .../fields/Account__c.field-meta.xml | 14 + .../main/pages/opportunityapplydiscount.page | 22 -- .../opportunityapplydiscount.page-meta.xml | 7 - .../main/pages/opportunityapplydiscounts.page | 4 +- .../opportunityapplydiscounts.page-meta.xml | 4 +- .../main/pages/opportunitycreateinvoice.page | 22 -- .../opportunitycreateinvoice.page-meta.xml | 7 - .../main/pages/opportunitycreateinvoices.page | 4 +- .../opportunitycreateinvoices.page-meta.xml | 4 +- .../main/pages/opportunityinvoicing.page | 7 - .../pages/opportunityinvoicing.page-meta.xml | 7 - .../main/pages/quickopportunitywizard.page | 51 ---- .../quickopportunitywizard.page-meta.xml | 7 - ...sePatternsSampleApp.permissionset-meta.xml | 81 +++++- ...ount.QuickOpportunity.quickAction-meta.xml | 8 + ...rtunity.ApplyDiscount.quickAction-meta.xml | 8 + ...tunity.BatchInvoicing.quickAction-meta.xml | 8 + ...rtunity.CreateInvoice.quickAction-meta.xml | 8 + .../main/triggers/DeveloperWorkItems.trigger | 7 +- .../DeveloperWorkItems.trigger-meta.xml | 2 +- .../main/triggers/Opportunities.trigger | 7 +- .../triggers/Opportunities.trigger-meta.xml | 2 +- .../test/classes/TestDataFactory.cls | 60 +++- .../test/classes/TestDataFactory.cls-meta.xml | 2 +- .../classes/TestIdGenerator.cls} | 22 +- .../test/classes/TestIdGenerator.cls-meta.xml | 5 + .../classes/actions/ApplyDiscountTest.cls | 57 ++++ .../actions/ApplyDiscountTest.cls-meta.xml | 5 + .../classes/actions/CreateInvoiceTest.cls} | 73 +++-- .../actions/CreateInvoiceTest.cls-meta.xml | 5 + ...OpportunityApplyDiscountControllerTest.cls | 36 ++- ...tyApplyDiscountControllerTest.cls-meta.xml | 2 +- ...OpportunityCreateInvoiceControllerTest.cls | 55 ++++ ...tyCreateInvoiceControllerTest.cls-meta.xml | 5 + .../OpportunityInvoicingControllerTest.cls} | 29 +- ...tunityInvoicingControllerTest.cls-meta.xml | 5 + .../QuickOpportunityWizardControllerTest.cls | 83 ++++++ ...portunityWizardControllerTest.cls-meta.xml | 5 + .../test/classes/domains/AccountsTest.cls | 36 ++- .../classes/domains/AccountsTest.cls-meta.xml | 2 +- .../domains/DeveloperWorkItemsTest.cls | 43 ++- .../DeveloperWorkItemsTest.cls-meta.xml | 2 +- .../classes/domains/OpportunitiesTest.cls | 107 ++++--- .../domains/OpportunitiesTest.cls-meta.xml | 2 +- .../selectors/ProductsSelectorTest.cls | 77 +++-- .../ProductsSelectorTest.cls-meta.xml | 2 +- .../classes/service/AccountsServiceTest.cls | 83 +++--- .../service/AccountsServiceTest.cls-meta.xml | 2 +- .../classes/service/InvoicingServiceTest.cls | 177 ++++++++++-- .../service/InvoicingServiceTest.cls-meta.xml | 2 +- .../OpportunitiesServiceIntegrationTest.cls | 119 ++++---- ...unitiesServiceIntegrationTest.cls-meta.xml | 2 +- .../service/OpportunitiesServiceTest.cls | 58 ++-- .../OpportunitiesServiceTest.cls-meta.xml | 2 +- .../OpportunitiesTriggerHandlerTest.cls | 127 +++++--- ...portunitiesTriggerHandlerTest.cls-meta.xml | 2 +- 192 files changed, 4842 insertions(+), 1803 deletions(-) create mode 100644 sfdx-source/apex-common-samplecode/main/aiAuthoringBundles/OpportunityOperations/OpportunityOperations.agent create mode 100644 sfdx-source/apex-common-samplecode/main/aiAuthoringBundles/OpportunityOperations/OpportunityOperations.bundle-meta.xml delete mode 100644 sfdx-source/apex-common-samplecode/main/classes/Application.cls create mode 100644 sfdx-source/apex-common-samplecode/main/classes/InvoicingTargetsRegistry.cls rename sfdx-source/apex-common-samplecode/main/classes/{domains/IAccounts.cls-meta.xml => InvoicingTargetsRegistry.cls-meta.xml} (80%) rename sfdx-source/apex-common-samplecode/main/classes/{domains/IAccounts.cls => UnitOfWork.cls} (63%) rename sfdx-source/apex-common-samplecode/main/classes/{Application.cls-meta.xml => UnitOfWork.cls-meta.xml} (80%) create mode 100644 sfdx-source/apex-common-samplecode/main/classes/actions/ApplyDiscount.cls rename sfdx-source/apex-common-samplecode/main/classes/{domains/IChargeable.cls-meta.xml => actions/ApplyDiscount.cls-meta.xml} (80%) create mode 100644 sfdx-source/apex-common-samplecode/main/classes/actions/CreateInvoice.cls rename sfdx-source/apex-common-samplecode/main/classes/{controllers/OpportunityConsoleController.cls-meta.xml => actions/CreateInvoice.cls-meta.xml} (80%) rename sfdx-source/apex-common-samplecode/main/classes/controllers/{OpportunityConsoleController.cls => OpportunityApplyDiscountsController.cls} (55%) create mode 100644 sfdx-source/apex-common-samplecode/main/classes/controllers/OpportunityApplyDiscountsController.cls-meta.xml create mode 100644 sfdx-source/apex-common-samplecode/main/classes/controllers/OpportunityCreateInvoicesController.cls create mode 100644 sfdx-source/apex-common-samplecode/main/classes/controllers/OpportunityCreateInvoicesController.cls-meta.xml delete mode 100755 sfdx-source/apex-common-samplecode/main/classes/domains/IDeveloperWorkItems.cls delete mode 100755 sfdx-source/apex-common-samplecode/main/classes/domains/IDeveloperWorkItems.cls-meta.xml delete mode 100644 sfdx-source/apex-common-samplecode/main/classes/domains/IOpportunities.cls delete mode 100644 sfdx-source/apex-common-samplecode/main/classes/domains/IOpportunities.cls-meta.xml delete mode 100644 sfdx-source/apex-common-samplecode/main/classes/domains/IOpportunityLineItems.cls delete mode 100644 sfdx-source/apex-common-samplecode/main/classes/domains/IOpportunityLineItems.cls-meta.xml create mode 100644 sfdx-source/apex-common-samplecode/main/classes/domains/TrainingWorkItems.cls create mode 100644 sfdx-source/apex-common-samplecode/main/classes/domains/TrainingWorkItems.cls-meta.xml create mode 100644 sfdx-source/apex-common-samplecode/main/classes/selectors/DeveloperWorkItemsSelector.cls create mode 100644 sfdx-source/apex-common-samplecode/main/classes/selectors/DeveloperWorkItemsSelector.cls-meta.xml delete mode 100644 sfdx-source/apex-common-samplecode/main/classes/selectors/IAccountsSelector.cls-meta.xml delete mode 100644 sfdx-source/apex-common-samplecode/main/classes/selectors/IOpportunitiesSelector.cls delete mode 100644 sfdx-source/apex-common-samplecode/main/classes/selectors/IOpportunitiesSelector.cls-meta.xml delete mode 100755 sfdx-source/apex-common-samplecode/main/classes/selectors/IOpportunityLineItemsSelector.cls delete mode 100755 sfdx-source/apex-common-samplecode/main/classes/selectors/IOpportunityLineItemsSelector.cls-meta.xml delete mode 100755 sfdx-source/apex-common-samplecode/main/classes/selectors/IPricebookEntriesSelector.cls delete mode 100755 sfdx-source/apex-common-samplecode/main/classes/selectors/IPricebookEntriesSelector.cls-meta.xml delete mode 100755 sfdx-source/apex-common-samplecode/main/classes/selectors/IPricebooksSelector.cls delete mode 100755 sfdx-source/apex-common-samplecode/main/classes/selectors/IPricebooksSelector.cls-meta.xml delete mode 100755 sfdx-source/apex-common-samplecode/main/classes/selectors/IProductsSelector.cls delete mode 100755 sfdx-source/apex-common-samplecode/main/classes/selectors/IProductsSelector.cls-meta.xml delete mode 100755 sfdx-source/apex-common-samplecode/main/classes/selectors/IUsersSelector.cls delete mode 100755 sfdx-source/apex-common-samplecode/main/classes/selectors/IUsersSelector.cls-meta.xml delete mode 100644 sfdx-source/apex-common-samplecode/main/classes/selectors/OpportunityInfo.cls delete mode 100644 sfdx-source/apex-common-samplecode/main/classes/selectors/OpportunityInfo.cls-meta.xml rename sfdx-source/apex-common-samplecode/main/classes/{domains/IChargeable.cls => selectors/OpportunitySummary.cls} (58%) create mode 100644 sfdx-source/apex-common-samplecode/main/classes/selectors/OpportunitySummary.cls-meta.xml create mode 100644 sfdx-source/apex-common-samplecode/main/classes/selectors/TrainingWorkItemsSelector.cls create mode 100644 sfdx-source/apex-common-samplecode/main/classes/selectors/TrainingWorkItemsSelector.cls-meta.xml delete mode 100644 sfdx-source/apex-common-samplecode/main/classes/service/AccountsServiceImpl.cls delete mode 100644 sfdx-source/apex-common-samplecode/main/classes/service/AccountsServiceImpl.cls-meta.xml delete mode 100644 sfdx-source/apex-common-samplecode/main/classes/service/IAccountsService.cls delete mode 100644 sfdx-source/apex-common-samplecode/main/classes/service/IAccountsService.cls-meta.xml delete mode 100644 sfdx-source/apex-common-samplecode/main/classes/service/IInvoicingService.cls delete mode 100644 sfdx-source/apex-common-samplecode/main/classes/service/IInvoicingService.cls-meta.xml delete mode 100644 sfdx-source/apex-common-samplecode/main/classes/service/IOpportunitiesService.cls-meta.xml delete mode 100644 sfdx-source/apex-common-samplecode/main/classes/service/InvoicingServiceImpl.cls-meta.xml delete mode 100644 sfdx-source/apex-common-samplecode/main/classes/service/OpportunitiesServiceImpl.cls delete mode 100644 sfdx-source/apex-common-samplecode/main/classes/service/OpportunitiesServiceImpl.cls-meta.xml create mode 100644 sfdx-source/apex-common-samplecode/main/customMetadata/InvoiceTargets.DeveloperWorkItem.md-meta.xml create mode 100644 sfdx-source/apex-common-samplecode/main/customMetadata/InvoiceTargets.Opportunity.md-meta.xml create mode 100644 sfdx-source/apex-common-samplecode/main/customMetadata/InvoiceTargets.TrainingWorkItem.md-meta.xml create mode 100644 sfdx-source/apex-common-samplecode/main/layouts/Account-Account Layout.layout-meta.xml create mode 100644 sfdx-source/apex-common-samplecode/main/layouts/InvoiceTargets__mdt-Invoice Targets Layout.layout-meta.xml create mode 100644 sfdx-source/apex-common-samplecode/main/layouts/Opportunity-Opportunity Layout.layout-meta.xml create mode 100644 sfdx-source/apex-common-samplecode/main/lwc/jsconfig.json create mode 100644 sfdx-source/apex-common-samplecode/main/lwc/opportunityApplyDiscount/__tests__/opportunityApplyDiscount.test.js create mode 100644 sfdx-source/apex-common-samplecode/main/lwc/opportunityApplyDiscount/opportunityApplyDiscount.html create mode 100644 sfdx-source/apex-common-samplecode/main/lwc/opportunityApplyDiscount/opportunityApplyDiscount.js create mode 100644 sfdx-source/apex-common-samplecode/main/lwc/opportunityApplyDiscount/opportunityApplyDiscount.js-meta.xml create mode 100644 sfdx-source/apex-common-samplecode/main/lwc/opportunityCreateInvoice/__tests__/opportunityCreateInvoice.test.js create mode 100644 sfdx-source/apex-common-samplecode/main/lwc/opportunityCreateInvoice/opportunityCreateInvoice.html create mode 100644 sfdx-source/apex-common-samplecode/main/lwc/opportunityCreateInvoice/opportunityCreateInvoice.js create mode 100644 sfdx-source/apex-common-samplecode/main/lwc/opportunityCreateInvoice/opportunityCreateInvoice.js-meta.xml create mode 100644 sfdx-source/apex-common-samplecode/main/lwc/opportunityInvoiceJob/__tests__/opportunityInvoiceJob.test.js create mode 100644 sfdx-source/apex-common-samplecode/main/lwc/opportunityInvoiceJob/opportunityInvoiceJob.html create mode 100644 sfdx-source/apex-common-samplecode/main/lwc/opportunityInvoiceJob/opportunityInvoiceJob.js create mode 100644 sfdx-source/apex-common-samplecode/main/lwc/opportunityInvoiceJob/opportunityInvoiceJob.js-meta.xml create mode 100644 sfdx-source/apex-common-samplecode/main/lwc/quickOpportunityWizard/__tests__/quickOpportunityWizard.test.js create mode 100644 sfdx-source/apex-common-samplecode/main/lwc/quickOpportunityWizard/quickOpportunityWizard.html create mode 100644 sfdx-source/apex-common-samplecode/main/lwc/quickOpportunityWizard/quickOpportunityWizard.js create mode 100644 sfdx-source/apex-common-samplecode/main/lwc/quickOpportunityWizard/quickOpportunityWizard.js-meta.xml delete mode 100644 sfdx-source/apex-common-samplecode/main/objects/Account/webLinks/QuickOpportunity.webLink-meta.xml create mode 100644 sfdx-source/apex-common-samplecode/main/objects/InvoiceTargets__mdt/InvoiceTargets__mdt.object-meta.xml create mode 100644 sfdx-source/apex-common-samplecode/main/objects/InvoiceTargets__mdt/fields/DomainClass__c.field-meta.xml create mode 100644 sfdx-source/apex-common-samplecode/main/objects/InvoiceTargets__mdt/fields/SObjectType__c.field-meta.xml create mode 100644 sfdx-source/apex-common-samplecode/main/objects/InvoiceTargets__mdt/fields/SelectorClass__c.field-meta.xml create mode 100644 sfdx-source/apex-common-samplecode/main/objects/Opportunity/Opportunity.object-meta.xml delete mode 100644 sfdx-source/apex-common-samplecode/main/objects/Opportunity/webLinks/ApplyDiscount.webLink-meta.xml delete mode 100644 sfdx-source/apex-common-samplecode/main/objects/Opportunity/webLinks/BatchInvoicing.webLink-meta.xml delete mode 100644 sfdx-source/apex-common-samplecode/main/objects/Opportunity/webLinks/CreateInvoice.webLink-meta.xml create mode 100644 sfdx-source/apex-common-samplecode/main/objects/WorkOrder__c/fields/Account__c.field-meta.xml delete mode 100644 sfdx-source/apex-common-samplecode/main/pages/opportunityapplydiscount.page delete mode 100644 sfdx-source/apex-common-samplecode/main/pages/opportunityapplydiscount.page-meta.xml delete mode 100644 sfdx-source/apex-common-samplecode/main/pages/opportunitycreateinvoice.page delete mode 100644 sfdx-source/apex-common-samplecode/main/pages/opportunitycreateinvoice.page-meta.xml delete mode 100644 sfdx-source/apex-common-samplecode/main/pages/opportunityinvoicing.page delete mode 100644 sfdx-source/apex-common-samplecode/main/pages/opportunityinvoicing.page-meta.xml delete mode 100644 sfdx-source/apex-common-samplecode/main/pages/quickopportunitywizard.page delete mode 100644 sfdx-source/apex-common-samplecode/main/pages/quickopportunitywizard.page-meta.xml create mode 100644 sfdx-source/apex-common-samplecode/main/quickActions/Account.QuickOpportunity.quickAction-meta.xml create mode 100644 sfdx-source/apex-common-samplecode/main/quickActions/Opportunity.ApplyDiscount.quickAction-meta.xml create mode 100644 sfdx-source/apex-common-samplecode/main/quickActions/Opportunity.BatchInvoicing.quickAction-meta.xml create mode 100644 sfdx-source/apex-common-samplecode/main/quickActions/Opportunity.CreateInvoice.quickAction-meta.xml rename sfdx-source/apex-common-samplecode/{main/classes/service/IOpportunitiesService.cls => test/classes/TestIdGenerator.cls} (71%) create mode 100644 sfdx-source/apex-common-samplecode/test/classes/TestIdGenerator.cls-meta.xml create mode 100644 sfdx-source/apex-common-samplecode/test/classes/actions/ApplyDiscountTest.cls create mode 100644 sfdx-source/apex-common-samplecode/test/classes/actions/ApplyDiscountTest.cls-meta.xml rename sfdx-source/apex-common-samplecode/{main/classes/service/InvoicingServiceImpl.cls => test/classes/actions/CreateInvoiceTest.cls} (50%) create mode 100644 sfdx-source/apex-common-samplecode/test/classes/actions/CreateInvoiceTest.cls-meta.xml create mode 100644 sfdx-source/apex-common-samplecode/test/classes/controllers/OpportunityCreateInvoiceControllerTest.cls create mode 100644 sfdx-source/apex-common-samplecode/test/classes/controllers/OpportunityCreateInvoiceControllerTest.cls-meta.xml rename sfdx-source/apex-common-samplecode/{main/classes/selectors/IAccountsSelector.cls => test/classes/controllers/OpportunityInvoicingControllerTest.cls} (62%) create mode 100644 sfdx-source/apex-common-samplecode/test/classes/controllers/OpportunityInvoicingControllerTest.cls-meta.xml create mode 100644 sfdx-source/apex-common-samplecode/test/classes/controllers/QuickOpportunityWizardControllerTest.cls create mode 100644 sfdx-source/apex-common-samplecode/test/classes/controllers/QuickOpportunityWizardControllerTest.cls-meta.xml diff --git a/.gitignore b/.gitignore index 1faec73..197a96d 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ .DS_Store target/ temp/ +.tmp/ /deploy/* /debug/ **/dep-dir.txt diff --git a/README.md b/README.md index f91bdae..af54577 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ FFLib Apex Common Sample ===================================== ![Push Source and Run Apex Tests](https://github.com/apex-enterprise-patterns/fflib-apex-common-samplecode/workflows/Create%20a%20Scratch%20Org,%20Push%20Source%20and%20Run%20Apex%20Tests/badge.svg) -**Dependencies:** Must deploy [Apex Mocks](https://github.com/apex-enterprise-patterns/fflib-apex-mocks) and [Apex Common](https://github.com/apex-enterprise-patterns/fflib-apex-common) before deploying this library +**Dependencies:** Deploy [Apex Mocks](https://github.com/apex-enterprise-patterns/fflib-apex-mocks) and [Apex Common](https://github.com/apex-enterprise-patterns/fflib-apex-common) before deploying this sample. | Library | Deploy | |---------|--------| @@ -19,23 +19,73 @@ This repository contains a sample application illustrating the Apex Enterprise P | Platform Feature | Patterns Used | |------------------|---------------| -| Custom Buttons | Building **UI** logic and calling **Service Layer** code from Controllers | +| Lightning Web Components & Quick Actions | **UI** logic in `@AuraEnabled` controllers calling **Service Layer** code | +| Visualforce (list views) | Bulk **UI** actions via `StandardSetController` pages | | Batch Apex | Reusing **Service** and **Selector Layer** code from within a Batch context | -| Integration API | Exposing an Integration API via **Service Layer** using Apex and REST | -| Apex Triggers | Factoring your Apex Trigger logic via the **Domain Layer** (wrappers) | +| Integration API | Exposing an Integration API via **Service Layer** using Apex REST | +| Apex Triggers | **Domain Layer** trigger handlers (`fflib_SObjectDomain`) separate from **Domain Layer** service behaviour (`fflib_SObjects`) | +| Invocable Apex & Agentforce | **Service Layer** exposed to Agentforce via invocable actions and an AI authoring bundle | +| Polymorphic invoicing | **Custom Metadata** (`InvoiceTargets__mdt`) drives invoicing across Opportunity, DeveloperWorkItem, and TrainingWorkItem | + +Architecture Notes +------------------ + +This sample uses **concrete** Domain, Selector, and Service classes with static `newInstance()` factories and `@TestVisible` mock hooks for unit tests. There is no `Application.cls` dependency-injection factory. + +| Component | Role | +|-----------|------| +| **Services** | `OpportunitiesService`, `InvoicingService`, `AccountsService` — orchestrate selectors, domains, and Unit of Work | +| **Domains** | Business logic on records (e.g. discounting, invoice DTO generation) | +| **Trigger handlers** | `fflib_SObjectDomain` subclasses wired from triggers | +| **UnitOfWork** | `UnitOfWork.cls` — thin factory returning `fflib_SObjectUnitOfWork` with `UserModeDML()` | +| **InvoicingTargetsRegistry** | Resolves invoice targets from `InvoiceTargets__mdt` at runtime | User Mode and CRUD/FLS ---------------------- -This sample enforces field-level security (FLS) by using fflib user mode throughout. Selectors pass `fflib_SObjectSelector.DataAccess.USER_MODE` into the `super(...)` constructor for FLS on queries; the UnitOfWork uses `UserModeDML()` via an inner `UserModeUnitOfWorkFactory` in `Application.cls` for FLS on DML. Tests that exercise USER_MODE code use `TestDataFactory` to create a Standard User with the `ApexEnterprisePatternsSampleApp` permission set, then run under `System.runAs(getRunAsUser())`; data setup requiring elevated permissions (e.g. PricebookEntry insert) runs in system context. +This sample targets **API 67.0 and above**, where Apex runs in **user mode by default** at the platform level. That aligns with enforcing CRUD/FLS in production without relying on implicit system-mode behaviour. + +When the codebase runs on **API 66 or below** (where system mode is still the platform default), fflib and this sample **still enforce user mode explicitly** through the patterns below — selectors, Unit of Work, and tests do not depend on the platform default alone. + +Selectors pass `fflib_SObjectSelector.DataAccess.USER_MODE` into the `super(...)` constructor for FLS on queries. DML runs through `UnitOfWork.newInstance()`, which uses `UserModeDML()`. + +Tests that exercise USER_MODE code use `TestDataFactory` to create a Standard User with the `ApexEnterprisePatternsSampleApp` permission set, then run under `System.runAs(getRunAsUser())`. **Setup, DML, queries, and assertions** that touch FLS-protected fields must all run inside that `runAs` block. Data setup requiring elevated permissions (e.g. `PricebookEntry` insert) may run in system context before `runAs`. | Area | Approach | |------|----------| | **Selectors** | `super(false, fflib_SObjectSelector.DataAccess.USER_MODE)` in selector constructors (and `includeFieldSetFields` overload where present) | -| **UnitOfWork** | Inner `UserModeUnitOfWorkFactory` in Application.cls; uses `UserModeDML()` | -| **Tests** | `TestDataFactory`; `@TestSetup` + `System.runAs(getRunAsUser())` for DML/selector tests | +| **UnitOfWork** | `UnitOfWork.cls` factory; uses `UserModeDML()` | +| **Tests** | `TestDataFactory`; `@TestSetup` + `System.runAs(getRunAsUser())` for USER_MODE tests | | **Permission set** | `ApexEnterprisePatternsSampleApp` grants field-level access for USER_MODE tests | +Local Development +----------------- + +**API version:** 67.0 and above (`sfdx-project.json`). API 67 introduced user mode as the Apex default; this sample is written for that baseline and remains compatible with later API versions. + +**Scratch org:** `config/project-scratch-def.json` enables Agentforce/Einstein (`Einstein1AIPlatform` feature) for deploying the AI authoring bundle and invocable actions. Your Dev Hub must support these features. + +Deploy in order: + +```bash +# 1. Create scratch org +sf org create scratch --definition-file config/project-scratch-def.json --alias fflib-sample-scratch --set-default + +# 2. Deploy dependencies +git clone https://github.com/apex-enterprise-patterns/fflib-apex-mocks.git temp/fflib-apex-mocks +git clone https://github.com/apex-enterprise-patterns/fflib-apex-common.git temp/fflib-apex-common +sf project deploy start --source-dir temp/fflib-apex-mocks --target-org fflib-sample-scratch +sf project deploy start --source-dir temp/fflib-apex-common --target-org fflib-sample-scratch + +# 3. Deploy this sample +sf project deploy start --target-org fflib-sample-scratch + +# 4. Run tests +sf apex run test --target-org fflib-sample-scratch --wait 10 +``` + +Assign `ApexEnterprisePatternsSampleApp` to users who need access to the sample app's custom fields and tabs. Tests assign this permission set automatically via `TestDataFactory`; manual assignment is not required to run Apex tests. + Application Enterprise Patterns on Salesforce Lightning Platform ================================================================ @@ -44,9 +94,8 @@ Design patterns are an invaluable tool for developers and architects looking to More Information on Trailhead -------------------------------------------- -There are two Trailhead Modules for Apex Enterprise Patterns: +Trailhead modules for Apex Enterprise Patterns: - [Apex Enterprise Patterns - Separation of Concerns](https://trailhead.salesforce.com/en/content/learn/modules/apex_patterns_sl/apex_patterns_sl_soc) - [Apex Enterprise Patterns - Service Layer](https://trailhead.salesforce.com/en/content/learn/modules/apex_patterns_sl) - [Apex Enterprise Patterns - Domain and Selector Layer](https://trailhead.salesforce.com/en/content/learn/modules/apex_patterns_dsl) - diff --git a/config/project-scratch-def.json b/config/project-scratch-def.json index 6e31671..0753f45 100644 --- a/config/project-scratch-def.json +++ b/config/project-scratch-def.json @@ -1,9 +1,19 @@ { "orgName": "apex-common-samplecode", "edition": "Developer", + "features": ["EnableSetPasswordInApi", "Einstein1AIPlatform"], "settings": { "lightningExperienceSettings": { "enableS1DesktopEnabled": true + }, + "mobileSettings": { + "enableS1EncryptedStoragePref2": false + }, + "agentPlatformSettings": { + "enableAgentPlatform": true + }, + "einsteinGptSettings": { + "enableEinsteinGptPlatform": true } } } diff --git a/sfdx-project.json b/sfdx-project.json index 88f5561..830cc21 100644 --- a/sfdx-project.json +++ b/sfdx-project.json @@ -11,5 +11,5 @@ ], "namespace": "", "sfdcLoginUrl": "https://login.salesforce.com", - "sourceApiVersion": "63.0" + "sourceApiVersion": "67.0" } \ No newline at end of file diff --git a/sfdx-source/apex-common-samplecode/main/aiAuthoringBundles/OpportunityOperations/OpportunityOperations.agent b/sfdx-source/apex-common-samplecode/main/aiAuthoringBundles/OpportunityOperations/OpportunityOperations.agent new file mode 100644 index 0000000..24d4423 --- /dev/null +++ b/sfdx-source/apex-common-samplecode/main/aiAuthoringBundles/OpportunityOperations/OpportunityOperations.agent @@ -0,0 +1,131 @@ +# OpportunityOperations - Employee agent for opportunity discounts and invoices +# Calls the ApplyDiscount and CreateInvoice invocable Apex actions + +config: + developer_name: "OpportunityOperations" + agent_label: "Opportunity Operations" + agent_type: "AgentforceEmployeeAgent" + description: "Helps employees apply opportunity discounts and create invoices from opportunities." + +variables: + currentRecordId: mutable string = "" + description: "Id of the record the employee opened the agent from" + visibility: "External" + opportunity_id: mutable string = "" + description: "Salesforce Opportunity Id for the current request" + discount_percentage: mutable number = 0 + description: "Discount percent to apply, for example 10 for 10 percent" + invoice_id: mutable string = "" + description: "Id of the Invoice__c created from an Opportunity" + invoice_name: mutable string = "" + description: "Auto-number Name of the Invoice__c created from an Opportunity" + +system: + messages: + welcome: "I can apply discounts to opportunities and create invoices from them. What would you like to do?" + error: "I could not complete that opportunity request. Please try again." + instructions: "You help sales employees discount opportunities and create invoices. Use only the provided Apex actions. Never invent an Opportunity Id. If the user names an Opportunity Id, use that. Otherwise use {!@variables.currentRecordId} when it is set. Ask for any missing Id or discount percent before calling an action." + +language: + default_locale: "en_US" + +start_agent agent_router: + description: "Welcome the user and route discount or invoice requests" + + reasoning: + instructions:| + Select the tool that best matches the user's message and conversation history. If it's unclear, make your best guess. + actions: + go_to_apply_discount: @utils.transition to @subagent.apply_discount + description: "User wants to apply a discount to an Opportunity" + go_to_create_invoice: @utils.transition to @subagent.create_invoice + description: "User wants to create an Invoice from an Opportunity" + +subagent apply_discount: + description: "Applies a percentage discount to an Opportunity through ApplyDiscount Apex" + + actions: + apply_opportunity_discount: + description: "Applies a percentage discount to an Opportunity and its discountable products" + require_user_confirmation: False + inputs: + opportunityId: object + description: "Id of the Opportunity to discount" + is_required: True + complex_data_type_name: "lightning__recordIdType" + discountPercentage: number + description: "Discount percent to apply, for example 10 for 10 percent" + is_required: True + outputs: + opportunityId: object + description: "Id of the Opportunity that was discounted" + complex_data_type_name: "lightning__recordIdType" + discountPercentage: number + description: "Discount percent that was applied" + target: "apex://ApplyDiscount" + + reasoning: + instructions:-> + | Apply a discount to an Opportunity using {!@actions.apply_opportunity_discount}. + You need an Opportunity Id and a discount percent. + Prefer an Opportunity Id the user named. + Otherwise use {!@variables.currentRecordId} when it is set. + If either the Opportunity Id or discount percent is missing, ask for it. + After the action succeeds, confirm the Opportunity Id and the percent that was applied. + if @variables.currentRecordId: + | Record page Opportunity Id: {!@variables.currentRecordId} + if @variables.opportunity_id: + | Current Opportunity Id: {!@variables.opportunity_id} + if @variables.discount_percentage > 0: + | Current discount percent: {!@variables.discount_percentage} + actions: + apply_opportunity_discount: @actions.apply_opportunity_discount + with opportunityId=... + with discountPercentage=... + set @variables.opportunity_id = @outputs.opportunityId + set @variables.discount_percentage = @outputs.discountPercentage + +subagent create_invoice: + description: "Creates an Invoice__c from an Opportunity through CreateInvoice Apex" + + actions: + create_opportunity_invoice: + description: "Creates an Invoice from an Opportunity, optionally applying a discount first" + require_user_confirmation: False + inputs: + opportunityId: object + description: "Id of the Opportunity to invoice" + is_required: True + complex_data_type_name: "lightning__recordIdType" + discountPercentage: number + description: "Optional discount percent applied before invoicing, for example 10 for 10 percent" + is_required: False + outputs: + invoiceId: object + description: "Id of the Invoice__c that was created" + complex_data_type_name: "lightning__recordIdType" + invoiceName: string + description: "Invoice Number (Name) of the Invoice__c that was created, for example INV-00000001" + target: "apex://CreateInvoice" + + reasoning: + instructions:-> + | Create an invoice from an Opportunity using {!@actions.create_opportunity_invoice}. + You need an Opportunity Id. + Prefer an Opportunity Id the user named. + Otherwise use {!@variables.currentRecordId} when it is set. + If no Opportunity Id is available, ask for it. + Only include a discount percent when the user asked for one. + After the action succeeds, tell the user the new Invoice Number (invoiceName), not the Salesforce Id. + if @variables.currentRecordId: + | Record page Opportunity Id: {!@variables.currentRecordId} + if @variables.opportunity_id: + | Current Opportunity Id: {!@variables.opportunity_id} + if @variables.invoice_name: + | Last Invoice Number: {!@variables.invoice_name} + actions: + create_opportunity_invoice: @actions.create_opportunity_invoice + with opportunityId=... + with discountPercentage=... + set @variables.invoice_id = @outputs.invoiceId + set @variables.invoice_name = @outputs.invoiceName diff --git a/sfdx-source/apex-common-samplecode/main/aiAuthoringBundles/OpportunityOperations/OpportunityOperations.bundle-meta.xml b/sfdx-source/apex-common-samplecode/main/aiAuthoringBundles/OpportunityOperations/OpportunityOperations.bundle-meta.xml new file mode 100644 index 0000000..50c47cd --- /dev/null +++ b/sfdx-source/apex-common-samplecode/main/aiAuthoringBundles/OpportunityOperations/OpportunityOperations.bundle-meta.xml @@ -0,0 +1,5 @@ + + + AGENT + v0.1 + diff --git a/sfdx-source/apex-common-samplecode/main/applications/ApexEnterprisePatternsSampleApp.app-meta.xml b/sfdx-source/apex-common-samplecode/main/applications/ApexEnterprisePatternsSampleApp.app-meta.xml index 30823c5..2690830 100644 --- a/sfdx-source/apex-common-samplecode/main/applications/ApexEnterprisePatternsSampleApp.app-meta.xml +++ b/sfdx-source/apex-common-samplecode/main/applications/ApexEnterprisePatternsSampleApp.app-meta.xml @@ -1,6 +1,22 @@ - standard-home + + #014486 + false + Sample application illustrating Apex Enterprise Paterns, https://github.com/financialforcedev/fflib-apex-common + Small + Large + false + false + false + Standard + standard-home + standard-Account + standard-Opportunity + standard-Product2 + Invoice__c + WorkOrder__c + Lightning diff --git a/sfdx-source/apex-common-samplecode/main/classes/Application.cls b/sfdx-source/apex-common-samplecode/main/classes/Application.cls deleted file mode 100644 index 609e7eb..0000000 --- a/sfdx-source/apex-common-samplecode/main/classes/Application.cls +++ /dev/null @@ -1,83 +0,0 @@ -/** - * Copyright (c) 2014-2016, FinancialForce.com, Inc - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, - * are permitted provided that the following conditions are met: - * - * - Redistributions of source code must retain the above copyright notice, - * this list of conditions and the following disclaimer. - * - Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation - * and/or other materials provided with the distribution. - * - Neither the name of the FinancialForce.com, inc nor the names of its contributors - * may be used to endorse or promote products derived from this software without - * specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND - * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES - * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL - * THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, - * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS - * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY - * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) - * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -**/ - -public class Application -{ - // Configure and create the UnitOfWorkFactory for this Application (UserMode DML for FLS) - public static final fflib_Application.UnitOfWorkFactory UnitOfWork = - new UserModeUnitOfWorkFactory( - new List { - Account.SObjectType, - Invoice__c.SObjectType, - InvoiceLine__c.SObjectType, - Opportunity.SObjectType, - Product2.SObjectType, - PricebookEntry.SObjectType, - OpportunityLineItem.SObjectType, - WorkOrder__c.SObjectType, - DeveloperWorkItem__c.SObjectType, - TrainingWorkItem__c.SObjectType, - DesignWorkItem__c.SObjectType }); - - // Configure and create the ServiceFactory for this Application - public static final fflib_Application.ServiceFactory Service = - new fflib_Application.ServiceFactory( - new Map { - IAccountsService.class => AccountsServiceImpl.class, - IOpportunitiesService.class => OpportunitiesServiceImpl.class, - IInvoicingService.class => InvoicingServiceImpl.class }); - - // Configure and create the SelectorFactory for this Application - public static final fflib_Application.SelectorFactory Selector = - new fflib_Application.SelectorFactory( - new Map { - Account.SObjectType => AccountsSelector.class, - Opportunity.SObjectType => OpportunitiesSelector.class, - OpportunityLineItem.SObjectType => OpportunityLineItemsSelector.class, - PricebookEntry.SObjectType => PricebookEntriesSelector.class, - Pricebook2.SObjectType => PricebooksSelector.class, - Product2.SObjectType => ProductsSelector.class, - User.sObjectType => UsersSelector.class }); - - // Configure and create the DomainFactory for this Application - public static final fflib_Application.DomainFactory Domain = - new fflib_Application.DomainFactory( - Application.Selector, - new Map { - Opportunity.SObjectType => Opportunities.Constructor.class, - OpportunityLineItem.SObjectType => OpportunityLineItems.Constructor.class, - Account.SObjectType => Accounts.Constructor.class, - DeveloperWorkItem__c.SObjectType => DeveloperWorkItems.class }); - - // Custom UnitOfWork factory that uses UserModeDML for FLS enforcement - private class UserModeUnitOfWorkFactory extends fflib_Application.UnitOfWorkFactory { - public UserModeUnitOfWorkFactory(List objectTypes) { super(objectTypes); } - public override fflib_ISObjectUnitOfWork newInstance() { - if (m_mockUow != null) return m_mockUow; - return new fflib_SObjectUnitOfWork(m_objectTypes, new fflib_SObjectUnitOfWork.UserModeDML()); - } - } -} diff --git a/sfdx-source/apex-common-samplecode/main/classes/InvoicingTargetsRegistry.cls b/sfdx-source/apex-common-samplecode/main/classes/InvoicingTargetsRegistry.cls new file mode 100644 index 0000000..8a26e68 --- /dev/null +++ b/sfdx-source/apex-common-samplecode/main/classes/InvoicingTargetsRegistry.cls @@ -0,0 +1,114 @@ +/** + * Copyright (c) 2012-2014, FinancialForce.com, inc + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * - Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * - Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * - Neither the name of the FinancialForce.com, inc nor the names of its contributors + * may be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL + * THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY + * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +**/ + +/** + * Registry for InvoicingService.generate: polymorphic resolution of domains/selectors + * for SObject types whose domain implements InvoicingService.ISupportInvoicing. + * + * Configure via InvoiceTargets__mdt only for SObject types whose domain implements ISupportInvoicing. + * Not a general-purpose factory. Use direct construction (e.g. AccountsSelector.newInstance(), + * Opportunities.newInstance(records)) when the SObject type is known at compile time. + */ +public inherited sharing class InvoicingTargetsRegistry { + private static fflib_Application.SelectorFactory selectorFactory; + private static fflib_Application.DomainFactory domainFactory; + + @TestVisible + private static Map mockSelectors; + @TestVisible + private static Map mockDomains; + + /** + * Constructs a domain for the given records using the type registered in InvoiceTargets__mdt. + * @param records Homogeneous SObject list whose type is registered for invoicing. Cannot be null. + * @return Domain instance for the records' SObject type. + * @throws InvoicingTargetsRegistryException When the type is unsupported or cannot be determined. + */ + public static fflib_IDomain newDomain(List records) { + SObjectType t = records.getSObjectType(); + if (t == null) { + throw new InvoicingTargetsRegistryException('Unable to determine SObjectType'); + } + if (mockDomains != null && mockDomains.containsKey(t)) { + return mockDomains.get(t); + } + ensureInitialized(); + return domainFactory.newInstance(records); + } + + /** + * Selects records by Id using the selector registered in InvoiceTargets__mdt. + * @param recordIds Homogeneous set of Ids. Cannot be null or empty. + * @return Matching SObjects queried by the configured selector. + * @throws InvoicingTargetsRegistryException When Ids are invalid or mixed. + */ + public static List selectById(Set recordIds) { + if (recordIds == null || recordIds.isEmpty()) { + throw new InvoicingTargetsRegistryException('Invalid record Ids'); + } + SObjectType t = new List(recordIds)[0].getSObjectType(); + for (Id recordId : recordIds) { + if (recordId.getSObjectType() != t) { + throw new InvoicingTargetsRegistryException('Mixed SObject types'); + } + } + if (mockSelectors != null && mockSelectors.containsKey(t)) { + return mockSelectors.get(t).selectSObjectsById(recordIds); + } + ensureInitialized(); + return selectorFactory.selectById(recordIds); + } + + private static void ensureInitialized() { + if (selectorFactory != null) { + return; + } + Map sobjectToDomain = new Map(); + Map sobjectToSelector = new Map(); + Map gd = Schema.getGlobalDescribe(); + for (InvoiceTargets__mdt config : [ + SELECT SObjectType__r.QualifiedApiName, DomainClass__c, SelectorClass__c + FROM InvoiceTargets__mdt + WHERE SObjectType__c != null + WITH USER_MODE + ]) { + SObjectType sot = gd.get(config.SObjectType__r.QualifiedApiName); + if (sot == null) { + continue; + } + sobjectToDomain.put(sot, Type.forName(config.DomainClass__c)); + sobjectToSelector.put(sot, Type.forName(config.SelectorClass__c)); + } + selectorFactory = new fflib_Application.SelectorFactory(sobjectToSelector); + domainFactory = new fflib_Application.DomainFactory(selectorFactory, sobjectToDomain); + } + + /** + * Thrown when registry lookup or domain/selector construction fails. + */ + public class InvoicingTargetsRegistryException extends Exception { + } +} diff --git a/sfdx-source/apex-common-samplecode/main/classes/domains/IAccounts.cls-meta.xml b/sfdx-source/apex-common-samplecode/main/classes/InvoicingTargetsRegistry.cls-meta.xml similarity index 80% rename from sfdx-source/apex-common-samplecode/main/classes/domains/IAccounts.cls-meta.xml rename to sfdx-source/apex-common-samplecode/main/classes/InvoicingTargetsRegistry.cls-meta.xml index 5f399c3..b009296 100644 --- a/sfdx-source/apex-common-samplecode/main/classes/domains/IAccounts.cls-meta.xml +++ b/sfdx-source/apex-common-samplecode/main/classes/InvoicingTargetsRegistry.cls-meta.xml @@ -1,5 +1,5 @@ - 63.0 + 67.0 Active diff --git a/sfdx-source/apex-common-samplecode/main/classes/domains/IAccounts.cls b/sfdx-source/apex-common-samplecode/main/classes/UnitOfWork.cls similarity index 63% rename from sfdx-source/apex-common-samplecode/main/classes/domains/IAccounts.cls rename to sfdx-source/apex-common-samplecode/main/classes/UnitOfWork.cls index f801697..5a92f79 100644 --- a/sfdx-source/apex-common-samplecode/main/classes/domains/IAccounts.cls +++ b/sfdx-source/apex-common-samplecode/main/classes/UnitOfWork.cls @@ -24,9 +24,37 @@ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. **/ -public interface IAccounts extends fflib_ISObjects -{ - List getAccounts(); - void setDescription(String description); - void updateOpportunityActivity(); -} \ No newline at end of file +/** + * Thin Unit of Work factory for the sample app. No Application class. + */ +public inherited sharing class UnitOfWork { + @TestVisible + private static fflib_ISObjectUnitOfWork mock; + + /** + * Returns a Unit of Work registered for this app's types, or {@link #mock} when tests have set one. + * DML runs in user mode. + * @return Unit of Work instance. + */ + public static fflib_ISObjectUnitOfWork newInstance() { + if (mock != null) { + return mock; + } + return new fflib_SObjectUnitOfWork( + new List{ + Account.SObjectType, + Opportunity.SObjectType, + Product2.SObjectType, + PricebookEntry.SObjectType, + OpportunityLineItem.SObjectType, + Invoice__c.SObjectType, + InvoiceLine__c.SObjectType, + WorkOrder__c.SObjectType, + DeveloperWorkItem__c.SObjectType, + TrainingWorkItem__c.SObjectType, + DesignWorkItem__c.SObjectType + }, + new fflib_SObjectUnitOfWork.UserModeDML() + ); + } +} diff --git a/sfdx-source/apex-common-samplecode/main/classes/Application.cls-meta.xml b/sfdx-source/apex-common-samplecode/main/classes/UnitOfWork.cls-meta.xml similarity index 80% rename from sfdx-source/apex-common-samplecode/main/classes/Application.cls-meta.xml rename to sfdx-source/apex-common-samplecode/main/classes/UnitOfWork.cls-meta.xml index 5f399c3..b009296 100644 --- a/sfdx-source/apex-common-samplecode/main/classes/Application.cls-meta.xml +++ b/sfdx-source/apex-common-samplecode/main/classes/UnitOfWork.cls-meta.xml @@ -1,5 +1,5 @@ - 63.0 + 67.0 Active diff --git a/sfdx-source/apex-common-samplecode/main/classes/actions/ApplyDiscount.cls b/sfdx-source/apex-common-samplecode/main/classes/actions/ApplyDiscount.cls new file mode 100644 index 0000000..11739a9 --- /dev/null +++ b/sfdx-source/apex-common-samplecode/main/classes/actions/ApplyDiscount.cls @@ -0,0 +1,88 @@ +/** + * Copyright (c) 2012-2014, FinancialForce.com, inc + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * - Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * - Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * - Neither the name of the FinancialForce.com, inc nor the names of its contributors + * may be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL + * THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY + * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +**/ + +/** + * Invocable action that applies a percentage discount to an Opportunity. + * Delegates to {@link OpportunitiesService}. + */ +public inherited sharing class ApplyDiscount { + /** + * Applies a discount for each request via {@link OpportunitiesService#applyDiscounts}. + * @param requests Opportunity Id and discount percent per request. Cannot be null. + * @return Opportunity Ids in the same order as {@code requests}. + */ + @InvocableMethod( + label='Apply Discount' + description='Applies a percentage discount to an Opportunity and its discountable products.' + category='Opportunities' + ) + public static List execute(List requests) { + List results = new List(); + for (Request request : requests) { + OpportunitiesService.newInstance().applyDiscounts( + new Set{ request.opportunityId }, + request.discountPercentage + ); + Result result = new Result(); + result.opportunityId = request.opportunityId; + result.discountPercentage = request.discountPercentage; + results.add(result); + } + return results; + } + + /** + * Input for {@link #execute}. + */ + public class Request { + /** + * Opportunity Id to discount. + */ + @InvocableVariable(label='Opportunity Id' required=true) + public Id opportunityId; + /** + * Discount percent to apply (for example 10 for 10%). + */ + @InvocableVariable(label='Discount Percentage' required=true) + public Decimal discountPercentage; + } + + /** + * Output for {@link #execute}. + */ + public class Result { + /** + * Id of the discounted Opportunity. + */ + @InvocableVariable(label='Opportunity Id') + public Id opportunityId; + /** + * Discount percent that was applied. + */ + @InvocableVariable(label='Discount Percentage') + public Decimal discountPercentage; + } +} diff --git a/sfdx-source/apex-common-samplecode/main/classes/domains/IChargeable.cls-meta.xml b/sfdx-source/apex-common-samplecode/main/classes/actions/ApplyDiscount.cls-meta.xml similarity index 80% rename from sfdx-source/apex-common-samplecode/main/classes/domains/IChargeable.cls-meta.xml rename to sfdx-source/apex-common-samplecode/main/classes/actions/ApplyDiscount.cls-meta.xml index 5f399c3..b009296 100644 --- a/sfdx-source/apex-common-samplecode/main/classes/domains/IChargeable.cls-meta.xml +++ b/sfdx-source/apex-common-samplecode/main/classes/actions/ApplyDiscount.cls-meta.xml @@ -1,5 +1,5 @@ - 63.0 + 67.0 Active diff --git a/sfdx-source/apex-common-samplecode/main/classes/actions/CreateInvoice.cls b/sfdx-source/apex-common-samplecode/main/classes/actions/CreateInvoice.cls new file mode 100644 index 0000000..d6a4e61 --- /dev/null +++ b/sfdx-source/apex-common-samplecode/main/classes/actions/CreateInvoice.cls @@ -0,0 +1,99 @@ +/** + * Copyright (c) 2012-2014, FinancialForce.com, inc + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * - Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * - Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * - Neither the name of the FinancialForce.com, inc nor the names of its contributors + * may be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL + * THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY + * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +**/ + +/** + * Invocable action that creates an Invoice from an Opportunity. + * Delegates to {@link OpportunitiesService}. + */ +public inherited sharing class CreateInvoice { + /** + * Creates an Invoice for each request via {@link OpportunitiesService#createInvoices}. + * @param requests Opportunity Id and optional discount percent per request. Cannot be null. + * @return Invoice Id and Name in the same order as {@code requests}. + */ + @InvocableMethod( + label='Create Invoice' + description='Creates an Invoice from an Opportunity, optionally applying a discount first.' + category='Opportunities' + ) + public static List execute(List requests) { + List results = new List(); + for (Request request : requests) { + Set invoiceIds = OpportunitiesService.newInstance().createInvoices( + new Set{ request.opportunityId }, + request.discountPercentage + ); + Id invoiceId = new List(invoiceIds)[0]; + Result result = new Result(); + result.invoiceId = invoiceId; + result.invoiceName = invoiceName(invoiceId); + results.add(result); + } + return results; + } + + @TestVisible + private static String testInvoiceName; + + private static String invoiceName(Id invoiceId) { + if (testInvoiceName != null) { + return testInvoiceName; + } + return [SELECT Name FROM Invoice__c WHERE Id = :invoiceId].Name; + } + + /** + * Input for {@link #execute}. + */ + public class Request { + /** + * Opportunity Id to invoice. + */ + @InvocableVariable(label='Opportunity Id' required=true) + public Id opportunityId; + /** + * Optional discount percent applied before invoicing (for example 10 for 10%). + */ + @InvocableVariable(label='Discount Percentage') + public Decimal discountPercentage; + } + + /** + * Output for {@link #execute}. + */ + public class Result { + /** + * Id of the created Invoice__c. + */ + @InvocableVariable(label='Invoice Id') + public Id invoiceId; + /** + * Auto-number Name of the created Invoice__c (for example INV-00000001). + */ + @InvocableVariable(label='Invoice Number') + public String invoiceName; + } +} diff --git a/sfdx-source/apex-common-samplecode/main/classes/controllers/OpportunityConsoleController.cls-meta.xml b/sfdx-source/apex-common-samplecode/main/classes/actions/CreateInvoice.cls-meta.xml similarity index 80% rename from sfdx-source/apex-common-samplecode/main/classes/controllers/OpportunityConsoleController.cls-meta.xml rename to sfdx-source/apex-common-samplecode/main/classes/actions/CreateInvoice.cls-meta.xml index 5f399c3..b009296 100644 --- a/sfdx-source/apex-common-samplecode/main/classes/controllers/OpportunityConsoleController.cls-meta.xml +++ b/sfdx-source/apex-common-samplecode/main/classes/actions/CreateInvoice.cls-meta.xml @@ -1,5 +1,5 @@ - 63.0 + 67.0 Active diff --git a/sfdx-source/apex-common-samplecode/main/classes/batchjobs/CreateInvoicesJob.cls b/sfdx-source/apex-common-samplecode/main/classes/batchjobs/CreateInvoicesJob.cls index b846109..748da5f 100644 --- a/sfdx-source/apex-common-samplecode/main/classes/batchjobs/CreateInvoicesJob.cls +++ b/sfdx-source/apex-common-samplecode/main/classes/batchjobs/CreateInvoicesJob.cls @@ -25,43 +25,48 @@ **/ /** - * Class implements the required interfaces for Scheduling and executing in Batch - * - * NOTE: The server and selector methods are reused, logic in this batch class is thus very light, - * focusing on passing parameters in and handling errors and notifications - * - **/ -public with sharing class CreateInvoicesJob + * Batch and schedulable job that invoices Opportunities marked Ready via {@link OpportunitiesService}. + * Accumulates per-chunk failures and emails the running user on finish. + */ +public inherited sharing class CreateInvoicesJob implements System.Schedulable, Database.Batchable, Database.Stateful { - // Simple job log for errors private List jobErrors = new List(); + /** + * Schedulable entry point that starts a new batch instance of this job. + * @param sc Schedulable context provided by the platform. + */ public void execute(SchedulableContext sc) { - // Start the job once the scheduled time has arrived Database.executeBatch(new CreateInvoicesJob()); } + /** + * Starts the batch using Opportunities with InvoicedStatus__c Ready. + * @param context Batchable context provided by the platform. + * @return QueryLocator from {@link OpportunitiesSelector#readyToInvoiceAsQueryLocator}. + */ public Database.QueryLocator start(Database.BatchableContext context) { - // Obtain appropriate query locator from the selector - return new OpportunitiesSelector().queryLocatorReadyToInvoice(); + return OpportunitiesSelector.newInstance().readyToInvoiceAsQueryLocator(); } + /** + * Creates invoices for the current chunk of Opportunities with zero discount. + * Failures are captured in {@link JobError} without failing the entire batch. + * @param context Batchable context provided by the platform. + * @param opps Opportunity chunk to invoice. + */ public void execute(Database.BatchableContext context, List opps) { try { - // Opportunities ready to be invoiced Set opportunitiesToInvoice = new Map(opps).keySet(); - - // Create invoices - OpportunitiesService.createInvoices(opportunitiesToInvoice, 0); + OpportunitiesService.newInstance().createInvoices(opportunitiesToInvoice, 0); } catch (Exception e) { - // Capture context and error for reporting once job complete JobError jobError = new JobError(); jobError.records = opps; jobError.message = e.getMessage(); @@ -69,16 +74,17 @@ public with sharing class CreateInvoicesJob } } + /** + * Emails the running user a summary of any chunk failures recorded during execute. + * @param context Batchable context provided by the platform. + */ public void finish(Database.BatchableContext context) { - // Simple notification of any errors received via email if(jobErrors.size() > 0) { - // Email address from user Map usersEmail = - new UsersSelector().getUsersEmail( + UsersSelector.newInstance().getUsersEmail( new Set { UserInfo.getUserId() }); - // Construct email body String emailBody = ''; for(JobError jobError : jobErrors) { @@ -90,7 +96,6 @@ public with sharing class CreateInvoicesJob emailBody += String.format('

Error {0} occurred during the processing of Opportunities {1}

', new List { jobError.message, String.join(failedOpps, ',') }); } - // Send email Messaging.SingleEmailMessage mail = new Messaging.SingleEmailMessage(); mail.setToAddresses(new String[] { usersEmail.get(UserInfo.getUserId()) }); mail.setReplyTo(usersEmail.get(UserInfo.getUserId())); @@ -102,11 +107,17 @@ public with sharing class CreateInvoicesJob } /** - * Simple wrapper class containing the error message and the records in scope at the time - **/ + * Captures a batch chunk failure message and the Opportunities that failed. + */ public class JobError { + /** + * Exception or error message for the failed chunk. + */ public String message; + /** + * Opportunities in the chunk that failed processing. + */ public List records; } -} \ No newline at end of file +} diff --git a/sfdx-source/apex-common-samplecode/main/classes/batchjobs/CreateInvoicesJob.cls-meta.xml b/sfdx-source/apex-common-samplecode/main/classes/batchjobs/CreateInvoicesJob.cls-meta.xml index 5f399c3..b009296 100644 --- a/sfdx-source/apex-common-samplecode/main/classes/batchjobs/CreateInvoicesJob.cls-meta.xml +++ b/sfdx-source/apex-common-samplecode/main/classes/batchjobs/CreateInvoicesJob.cls-meta.xml @@ -1,5 +1,5 @@ - 63.0 + 67.0 Active diff --git a/sfdx-source/apex-common-samplecode/main/classes/controllers/OpportunityApplyDiscountController.cls b/sfdx-source/apex-common-samplecode/main/classes/controllers/OpportunityApplyDiscountController.cls index 3a71f56..dff01fe 100644 --- a/sfdx-source/apex-common-samplecode/main/classes/controllers/OpportunityApplyDiscountController.cls +++ b/sfdx-source/apex-common-samplecode/main/classes/controllers/OpportunityApplyDiscountController.cls @@ -25,65 +25,33 @@ **/ /** - * Controller shared by Apply Discount and Apply Discounts Custom Buttons (and related pages) on the Opportunity - * - * NOTE: The same service method is called regardless, logic in this controller is thus very light, - * focusing on passing parameters in and handling errors and notifications - **/ -public with sharing class OpportunityApplyDiscountController + * Controller for the Apply Discount Lightning action. + * Delegates discounting to {@link OpportunitiesService}. + */ +public inherited sharing class OpportunityApplyDiscountController { - public Decimal DiscountPercentage { get; set; } - - private ApexPages.StandardController standardController = null; - - private ApexPages.StandardSetController standardSetController = null; - - public OpportunityApplyDiscountController(ApexPages.StandardController controller) - { - standardController = controller; - } - - public OpportunityApplyDiscountController(ApexPages.StandardSetController controller) - { - standardSetController = controller; - } - - public PageReference applyDiscount() + /** + * Applies a discount to a single Opportunity from the Lightning action. + * @param opportunityId Opportunity Id to discount. Cannot be null. + * @param discountPercentage Discount percent to apply (for example 10 for 10%). + */ + @AuraEnabled(cacheable=false) + public static void applyDiscount(Id opportunityId, Decimal discountPercentage) { try { - // Apply discount entered to the current Opportunity - OpportunitiesService.applyDiscounts( - new Set { standardController.getId() }, DiscountPercentage); + OpportunitiesService.newInstance().applyDiscounts(new Set{ opportunityId }, discountPercentage); } catch (Exception e) { - ApexPages.addMessages(e); - } - - return ApexPages.hasMessages() ? null : standardController.view(); + throw toAura(e); + } } - public PageReference applyDiscounts() + private static AuraHandledException toAura(Exception e) { - try - { - // Selected Opportunity Id's - Set selectedOpportunityIds = - new Map(standardSetController.getSelected()).keySet(); - - // Apply discount entered to the selected Opportunities - OpportunitiesService.applyDiscounts( - selectedOpportunityIds, DiscountPercentage); - - // Confirm submission - ApexPages.addMessage(new ApexPages.Message(ApexPages.Severity.INFO, 'Opportunities updated.')); - } - catch (Exception e) - { - ApexPages.addMessages(e); - } - - return null; + AuraHandledException auraException = new AuraHandledException(e.getMessage()); + auraException.setMessage(e.getMessage()); + return auraException; } -} \ No newline at end of file +} diff --git a/sfdx-source/apex-common-samplecode/main/classes/controllers/OpportunityApplyDiscountController.cls-meta.xml b/sfdx-source/apex-common-samplecode/main/classes/controllers/OpportunityApplyDiscountController.cls-meta.xml index 5f399c3..b009296 100644 --- a/sfdx-source/apex-common-samplecode/main/classes/controllers/OpportunityApplyDiscountController.cls-meta.xml +++ b/sfdx-source/apex-common-samplecode/main/classes/controllers/OpportunityApplyDiscountController.cls-meta.xml @@ -1,5 +1,5 @@ - 63.0 + 67.0 Active diff --git a/sfdx-source/apex-common-samplecode/main/classes/controllers/OpportunityConsoleController.cls b/sfdx-source/apex-common-samplecode/main/classes/controllers/OpportunityApplyDiscountsController.cls similarity index 55% rename from sfdx-source/apex-common-samplecode/main/classes/controllers/OpportunityConsoleController.cls rename to sfdx-source/apex-common-samplecode/main/classes/controllers/OpportunityApplyDiscountsController.cls index 0868ea3..aa37aa6 100644 --- a/sfdx-source/apex-common-samplecode/main/classes/controllers/OpportunityConsoleController.cls +++ b/sfdx-source/apex-common-samplecode/main/classes/controllers/OpportunityApplyDiscountsController.cls @@ -24,12 +24,40 @@ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. **/ -public with sharing class OpportunityConsoleController -{ - @RemoteAction - public static void applyDiscount(Id opportunityId, Decimal discountPercent) - { - // Call service - OpportunitiesService.applyDiscounts(new Set { opportunityId }, discountPercent); +/** + * Visualforce controller for the Apply Discounts list-view button. + * Delegates discounting to {@link OpportunitiesService}. + */ +public inherited sharing class OpportunityApplyDiscountsController { + /** + * Discount percent entered on the list page (for example 10 for 10%). + */ + public Decimal DiscountPercentage { get; set; } + + private ApexPages.StandardSetController standardSetController; + + /** + * Creates a controller for a list StandardSetController (bulk apply). + * @param controller StandardSetController with selected Opportunities. Cannot be null. + */ + public OpportunityApplyDiscountsController(ApexPages.StandardSetController controller) { + standardSetController = controller; + } + + /** + * Applies {@link #DiscountPercentage} to all selected Opportunities on the list view. + * @return Null so the list page remains; adds an info message on success. + */ + public PageReference applyDiscounts() { + try { + Set selectedOpportunityIds = + new Map(standardSetController.getSelected()).keySet(); + OpportunitiesService.newInstance() + .applyDiscounts(selectedOpportunityIds, DiscountPercentage); + ApexPages.addMessage(new ApexPages.Message(ApexPages.Severity.INFO, 'Opportunities updated.')); + } catch (Exception e) { + ApexPages.addMessages(e); + } + return null; } -} \ No newline at end of file +} diff --git a/sfdx-source/apex-common-samplecode/main/classes/controllers/OpportunityApplyDiscountsController.cls-meta.xml b/sfdx-source/apex-common-samplecode/main/classes/controllers/OpportunityApplyDiscountsController.cls-meta.xml new file mode 100644 index 0000000..b009296 --- /dev/null +++ b/sfdx-source/apex-common-samplecode/main/classes/controllers/OpportunityApplyDiscountsController.cls-meta.xml @@ -0,0 +1,5 @@ + + + 67.0 + Active + diff --git a/sfdx-source/apex-common-samplecode/main/classes/controllers/OpportunityCreateInvoiceController.cls b/sfdx-source/apex-common-samplecode/main/classes/controllers/OpportunityCreateInvoiceController.cls index 209e1f6..6bcd2b5 100644 --- a/sfdx-source/apex-common-samplecode/main/classes/controllers/OpportunityCreateInvoiceController.cls +++ b/sfdx-source/apex-common-samplecode/main/classes/controllers/OpportunityCreateInvoiceController.cls @@ -25,67 +25,35 @@ **/ /** - * Controller shared by Create Invoice and Create Invoices Custom Buttons (and related pages) on the Opportunity - * - * NOTE: The same service method is called regardless, logic in this controller is thus very light, - * focusing on passing parameters in and handling errors and notifications - **/ -public with sharing class OpportunityCreateInvoiceController + * Controller for the Create Invoice Lightning action. + * Delegates invoice creation to {@link OpportunitiesService}. + */ +public inherited sharing class OpportunityCreateInvoiceController { - public Decimal DiscountPercentage { get; set; } - - private ApexPages.StandardController standardController = null; - - private ApexPages.StandardSetController standardSetController = null; - - public OpportunityCreateInvoiceController(ApexPages.StandardController controller) - { - standardController = controller; - } - - public OpportunityCreateInvoiceController(ApexPages.StandardSetController controller) - { - standardSetController = controller; - } - - public PageReference createInvoice() + /** + * Creates an invoice for a single Opportunity from the Lightning action. + * @param opportunityId Opportunity Id to invoice. Cannot be null. + * @param discountPercentage Optional discount applied before invoicing when greater than zero. + * @return Id of the first created Invoice__c. + */ + @AuraEnabled(cacheable=false) + public static Id createInvoice(Id opportunityId, Decimal discountPercentage) { try { - // Create Invoice the Opportunity - Set invoiceIds = OpportunitiesService.createInvoices( - new Set { standardController.getId() }, DiscountPercentage); - - // Redirect to Invoice - return new PageReference('/'+new List(invoiceIds)[0]); + Set invoiceIds = OpportunitiesService.newInstance().createInvoices(new Set{ opportunityId }, discountPercentage); + return new List(invoiceIds)[0]; } - catch (Exception e) + catch (Exception e) { - ApexPages.addMessages(e); - } - - return ApexPages.hasMessages() ? null : standardController.view(); + throw toAura(e); + } } - - public PageReference createInvoices() + + private static AuraHandledException toAura(Exception e) { - try - { - // Selected Opportunity Id's - Set selectedOpportunityIds = - new Map(standardSetController.getSelected()).keySet(); - - // Create invoices for selected Invoices, optionally applying discount before hand - OpportunitiesService.createInvoices(selectedOpportunityIds, DiscountPercentage); - - // Confirm - ApexPages.addMessage(new ApexPages.Message(ApexPages.Severity.INFO, 'Invoices created.')); - } - catch (Exception e) - { - ApexPages.addMessages(e); - } - - return null; - } -} \ No newline at end of file + AuraHandledException auraException = new AuraHandledException(e.getMessage()); + auraException.setMessage(e.getMessage()); + return auraException; + } +} diff --git a/sfdx-source/apex-common-samplecode/main/classes/controllers/OpportunityCreateInvoiceController.cls-meta.xml b/sfdx-source/apex-common-samplecode/main/classes/controllers/OpportunityCreateInvoiceController.cls-meta.xml index 5f399c3..b009296 100644 --- a/sfdx-source/apex-common-samplecode/main/classes/controllers/OpportunityCreateInvoiceController.cls-meta.xml +++ b/sfdx-source/apex-common-samplecode/main/classes/controllers/OpportunityCreateInvoiceController.cls-meta.xml @@ -1,5 +1,5 @@ - 63.0 + 67.0 Active diff --git a/sfdx-source/apex-common-samplecode/main/classes/controllers/OpportunityCreateInvoicesController.cls b/sfdx-source/apex-common-samplecode/main/classes/controllers/OpportunityCreateInvoicesController.cls new file mode 100644 index 0000000..bec59ef --- /dev/null +++ b/sfdx-source/apex-common-samplecode/main/classes/controllers/OpportunityCreateInvoicesController.cls @@ -0,0 +1,64 @@ +/** + * Copyright (c) 2013-2014, FinancialForce.com, inc + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * - Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * - Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * - Neither the name of the FinancialForce.com, inc nor the names of its contributors + * may be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL + * THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY + * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +**/ + +/** + * Visualforce controller for the Create Invoices list-view button. + * Delegates invoice creation to {@link OpportunitiesService}. + */ +public inherited sharing class OpportunityCreateInvoicesController { + /** + * Optional discount percent applied before invoice creation + * (for example 10 for 10%). + */ + public Decimal DiscountPercentage { get; set; } + + private ApexPages.StandardSetController standardSetController; + + /** + * Creates a controller for a list StandardSetController (bulk create). + * @param controller StandardSetController with selected Opportunities. Cannot be null. + */ + public OpportunityCreateInvoicesController(ApexPages.StandardSetController controller) { + standardSetController = controller; + } + + /** + * Creates invoices for all selected Opportunities on the list view. + * @return Null so the list page remains; adds an info message on success. + */ + public PageReference createInvoices() { + try { + Set selectedOpportunityIds = + new Map(standardSetController.getSelected()).keySet(); + OpportunitiesService.newInstance() + .createInvoices(selectedOpportunityIds, DiscountPercentage); + ApexPages.addMessage(new ApexPages.Message(ApexPages.Severity.INFO, 'Invoices created.')); + } catch (Exception e) { + ApexPages.addMessages(e); + } + return null; + } +} diff --git a/sfdx-source/apex-common-samplecode/main/classes/controllers/OpportunityCreateInvoicesController.cls-meta.xml b/sfdx-source/apex-common-samplecode/main/classes/controllers/OpportunityCreateInvoicesController.cls-meta.xml new file mode 100644 index 0000000..b009296 --- /dev/null +++ b/sfdx-source/apex-common-samplecode/main/classes/controllers/OpportunityCreateInvoicesController.cls-meta.xml @@ -0,0 +1,5 @@ + + + 67.0 + Active + diff --git a/sfdx-source/apex-common-samplecode/main/classes/controllers/OpportunityInvoicingController.cls b/sfdx-source/apex-common-samplecode/main/classes/controllers/OpportunityInvoicingController.cls index 89c00b6..b605d13 100644 --- a/sfdx-source/apex-common-samplecode/main/classes/controllers/OpportunityInvoicingController.cls +++ b/sfdx-source/apex-common-samplecode/main/classes/controllers/OpportunityInvoicingController.cls @@ -25,37 +25,31 @@ **/ /** - * Small example showing a controller using the service layer to submit a Batch Apex job - **/ -public with sharing class OpportunityInvoicingController + * Controller that submits the Opportunity invoicing Batch Apex job from a headless Lightning action. + */ +public inherited sharing class OpportunityInvoicingController { - public Id JobId {get;set;} - - public OpportunityInvoicingController(ApexPages.StandardSetController standardSetControlelr) - { - ApexPages.addMessage( - new ApexPages.Message( - ApexPages.Severity.INFO, 'Click Start to scan Opportunities that are ready for invoicing and process them.')); - } - - public PageReference startJob() + /** + * Submits {@link CreateInvoicesJob} through {@link OpportunitiesService#submitInvoicingJob}. + * @return Async Apex job Id returned after a successful submit. + */ + @AuraEnabled(cacheable=false) + public static Id startJob() { try - { - // Ask the service to submit the job - JobId = OpportunitiesService.submitInvoicingJob(); - - // Confirm - ApexPages.addMessage( - new ApexPages.Message( - ApexPages.Severity.INFO, 'The job has been submitted, you will receive an email notificaiton of any issues.')); + { + return OpportunitiesService.newInstance().submitInvoicingJob(); } catch (Exception e) { - // Client displays any issues submitting the job - ApexPages.addMessages(e); + throw toAura(e); } - - return null; } -} \ No newline at end of file + + private static AuraHandledException toAura(Exception e) + { + AuraHandledException auraException = new AuraHandledException(e.getMessage()); + auraException.setMessage(e.getMessage()); + return auraException; + } +} diff --git a/sfdx-source/apex-common-samplecode/main/classes/controllers/OpportunityInvoicingController.cls-meta.xml b/sfdx-source/apex-common-samplecode/main/classes/controllers/OpportunityInvoicingController.cls-meta.xml index 5f399c3..b009296 100644 --- a/sfdx-source/apex-common-samplecode/main/classes/controllers/OpportunityInvoicingController.cls-meta.xml +++ b/sfdx-source/apex-common-samplecode/main/classes/controllers/OpportunityInvoicingController.cls-meta.xml @@ -1,5 +1,5 @@ - 63.0 + 67.0 Active diff --git a/sfdx-source/apex-common-samplecode/main/classes/controllers/QuickOpportunityWizardController.cls b/sfdx-source/apex-common-samplecode/main/classes/controllers/QuickOpportunityWizardController.cls index dd2fa69..a437dc9 100644 --- a/sfdx-source/apex-common-samplecode/main/classes/controllers/QuickOpportunityWizardController.cls +++ b/sfdx-source/apex-common-samplecode/main/classes/controllers/QuickOpportunityWizardController.cls @@ -25,112 +25,201 @@ **/ /** - * This controller is not using the pattern approach in this code sample version. See the other two controllers - * in this sample for an illustration of how the patterns are used. - **/ -public with sharing class QuickOpportunityWizardController -{ - public ViewState ViewState { get; set; } - - private ApexPages.StandardController standardController; - - public QuickOpportunityWizardController(ApexPages.StandardController controller) + * Wizard to create an Opportunity from recently used line items on an Account. + * Intentionally not pattern-based (matches sample). + */ +public inherited sharing class QuickOpportunityWizardController +{ + /** + * Loads default Opportunity name and recently used line items for the Account Lightning action. + * @param accountId Account Id whose recent opportunity products are offered. Cannot be null. + * @return Wizard state with default name and selectable line items. + */ + @AuraEnabled(cacheable=false) + public static ViewState getWizardData(Id accountId) { - standardController = controller; + try + { + Account account = [SELECT Id, Name FROM Account WHERE Id = :accountId]; + ViewState viewState = new ViewState(); + viewState.opportunityName = account.Name; + viewState.selectLineItemList = new List(); - // Create a new Opportunity defaulting from the Account - Account account = (Account) standardController.getRecord(); - viewState = new ViewState(); - viewState.Opportunity = new Opportunity(); - viewState.Opportunity.Name = account.Name; - viewState.Opportunity.AccountId = account.Id; - viewState.Opportunity.DiscountType__c = OpportunitySettings__c.getInstance().DiscountType__c; - viewState.SelectLineItemList = new List(); - - // Recently used Opportunity lines - List lines = - [SELECT - Id, - Description, - ListPrice, - OpportunityId, - PricebookEntryId, - Quantity, - SortOrder, - TotalPrice, - UnitPrice, - PricebookEntry.Id, - PricebookEntry.IsActive, - PricebookEntry.Name, - PricebookEntry.Pricebook2Id, - PricebookEntry.Product2Id, - PricebookEntry.ProductCode, - PricebookEntry.UnitPrice, - PricebookEntry.UseStandardPrice, - PricebookEntry.Pricebook2.Description, - PricebookEntry.Pricebook2.Id, - PricebookEntry.Pricebook2.IsActive, - PricebookEntry.Pricebook2.IsStandard, - PricebookEntry.Pricebook2.Name - FROM OpportunityLineItem - WHERE Opportunity.Account.Id = :account.Id - ORDER BY SystemModstamp - DESC LIMIT 10]; - - // Clone these to be available for selection for the new opportunity - for(OpportunityLineItem item : lines) + List lines = [ + SELECT + Id, + OpportunityId, + Description, + Quantity, + UnitPrice, + ListPrice, + PricebookEntryId, + SortOrder, + PricebookEntry.Name, + PricebookEntry.Product2Id, + PricebookEntry.Product2.DiscountingApproved__c, + PricebookEntry.Pricebook2.Name + FROM OpportunityLineItem + WHERE Opportunity.Account.Id = :account.Id + ORDER BY SystemModstamp DESC + LIMIT 10 + ]; + + Integer rowKey = 0; + for(OpportunityLineItem item : lines) + { + SelectLineItem sli = new SelectLineItem(); + sli.key = String.valueOf(rowKey++); + sli.selected = false; + sli.productName = item.PricebookEntry.Name; + sli.pricebookName = item.PricebookEntry.Pricebook2.Name; + sli.description = item.Description; + sli.listPrice = item.ListPrice; + sli.unitPrice = item.UnitPrice; + sli.quantity = item.Quantity; + sli.pricebookEntryId = item.PricebookEntryId; + viewState.selectLineItemList.add(sli); + } + return viewState; + } + catch (Exception e) { - SelectLineItem sli = new SelectLineItem(); - sli.OpportunityLineItem = item.clone(false, true, false, false); - sli.OpportunityLineItem.OpportunityId = null; // this will be set as the new opportunity Id - sli.OpportunityLineItem.TotalPrice = null; // must null this as the record also includes a unit price - sli.Selected = false; - viewState.SelectLineItemList.add(sli); + throw toAura(e); } } - - public PageReference createOpportunity() + + /** + * Inserts the Opportunity and selected line items, then returns the new Opportunity Id. + * @param accountId Account Id to own the Opportunity. Cannot be null. + * @param name Opportunity name. + * @param stageName Opportunity stage. + * @param closeDate Opportunity close date. + * @param selectLineItemList Line items from the wizard; only selected rows are inserted. + * @return Id of the inserted Opportunity. + */ + @AuraEnabled(cacheable=false) + public static Id createOpportunity( + Id accountId, + String name, + String stageName, + Date closeDate, + List selectLineItemList + ) { try { - // Insert new Opportunity - insert Viewstate.Opportunity; - - // Insert selected Opportunity Lines (cloned from those recently used) - List lines = new List(); - for(SelectLineItem item : ViewState.SelectLineItemList) + Opportunity opportunity = new Opportunity(); + opportunity.Name = name; + opportunity.AccountId = accountId; + opportunity.StageName = stageName; + opportunity.CloseDate = closeDate; + opportunity.DiscountType__c = OpportunitySettings__c.getInstance().DiscountType__c; + Database.insert(opportunity); + + List lines = new List(); + if(selectLineItemList!=null) { - if(item.Selected) + for(SelectLineItem item : selectLineItemList) { - OpportunityLineItem line = item.OpportunityLineItem; - line.OpportunityId = ViewState.Opportunity.Id; - lines.add(line); + if(item!=null && item.selected==true) + { + lines.add( + new OpportunityLineItem( + OpportunityId = opportunity.Id, + PricebookEntryId = item.pricebookEntryId, + Description = item.description, + Quantity = item.quantity, + UnitPrice = item.unitPrice + ) + ); + } } } - insert lines; - - // Redirect to the new Opportunity - return new PageReference('/' + viewState.Opportunity.Id); + if(!lines.isEmpty()) + { + Database.insert(lines); + } + return opportunity.Id; } catch (Exception e) { - ApexPages.addMessages(e); + throw toAura(e); } - - return null; } - public class ViewState + private static AuraHandledException toAura(Exception e) { - public Opportunity Opportunity {get;set;} - - public List SelectLineItemList {get;set;} + AuraHandledException auraException = new AuraHandledException(e.getMessage()); + auraException.setMessage(e.getMessage()); + return auraException; } + /** + * Holds wizard Opportunity defaults and selectable recent line items. + */ + public class ViewState +{ + /** + * Default Opportunity name, taken from the Account. + */ + @AuraEnabled + public String opportunityName { get; set; } + /** + * Recent line items the user may select to copy onto the new Opportunity. + */ + @AuraEnabled + public List selectLineItemList { get; set; } + } + + /** + * Wrapper for a cloned OpportunityLineItem and its selection flag in the wizard. + */ public class SelectLineItem - { - public OpportunityLineItem OpportunityLineItem {get;set;} - - public Boolean Selected {get;set;} +{ + /** + * Client-side row key for the datatable. + */ + @AuraEnabled + public String key { get; set; } + /** + * Whether the user selected this line for the new Opportunity. + */ + @AuraEnabled + public Boolean selected { get; set; } + /** + * Product name from the price book entry. + */ + @AuraEnabled + public String productName { get; set; } + /** + * Price book name from the price book entry. + */ + @AuraEnabled + public String pricebookName { get; set; } + /** + * Line description to insert when selected. + */ + @AuraEnabled + public String description { get; set; } + /** + * List price shown read-only in the wizard. + */ + @AuraEnabled + public Decimal listPrice { get; set; } + /** + * Unit price to insert when selected. + */ + @AuraEnabled + public Decimal unitPrice { get; set; } + /** + * Quantity to insert when selected. + */ + @AuraEnabled + public Decimal quantity { get; set; } + /** + * Price book entry used when inserting the line. + */ + @AuraEnabled + public Id pricebookEntryId { get; set; } } -} \ No newline at end of file +} diff --git a/sfdx-source/apex-common-samplecode/main/classes/controllers/QuickOpportunityWizardController.cls-meta.xml b/sfdx-source/apex-common-samplecode/main/classes/controllers/QuickOpportunityWizardController.cls-meta.xml index 5f399c3..b009296 100644 --- a/sfdx-source/apex-common-samplecode/main/classes/controllers/QuickOpportunityWizardController.cls-meta.xml +++ b/sfdx-source/apex-common-samplecode/main/classes/controllers/QuickOpportunityWizardController.cls-meta.xml @@ -1,5 +1,5 @@ - 63.0 + 67.0 Active diff --git a/sfdx-source/apex-common-samplecode/main/classes/domains/AbstractChargeable.cls b/sfdx-source/apex-common-samplecode/main/classes/domains/AbstractChargeable.cls index 04dac51..677c425 100644 --- a/sfdx-source/apex-common-samplecode/main/classes/domains/AbstractChargeable.cls +++ b/sfdx-source/apex-common-samplecode/main/classes/domains/AbstractChargeable.cls @@ -24,39 +24,62 @@ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. **/ -public with sharing abstract class AbstractChargeable extends fflib_SObjectDomain - implements IChargeable +/** + * Abstract chargeable work-item domain that derives cost as hours worked times 100. + * Concrete subclasses supply the cost field and hour calculation for their SObject type. + */ +public abstract inherited sharing class AbstractChargeable extends fflib_SObjectDomain { + /** + * Creates a domain wrapping the given records. + * @param records Work-item records to manage. Null is treated as an empty list. + */ public AbstractChargeable(List records) { super(records); } - + + /** + * Returns the SObject field that stores the calculated cost of hours worked. + * @return Schema field token for the cost field on the concrete work-item type. + */ public abstract Schema.SObjectField getCostOfHoursWorkedField(); - + + /** + * Calculates hours worked for each record in domain order. + * @return Hours worked per record; size matches {@link #getRecords()}. + */ public abstract List calculateHoursWorked(); - + + /** + * Before-insert hook that recalculates cost of hours worked on all records. + */ public override void onBeforeInsert() { - updateCostOfHoursWorked(); + updateCostOfHoursWorked(); } - - public override void onBeforeUpdate(Map existingRecords) + + /** + * Before-update hook that recalculates cost of hours worked on all records. + * @param existingRecords Prior versions of the records keyed by Id. May be unused by the base implementation. + */ + public override void onBeforeUpdate(Map existingRecords) { updateCostOfHoursWorked(); - } - - public void updateCostOfHoursWorked() + } + + /** + * Sets each record's cost field to hours worked times 100 using + * {@link #calculateHoursWorked()} and {@link #getCostOfHoursWorkedField()}. + */ + public virtual void updateCostOfHoursWorked() { List hoursWorked = calculateHoursWorked(); - Schema.SObjectField costOfHoursWorkedField = getCostOfHoursWorkedField(); - - Integer hoursWorkedRecordIdx = 0; - - for(SObject record : this.records) + Integer hoursWorkedRecordIdx = 0; + for(SObject record : Records) { - record.put(costOfHoursWorkedField, hoursWorked[hoursWorkedRecordIdx++] * 100); + record.put(costOfHoursWorkedField, hoursWorked[hoursWorkedRecordIdx++] * 100); } } -} \ No newline at end of file +} diff --git a/sfdx-source/apex-common-samplecode/main/classes/domains/AbstractChargeable.cls-meta.xml b/sfdx-source/apex-common-samplecode/main/classes/domains/AbstractChargeable.cls-meta.xml index 5f399c3..b009296 100644 --- a/sfdx-source/apex-common-samplecode/main/classes/domains/AbstractChargeable.cls-meta.xml +++ b/sfdx-source/apex-common-samplecode/main/classes/domains/AbstractChargeable.cls-meta.xml @@ -1,5 +1,5 @@ - 63.0 + 67.0 Active diff --git a/sfdx-source/apex-common-samplecode/main/classes/domains/Accounts.cls b/sfdx-source/apex-common-samplecode/main/classes/domains/Accounts.cls index e889c08..ba7548d 100644 --- a/sfdx-source/apex-common-samplecode/main/classes/domains/Accounts.cls +++ b/sfdx-source/apex-common-samplecode/main/classes/domains/Accounts.cls @@ -24,49 +24,65 @@ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. **/ -public with sharing class Accounts extends fflib_SObjects - implements IAccounts +/** + * Domain logic for Account records, including opportunity activity description updates. + */ +public virtual inherited sharing class Accounts extends fflib_SObjects { - public static final String OPPORTUNITY_RAISED_DESCRIPTION = 'Last Opportunity Raised'; + @TestVisible + private static Accounts mock; - public static IAccounts newInstance(List recordList) - { - return (IAccounts) Application.Domain.newInstance(recordList); - } - - public static IAccounts newInstance(Set recordIdSet) + /** + * Factory that wraps the given Account records, or {@link #mock} when tests have set one. + * @param recordList Accounts to manage. Null is treated as an empty list. + * @return {@link Accounts} instance. + */ + public static Accounts newInstance(List recordList) { - return (IAccounts) Application.Domain.newInstance(recordIdSet); + return mock!=null ? mock : new Accounts(recordList); } - + + /** + * Prefix written to Account.Description when opportunity activity is recorded. + */ + public static final String OPPORTUNITY_RAISED_DESCRIPTION = 'Last Opportunity Raised'; + + /** + * Creates a domain wrapping the given Account records. + * @param sObjectList Accounts to manage. Null is treated as an empty list. + */ public Accounts(List sObjectList) { - super(sObjectList, Schema.Account.SObjectType); + super(sObjectList==null ? new List() : sObjectList); } + /** + * Returns the Account records held by this domain. + * @return The in-memory Account list; never null. Same as {@link #getRecords()}. + */ public List getAccounts() { return (List) getRecords(); } + /** + * Sets Description on every Account in the domain. + * @param description Value to assign to Account.Description. + */ public void setDescription(String description) { - for (Account account : getAccounts()) + for(Account account : (List) getRecords()) { account.Description = description; } } + /** + * Updates Description to record that an opportunity was raised today. + * Uses {@link #OPPORTUNITY_RAISED_DESCRIPTION} plus {@code System.today()}. + */ public void updateOpportunityActivity() { setDescription(OPPORTUNITY_RAISED_DESCRIPTION + ' ' + System.today()); } - - public class Constructor implements fflib_IDomainConstructor - { - public fflib_SObjects construct(List objectList) - { - return new Accounts((List) objectList); - } - } -} \ No newline at end of file +} diff --git a/sfdx-source/apex-common-samplecode/main/classes/domains/Accounts.cls-meta.xml b/sfdx-source/apex-common-samplecode/main/classes/domains/Accounts.cls-meta.xml index 5f399c3..b009296 100644 --- a/sfdx-source/apex-common-samplecode/main/classes/domains/Accounts.cls-meta.xml +++ b/sfdx-source/apex-common-samplecode/main/classes/domains/Accounts.cls-meta.xml @@ -1,5 +1,5 @@ - 63.0 + 67.0 Active diff --git a/sfdx-source/apex-common-samplecode/main/classes/domains/DeveloperWorkItems.cls b/sfdx-source/apex-common-samplecode/main/classes/domains/DeveloperWorkItems.cls index 09a5fd7..743516c 100644 --- a/sfdx-source/apex-common-samplecode/main/classes/domains/DeveloperWorkItems.cls +++ b/sfdx-source/apex-common-samplecode/main/classes/domains/DeveloperWorkItems.cls @@ -24,50 +24,101 @@ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. **/ -public with sharing class DeveloperWorkItems extends AbstractChargeable - implements IDeveloperWorkItems +/** + * Domain for DeveloperWorkItem__c that calculates consultancy cost and builds invoice DTOs. + * Implements {@link InvoicingService.ISupportInvoicing} for polymorphic invoicing. + */ +public virtual inherited sharing class DeveloperWorkItems extends AbstractChargeable + implements InvoicingService.ISupportInvoicing { - public static IDeveloperWorkItems newInstance(List recordList) - { - return (IDeveloperWorkItems) Application.Domain.newInstance(recordList); - } - public static IDeveloperWorkItems newInstance(Set recordIdSet) + @TestVisible + private static DeveloperWorkItems mock; + + /** + * Factory that wraps the given developer work items, or {@link #mock} when tests have set one. + * @param recordList Developer work items to manage. Null is treated as an empty list. + * @return {@link DeveloperWorkItems} instance. + */ + public static DeveloperWorkItems newInstance(List recordList) { - return (IDeveloperWorkItems) Application.Domain.newInstance(recordIdSet); + return mock!=null ? mock : new DeveloperWorkItems(recordList); } - + + /** + * Creates a domain wrapping the given developer work items. + * @param workItems Developer work items to manage. Null is treated as an empty list. + */ public DeveloperWorkItems(List workItems) { - super(workItems); + super(workItems==null ? new List() : workItems); } - + + /** + * Returns the field that stores calculated developer cost. + * @return {@code DeveloperWorkItem__c.DeveloperCost__c}. + */ public override Schema.SObjectField getCostOfHoursWorkedField() { - // Field base class will populate once it has calculated the cost return DeveloperWorkItem__c.DeveloperCost__c; } - + + /** + * Sums coding, review, and technical design hours for each work item. + * @return Hours worked per record in domain order. + */ public override List calculateHoursWorked() { - // Calculate the hours worked for each record - List hoursWorked = new List(); - for(DeveloperWorkItem__c workItem : (List) this.records) + List hoursWorked = new List(); + for(DeveloperWorkItem__c workItem : (List) Records) { - // This calculation is intentionally simplistic for illustration purposes - hoursWorked.add((Integer) - (workItem.CodingHours__c + - workItem.CodeReviewingHours__c + - workItem.TechnicalDesignHours__c)); + hoursWorked.add( + (Integer) ( + workItem.CodingHours__c + + workItem.CodeReviewingHours__c + + workItem.TechnicalDesignHours__c + ) + ); } return hoursWorked; - } - + } + + /** + * Adds one invoice per work item that has a related Work Order account. + * Unit price comes from DeveloperCost__c; work items without an account are skipped. + * @param invoiceFactory Factory that collects invoice headers and lines. Cannot be null. + */ + public virtual void generate(InvoicingService.InvoiceFactory invoiceFactory) + { + for(DeveloperWorkItem__c wi : (List) Records) + { + Id accountId = wi.WorkOrder__r!=null ? wi.WorkOrder__r.Account__c : null; + if(accountId==null) + { + continue; + } + InvoicingService.Invoice invoice = new InvoicingService.Invoice(); + invoice.Account = accountId; + invoice.Description = 'Developer work: ' + (wi.Name!=null ? wi.Name : 'Work Item'); + invoice.InvoiceDate = Date.today(); + InvoicingService.InvoiceLine line = new InvoicingService.InvoiceLine(); + line.Description = 'Developer consultancy'; + line.Product = null; + line.UnitPrice = wi.DeveloperCost__c!=null ? wi.DeveloperCost__c : 0; + line.Quantity = 1; + invoice.Lines = new List{ line }; + invoiceFactory.add(invoice); + } + } + + /** + * Constructs this domain from a trigger record list. + */ public class Constructor implements fflib_SObjectDomain.IConstructable { public fflib_SObjectDomain construct(List sObjectList) { - return new DeveloperWorkItems(sObjectList); + return new DeveloperWorkItems((List) sObjectList); } - } -} \ No newline at end of file + } +} diff --git a/sfdx-source/apex-common-samplecode/main/classes/domains/DeveloperWorkItems.cls-meta.xml b/sfdx-source/apex-common-samplecode/main/classes/domains/DeveloperWorkItems.cls-meta.xml index 5f399c3..b009296 100644 --- a/sfdx-source/apex-common-samplecode/main/classes/domains/DeveloperWorkItems.cls-meta.xml +++ b/sfdx-source/apex-common-samplecode/main/classes/domains/DeveloperWorkItems.cls-meta.xml @@ -1,5 +1,5 @@ - 63.0 + 67.0 Active diff --git a/sfdx-source/apex-common-samplecode/main/classes/domains/IDeveloperWorkItems.cls b/sfdx-source/apex-common-samplecode/main/classes/domains/IDeveloperWorkItems.cls deleted file mode 100755 index 54590a2..0000000 --- a/sfdx-source/apex-common-samplecode/main/classes/domains/IDeveloperWorkItems.cls +++ /dev/null @@ -1,3 +0,0 @@ -public interface IDeveloperWorkItems extends IChargeable -{ -} diff --git a/sfdx-source/apex-common-samplecode/main/classes/domains/IDeveloperWorkItems.cls-meta.xml b/sfdx-source/apex-common-samplecode/main/classes/domains/IDeveloperWorkItems.cls-meta.xml deleted file mode 100755 index 5f399c3..0000000 --- a/sfdx-source/apex-common-samplecode/main/classes/domains/IDeveloperWorkItems.cls-meta.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - 63.0 - Active - diff --git a/sfdx-source/apex-common-samplecode/main/classes/domains/IOpportunities.cls b/sfdx-source/apex-common-samplecode/main/classes/domains/IOpportunities.cls deleted file mode 100644 index 3fb4a63..0000000 --- a/sfdx-source/apex-common-samplecode/main/classes/domains/IOpportunities.cls +++ /dev/null @@ -1,31 +0,0 @@ -/** - * Copyright (c) 2012, FinancialForce.com, inc - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, - * are permitted provided that the following conditions are met: - * - * - Redistributions of source code must retain the above copyright notice, - * this list of conditions and the following disclaimer. - * - Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation - * and/or other materials provided with the distribution. - * - Neither the name of the FinancialForce.com, inc nor the names of its contributors - * may be used to endorse or promote products derived from this software without - * specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND - * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES - * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL - * THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, - * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS - * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY - * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) - * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -**/ - -public interface IOpportunities extends fflib_ISObjects -{ - void applyDiscount(Decimal discountPercentage, fflib_ISObjectUnitOfWork uow); - Set getAccountIds(); -} \ No newline at end of file diff --git a/sfdx-source/apex-common-samplecode/main/classes/domains/IOpportunities.cls-meta.xml b/sfdx-source/apex-common-samplecode/main/classes/domains/IOpportunities.cls-meta.xml deleted file mode 100644 index 5f399c3..0000000 --- a/sfdx-source/apex-common-samplecode/main/classes/domains/IOpportunities.cls-meta.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - 63.0 - Active - diff --git a/sfdx-source/apex-common-samplecode/main/classes/domains/IOpportunityLineItems.cls b/sfdx-source/apex-common-samplecode/main/classes/domains/IOpportunityLineItems.cls deleted file mode 100644 index 7e723b6..0000000 --- a/sfdx-source/apex-common-samplecode/main/classes/domains/IOpportunityLineItems.cls +++ /dev/null @@ -1,30 +0,0 @@ -/** - * Copyright (c) 2012-2014, FinancialForce.com, inc - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, - * are permitted provided that the following conditions are met: - * - * - Redistributions of source code must retain the above copyright notice, - * this list of conditions and the following disclaimer. - * - Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation - * and/or other materials provided with the distribution. - * - Neither the name of the FinancialForce.com, inc nor the names of its contributors - * may be used to endorse or promote products derived from this software without - * specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND - * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES - * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL - * THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, - * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS - * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY - * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) - * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -**/ - -public interface IOpportunityLineItems extends fflib_ISObjectDomain -{ - void applyDiscount(IOpportunities opps, Decimal discountPercentage, fflib_ISObjectUnitOfWork uow); -} \ No newline at end of file diff --git a/sfdx-source/apex-common-samplecode/main/classes/domains/IOpportunityLineItems.cls-meta.xml b/sfdx-source/apex-common-samplecode/main/classes/domains/IOpportunityLineItems.cls-meta.xml deleted file mode 100644 index 5f399c3..0000000 --- a/sfdx-source/apex-common-samplecode/main/classes/domains/IOpportunityLineItems.cls-meta.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - 63.0 - Active - diff --git a/sfdx-source/apex-common-samplecode/main/classes/domains/Opportunities.cls b/sfdx-source/apex-common-samplecode/main/classes/domains/Opportunities.cls index 567c080..00c5a24 100644 --- a/sfdx-source/apex-common-samplecode/main/classes/domains/Opportunities.cls +++ b/sfdx-source/apex-common-samplecode/main/classes/domains/Opportunities.cls @@ -24,28 +24,42 @@ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. **/ -public class Opportunities extends fflib_SObjects - implements IOpportunities, InvoicingService.ISupportInvoicing +/** + * Domain for Opportunity records that supports discounting and invoice generation. + * Implements {@link InvoicingService.ISupportInvoicing} for polymorphic invoicing. + */ +public virtual inherited sharing class Opportunities extends fflib_SObjects + implements InvoicingService.ISupportInvoicing { - public static IOpportunities newInstance(List recordList) - { - return (IOpportunities) Application.Domain.newInstance(recordList); - } + @TestVisible + private static Opportunities mock; - public static IOpportunities newInstance(Set recordIdSet) + /** + * Factory that wraps the given Opportunity records, or {@link #mock} when tests have set one. + * @param recordList Opportunities to manage. Null is treated as an empty list. + * @return {@link Opportunities} instance. + */ + public static Opportunities newInstance(List recordList) { - return (IOpportunities) Application.Domain.newInstance(recordIdSet); + return mock!=null ? mock : new Opportunities(recordList); } - + + /** + * Creates a domain wrapping the given Opportunity records. + * @param sObjectList Opportunities to manage. Null is treated as an empty list. + */ public Opportunities(List sObjectList) { - // Domain classes are initialised with lists to enforce bulkification throughout - super(sObjectList); + super(sObjectList==null ? new List() : sObjectList); } - - public void generate(InvoicingService.InvoiceFactory invoiceFactory) + + /** + * Builds invoice DTOs from each Opportunity and its line items into the factory. + * Invoice date is CloseDate plus 14 days; product comes from the pricebook entry. + * @param invoiceFactory Factory that collects invoice headers and lines. Cannot be null. + */ + public virtual void generate(InvoicingService.InvoiceFactory invoiceFactory) { - // Utilise InvoiceFactory to create invoices from Opportunity details for(Opportunity opportunity : (List) getRecords()) { InvoicingService.Invoice invoice = new InvoicingService.Invoice(); @@ -53,65 +67,70 @@ public class Opportunities extends fflib_SObjects invoice.Description = opportunity.Description; invoice.InvoiceDate = opportunity.CloseDate.addDays(14); invoice.Lines = new List(); - for(OpportunityLineItem lineItem : opportunity.OpportunityLineItems) + if(opportunity.OpportunityLineItems!=null) { - InvoicingService.InvoiceLine invoiceLineItem = new InvoicingService.InvoiceLine(); - invoiceLineItem.Description = lineItem.Description; - invoiceLineItem.Product = lineItem.PricebookEntry.Product2Id; - invoiceLineItem.UnitPrice = lineItem.UnitPrice; - invoiceLineItem.Quantity = lineItem.Quantity; - invoice.Lines.add(invoiceLineItem); - } + for(OpportunityLineItem lineItem : opportunity.OpportunityLineItems) + { + InvoicingService.InvoiceLine invoiceLineItem = new InvoicingService.InvoiceLine(); + invoiceLineItem.Description = lineItem.Description; + invoiceLineItem.Product = lineItem.PricebookEntry.Product2Id; + invoiceLineItem.UnitPrice = lineItem.UnitPrice; + invoiceLineItem.Quantity = lineItem.Quantity; + invoice.Lines.add(invoiceLineItem); + } + } invoiceFactory.add(invoice); } } - public void applyDiscount(Decimal discountPercentage, fflib_ISObjectUnitOfWork uow) + /** + * Applies a percentage discount to Opportunities without lines, or to their line items. + * Registers dirty records on the Unit of Work. Does not commit. + * @param discountPercentage Discount percent to apply (for example 10 for 10%). Null is treated as zero. + * @param uow Unit of Work that receives dirty Opportunity / line item records. Cannot be null. + */ + public virtual void applyDiscount(Decimal discountPercentage, fflib_ISObjectUnitOfWork uow) { - // Calculate discount factor Decimal factor = calculateDiscountFactor(discountPercentage); - - // Opportunity lines to apply discount to List linesToApplyDiscount = new List(); - - // Apply discount + for(Opportunity opportunity : (List) getRecords()) { - // Apply to the Opportunity Amount? - if(opportunity.OpportunityLineItems == null - || opportunity.OpportunityLineItems.isEmpty()) + if(opportunity.OpportunityLineItems==null || opportunity.OpportunityLineItems.isEmpty()) { - // Adjust the Amount on the Opportunity if no lines opportunity.Amount = opportunity.Amount * factor; uow.registerDirty(opportunity); } else { - // Collect lines to apply discount to linesToApplyDiscount.addAll(opportunity.OpportunityLineItems); } - } - - // Apply discount to lines - IOpportunityLineItems lineItems = (IOpportunityLineItems) Application.Domain.newInstance(linesToApplyDiscount); + } - lineItems.applyDiscount(this, discountPercentage, uow); + OpportunityLineItems.newInstance(linesToApplyDiscount).applyDiscount(this, discountPercentage, uow); } + /** + * Converts a discount percentage into a multiplicative factor. + * @param discountPercentage Discount percent (for example 10 for 10%). Null is treated as zero. + * @return Factor to multiply prices by (for example 0.9 for a 10% discount). + */ public static Decimal calculateDiscountFactor(Decimal discountPercentage) { - // Calculate discount factor Decimal discountProportion = discountPercentage==null ? 0 : discountPercentage / 100; - Decimal factor = 1 - discountProportion; - return factor; + return 1 - discountProportion; } + /** + * Collects distinct Account Ids from the Opportunities in this domain. + * @return Account Ids present on the records; empty when none have an AccountId. + */ public Set getAccountIds() { Set result = new Set(); - for (Opportunity record : (List) getRecords()) + for(Opportunity record : (List) getRecords()) { - if (record.AccountId != null) + if(record.AccountId!=null) { result.add(record.AccountId); } @@ -119,11 +138,14 @@ public class Opportunities extends fflib_SObjects return result; } + /** + * Constructs this domain from a record list for {@link fflib_Application.DomainFactory}. + */ public class Constructor implements fflib_IDomainConstructor { - public fflib_SObjects construct(List objectList) + public fflib_IDomain construct(List objects) { - return new Opportunities((List) objectList); + return new Opportunities((List) objects); } } -} \ No newline at end of file +} diff --git a/sfdx-source/apex-common-samplecode/main/classes/domains/Opportunities.cls-meta.xml b/sfdx-source/apex-common-samplecode/main/classes/domains/Opportunities.cls-meta.xml index 5f399c3..b009296 100644 --- a/sfdx-source/apex-common-samplecode/main/classes/domains/Opportunities.cls-meta.xml +++ b/sfdx-source/apex-common-samplecode/main/classes/domains/Opportunities.cls-meta.xml @@ -1,5 +1,5 @@ - 63.0 + 67.0 Active diff --git a/sfdx-source/apex-common-samplecode/main/classes/domains/OpportunityLineItems.cls b/sfdx-source/apex-common-samplecode/main/classes/domains/OpportunityLineItems.cls index a3ce517..78a0bb9 100644 --- a/sfdx-source/apex-common-samplecode/main/classes/domains/OpportunityLineItems.cls +++ b/sfdx-source/apex-common-samplecode/main/classes/domains/OpportunityLineItems.cls @@ -24,58 +24,59 @@ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. **/ -public with sharing class OpportunityLineItems extends fflib_SObjectDomain - implements IOpportunityLineItems +/** + * Domain for OpportunityLineItem discounting based on parent Opportunity discount rules. + */ +public virtual inherited sharing class OpportunityLineItems extends fflib_SObjects { - public static IOpportunityLineItems newInstance(List recordList) - { - return (IOpportunityLineItems) Application.Domain.newInstance(recordList); - } + @TestVisible + private static OpportunityLineItems mock; - public static IOpportunityLineItems newInstance(Set recordIdSet) + /** + * Factory that wraps the given line items, or {@link #mock} when tests have set one. + * @param recordList Opportunity line items to manage. Null is treated as an empty list. + * @return {@link OpportunityLineItems} instance. + */ + public static OpportunityLineItems newInstance(List recordList) { - return (IOpportunityLineItems) Application.Domain.newInstance(recordIdSet); + return mock!=null ? mock : new OpportunityLineItems(recordList); } - + + /** + * Creates a domain wrapping the given OpportunityLineItem records. + * @param sObjectList Opportunity line items to manage. Null is treated as an empty list. + */ public OpportunityLineItems(List sObjectList) { - super(sObjectList); + super(sObjectList==null ? new List() : sObjectList); } - public void applyDiscount(IOpportunities opps, Decimal discountPercentage, fflib_ISObjectUnitOfWork uow) + /** + * Applies a percentage discount to UnitPrice on the wrapped line items. + * When the parent Opportunity DiscountType__c is Approved Products, only lines whose + * product has DiscountingApproved__c true are changed. Registers a minimal dirty + * record (Id + UnitPrice) on the Unit of Work so user-mode DML does not touch unread fields. + * @param opps Parent opportunities providing DiscountType__c context. Cannot be null. + * @param discountPercentage Discount percent to apply. Null is treated as zero by {@link Opportunities#calculateDiscountFactor}. + * @param uow Unit of Work that receives dirty line items. Cannot be null. + */ + public virtual void applyDiscount(Opportunities opps, Decimal discountPercentage, fflib_ISObjectUnitOfWork uow) { - // Opportunties by Id - Map opportunitiesById = - new Map((List) opps.getRecords()); - - // Calculate discount factor + Map opportunitiesById = new Map((List) opps.getRecords()); Decimal factor = Opportunities.calculateDiscountFactor(discountPercentage); - - // Adjust UnitPrice of each line according to Discount Type of Opportunity - for(OpportunityLineItem line : (List) this.records) + + for(OpportunityLineItem line : (List) getRecords()) { - // Skip products that have not been approved for discounting Opportunity opportunity = opportunitiesById.get(line.OpportunityId); - if (opportunity.DiscountType__c == 'Approved Products') + if(opportunity.DiscountType__c=='Approved Products') { - if (line.PricebookEntry.Product2.DiscountingApproved__c == false) + if(line.PricebookEntry.Product2.DiscountingApproved__c==false) { continue; } } - - // Minimal record (Id + UnitPrice only): USER_MODE DML enforces FLS on every field in the record; queried records carry OpportunityId, PricebookEntryId, etc. that trigger "fields being inaccessible". - OpportunityLineItem updateOli = new OpportunityLineItem(Id = line.Id); - updateOli.UnitPrice = line.UnitPrice * factor; - uow.registerDirty(updateOli); - } - } - - public class Constructor implements fflib_SObjectDomain.IConstructable - { - public fflib_SObjectDomain construct(List sObjectList) - { - return new OpportunityLineItems(sObjectList); + line.UnitPrice = line.UnitPrice * factor; + uow.registerDirty(new OpportunityLineItem(Id = line.Id, UnitPrice = line.UnitPrice)); } } -} \ No newline at end of file +} diff --git a/sfdx-source/apex-common-samplecode/main/classes/domains/OpportunityLineItems.cls-meta.xml b/sfdx-source/apex-common-samplecode/main/classes/domains/OpportunityLineItems.cls-meta.xml index 5f399c3..b009296 100644 --- a/sfdx-source/apex-common-samplecode/main/classes/domains/OpportunityLineItems.cls-meta.xml +++ b/sfdx-source/apex-common-samplecode/main/classes/domains/OpportunityLineItems.cls-meta.xml @@ -1,5 +1,5 @@ - 63.0 + 67.0 Active diff --git a/sfdx-source/apex-common-samplecode/main/classes/domains/TrainingWorkItems.cls b/sfdx-source/apex-common-samplecode/main/classes/domains/TrainingWorkItems.cls new file mode 100644 index 0000000..0db3031 --- /dev/null +++ b/sfdx-source/apex-common-samplecode/main/classes/domains/TrainingWorkItems.cls @@ -0,0 +1,110 @@ +/** + * Copyright (c) 2012-2014, FinancialForce.com, inc + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * - Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * - Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * - Neither the name of the FinancialForce.com, inc nor the names of its contributors + * may be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL + * THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY + * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +**/ + +/** + * Domain for TrainingWorkItem__c that calculates training cost and builds invoice DTOs. + * Implements {@link InvoicingService.ISupportInvoicing} for polymorphic invoicing. + */ +public virtual inherited sharing class TrainingWorkItems extends AbstractChargeable + implements InvoicingService.ISupportInvoicing { + + @TestVisible + private static TrainingWorkItems mock; + + /** + * Factory that wraps the given training work items, or {@link #mock} when tests have set one. + * @param recordList Training work items to manage. Null is treated as an empty list. + * @return {@link TrainingWorkItems} instance. + */ + public static TrainingWorkItems newInstance(List recordList) { + return mock != null ? mock : new TrainingWorkItems(recordList); + } + + /** + * Creates a domain wrapping the given training work items. + * @param workItems Training work items to manage. Null is treated as an empty list. + */ + public TrainingWorkItems(List workItems) { + super(workItems == null ? new List() : workItems); + } + + /** + * Returns the field that stores calculated training cost. + * @return {@code TrainingWorkItem__c.TrainingCost__c}. + */ + public override Schema.SObjectField getCostOfHoursWorkedField() { + return TrainingWorkItem__c.TrainingCost__c; + } + + /** + * Sums delivery and preparation hours for each work item; null hour fields count as zero. + * @return Hours worked per record in domain order. + */ + public override List calculateHoursWorked() { + List hoursWorked = new List(); + for (TrainingWorkItem__c wi : (List) Records) { + Decimal d = + (wi.DeliveryHours__c != null ? wi.DeliveryHours__c : 0) + + (wi.PreparationHours__c != null ? wi.PreparationHours__c : 0); + hoursWorked.add(d.intValue()); + } + return hoursWorked; + } + + /** + * Adds one invoice per work item that has a related Work Order account. + * Unit price comes from TrainingCost__c; work items without an account are skipped. + * @param invoiceFactory Factory that collects invoice headers and lines. Cannot be null. + */ + public virtual void generate(InvoicingService.InvoiceFactory invoiceFactory) { + for (TrainingWorkItem__c wi : (List) Records) { + Id accountId = wi.WorkOrder__r != null ? wi.WorkOrder__r.Account__c : null; + if (accountId == null) { + continue; + } + InvoicingService.Invoice invoice = new InvoicingService.Invoice(); + invoice.Account = accountId; + invoice.Description = 'Training: ' + (wi.Name != null ? wi.Name : 'Work Item'); + invoice.InvoiceDate = Date.today(); + InvoicingService.InvoiceLine line = new InvoicingService.InvoiceLine(); + line.Description = 'Training delivery'; + line.Product = null; + line.UnitPrice = wi.TrainingCost__c != null ? wi.TrainingCost__c : 0; + line.Quantity = 1; + invoice.Lines = new List{ line }; + invoiceFactory.add(invoice); + } + } + + /** + * Constructs this domain from a trigger record list. + */ + public class Constructor implements fflib_SObjectDomain.IConstructable { + public fflib_SObjectDomain construct(List sObjectList) { + return new TrainingWorkItems((List) sObjectList); + } + } +} diff --git a/sfdx-source/apex-common-samplecode/main/classes/domains/TrainingWorkItems.cls-meta.xml b/sfdx-source/apex-common-samplecode/main/classes/domains/TrainingWorkItems.cls-meta.xml new file mode 100644 index 0000000..b009296 --- /dev/null +++ b/sfdx-source/apex-common-samplecode/main/classes/domains/TrainingWorkItems.cls-meta.xml @@ -0,0 +1,5 @@ + + + 67.0 + Active + diff --git a/sfdx-source/apex-common-samplecode/main/classes/restapis/OpportunitiesResource.cls b/sfdx-source/apex-common-samplecode/main/classes/restapis/OpportunitiesResource.cls index 0bc129b..0d0da79 100644 --- a/sfdx-source/apex-common-samplecode/main/classes/restapis/OpportunitiesResource.cls +++ b/sfdx-source/apex-common-samplecode/main/classes/restapis/OpportunitiesResource.cls @@ -24,23 +24,25 @@ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. **/ +/** + * REST resource that applies a discount to a single Opportunity identified in the URL path. + * Path pattern: {@code /opportunities/{id}/applydiscount} with optional {@code discount} query parameter. + */ @RestResource(UrlMapping='/opportunities/*/applydiscount') -global with sharing class OpportunitiesResource +global inherited sharing class OpportunitiesResource { + /** + * Applies a discount to the Opportunity Id in the request URI via {@link OpportunitiesService}. + * Reads the {@code discount} query parameter; defaults to zero when absent. + */ @HttpPost - // e.g. https://ue2.salesforce.com/services/apexrest/opportunities/006b0000003u4Ln/applydiscount?discount=10 global static void applyDiscount() { - // Take apart the URI to discover the Opportunity Id - RestRequest req = RestContext.request; + RestRequest req = RestContext.request; String[] uriParts = req.requestURI.split('/'); - - // Determine service parameter values from REST context - Id opportunityId = uriParts[2]; // get from URL path + Id opportunityId = uriParts[2]; String discountParam = req.params.get('discount'); Decimal discountPercent = discountParam!=null ? Decimal.valueOf(discountParam) : 0; - - // Call Service Method - OpportunitiesService.applyDiscounts(new Set { opportunityId }, discountPercent ); + OpportunitiesService.newInstance().applyDiscounts(new Set{ opportunityId }, discountPercent); } -} \ No newline at end of file +} diff --git a/sfdx-source/apex-common-samplecode/main/classes/restapis/OpportunitiesResource.cls-meta.xml b/sfdx-source/apex-common-samplecode/main/classes/restapis/OpportunitiesResource.cls-meta.xml index 5f399c3..b009296 100644 --- a/sfdx-source/apex-common-samplecode/main/classes/restapis/OpportunitiesResource.cls-meta.xml +++ b/sfdx-source/apex-common-samplecode/main/classes/restapis/OpportunitiesResource.cls-meta.xml @@ -1,5 +1,5 @@ - 63.0 + 67.0 Active diff --git a/sfdx-source/apex-common-samplecode/main/classes/selectors/AccountsSelector.cls b/sfdx-source/apex-common-samplecode/main/classes/selectors/AccountsSelector.cls index 1dd1876..79911e0 100644 --- a/sfdx-source/apex-common-samplecode/main/classes/selectors/AccountsSelector.cls +++ b/sfdx-source/apex-common-samplecode/main/classes/selectors/AccountsSelector.cls @@ -25,51 +25,77 @@ **/ /** - * This is a basic selector for Account for this app, note how it implements selectByOpportunity - **/ -public class AccountsSelector extends fflib_SObjectSelector - implements IAccountsSelector + * Selector for Account queries used by account and opportunity activity flows. + */ +public virtual inherited sharing class AccountsSelector extends fflib_SObjectSelector { - public static IAccountsSelector newInstance() + @TestVisible + private static AccountsSelector mock; + + /** + * Factory that returns this selector, or {@link #mock} when tests have set one. + * @return {@link AccountsSelector} instance. + */ + public static AccountsSelector newInstance() { - return (IAccountsSelector) Application.Selector.newInstance(Account.SObjectType); + return mock!=null ? mock : new AccountsSelector(); } + /** + * Creates a selector that queries in user mode. + */ public AccountsSelector() { super(false, fflib_SObjectSelector.DataAccess.USER_MODE); } + /** + * Fields included in default queries from this selector. + * @return Account Id, Name, and Description. + */ public List getSObjectFieldList() { - return new List { + return new List{ Account.Id, Account.Name, Account.Description }; } + /** + * SObject type this selector queries. + * @return {@code Account.SObjectType}. + */ public Schema.SObjectType getSObjectType() { return Account.SObjectType; } + /** + * Selects Accounts by Id with Id, Name, and Description. + * @param idSet Account Ids to query. Empty set returns an empty list. + * @return Matching Accounts; empty list when none match. + */ public List selectById(Set idSet) { return (List) selectSObjectsById(idSet); } - + + /** + * Selects Accounts related to the given Opportunities via AccountId. + * @param opportunities Opportunities whose AccountId values are collected. Cannot be null. + * @return Matching Accounts; empty list when no AccountIds are present. + */ public List selectByOpportunity(List opportunities) { - // Related Accounts Set accountIds = new Set(); - for (Opportunity opp : (List) opportunities) + for(Opportunity opp : opportunities) { - if (opp.AccountId != null) + if(opp.AccountId!=null) { accountIds.add(opp.AccountId); } } return selectById(accountIds); - } -} \ No newline at end of file + } +} diff --git a/sfdx-source/apex-common-samplecode/main/classes/selectors/AccountsSelector.cls-meta.xml b/sfdx-source/apex-common-samplecode/main/classes/selectors/AccountsSelector.cls-meta.xml index 5f399c3..b009296 100644 --- a/sfdx-source/apex-common-samplecode/main/classes/selectors/AccountsSelector.cls-meta.xml +++ b/sfdx-source/apex-common-samplecode/main/classes/selectors/AccountsSelector.cls-meta.xml @@ -1,5 +1,5 @@ - 63.0 + 67.0 Active diff --git a/sfdx-source/apex-common-samplecode/main/classes/selectors/DeveloperWorkItemsSelector.cls b/sfdx-source/apex-common-samplecode/main/classes/selectors/DeveloperWorkItemsSelector.cls new file mode 100644 index 0000000..d5417a3 --- /dev/null +++ b/sfdx-source/apex-common-samplecode/main/classes/selectors/DeveloperWorkItemsSelector.cls @@ -0,0 +1,92 @@ +/** + * Copyright (c) 2012-2014, FinancialForce.com, inc + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * - Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * - Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * - Neither the name of the FinancialForce.com, inc nor the names of its contributors + * may be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL + * THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY + * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +**/ + +/** + * Selector for DeveloperWorkItem__c including related Work Order account for invoicing. + */ +public virtual inherited sharing class DeveloperWorkItemsSelector extends fflib_SObjectSelector { + @TestVisible + private static DeveloperWorkItemsSelector mock; + + /** + * Factory that returns this selector, or {@link #mock} when tests have set one. + * @return {@link DeveloperWorkItemsSelector} instance. + */ + public static DeveloperWorkItemsSelector newInstance() { + return mock != null ? mock : new DeveloperWorkItemsSelector(); + } + + /** + * Creates a selector that queries in user mode. + */ + public DeveloperWorkItemsSelector() { + super(false, fflib_SObjectSelector.DataAccess.USER_MODE); + } + + /** + * Fields included in default queries from this selector. + * @return DeveloperWorkItem__c Id, Name, WorkOrder__c, DeveloperCost__c, CodingHours__c, CodeReviewingHours__c, and TechnicalDesignHours__c. + */ + public List getSObjectFieldList() { + return new List{ + DeveloperWorkItem__c.Id, + DeveloperWorkItem__c.Name, + DeveloperWorkItem__c.WorkOrder__c, + DeveloperWorkItem__c.DeveloperCost__c, + DeveloperWorkItem__c.CodingHours__c, + DeveloperWorkItem__c.CodeReviewingHours__c, + DeveloperWorkItem__c.TechnicalDesignHours__c + }; + } + + /** + * SObject type this selector queries. + * @return {@code DeveloperWorkItem__c.SObjectType}. + */ + public Schema.SObjectType getSObjectType() { + return DeveloperWorkItem__c.SObjectType; + } + + /** + * Selects developer work items by Id with fields needed for cost and invoicing. + * @param idSet DeveloperWorkItem__c Ids to query. Empty set returns an empty list. + * @return Matching DeveloperWorkItem__c records; empty list when none match. + */ + public List selectById(Set idSet) { + return (List) selectSObjectsById(idSet); + } + + /** + * Factory {@code selectById} uses this method; invoicing needs Work Order account. + * @param idSet DeveloperWorkItem__c Ids to query. Empty set returns an empty list. + * @return Matching work items with WorkOrder__r.Account__c; empty list when none match. + */ + public override List selectSObjectsById(Set idSet) { + return Database.query( + newQueryFactory().selectField('WorkOrder__r.Account__c').setCondition('Id IN :idSet').toSOQL() + ); + } +} diff --git a/sfdx-source/apex-common-samplecode/main/classes/selectors/DeveloperWorkItemsSelector.cls-meta.xml b/sfdx-source/apex-common-samplecode/main/classes/selectors/DeveloperWorkItemsSelector.cls-meta.xml new file mode 100644 index 0000000..b009296 --- /dev/null +++ b/sfdx-source/apex-common-samplecode/main/classes/selectors/DeveloperWorkItemsSelector.cls-meta.xml @@ -0,0 +1,5 @@ + + + 67.0 + Active + diff --git a/sfdx-source/apex-common-samplecode/main/classes/selectors/IAccountsSelector.cls-meta.xml b/sfdx-source/apex-common-samplecode/main/classes/selectors/IAccountsSelector.cls-meta.xml deleted file mode 100644 index 5f399c3..0000000 --- a/sfdx-source/apex-common-samplecode/main/classes/selectors/IAccountsSelector.cls-meta.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - 63.0 - Active - diff --git a/sfdx-source/apex-common-samplecode/main/classes/selectors/IOpportunitiesSelector.cls b/sfdx-source/apex-common-samplecode/main/classes/selectors/IOpportunitiesSelector.cls deleted file mode 100644 index c7276f1..0000000 --- a/sfdx-source/apex-common-samplecode/main/classes/selectors/IOpportunitiesSelector.cls +++ /dev/null @@ -1,33 +0,0 @@ -/** - * Copyright (c) 2012-2014, FinancialForce.com, inc - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, - * are permitted provided that the following conditions are met: - * - * - Redistributions of source code must retain the above copyright notice, - * this list of conditions and the following disclaimer. - * - Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation - * and/or other materials provided with the distribution. - * - Neither the name of the FinancialForce.com, inc nor the names of its contributors - * may be used to endorse or promote products derived from this software without - * specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND - * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES - * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL - * THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, - * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS - * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY - * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) - * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -**/ - -public interface IOpportunitiesSelector extends fflib_ISObjectSelector -{ - List selectById(Set idSet); - List selectByIdWithProducts(Set idSet); - List selectOpportunityInfo(Set idSet); - Database.QueryLocator queryLocatorReadyToInvoice(); -} \ No newline at end of file diff --git a/sfdx-source/apex-common-samplecode/main/classes/selectors/IOpportunitiesSelector.cls-meta.xml b/sfdx-source/apex-common-samplecode/main/classes/selectors/IOpportunitiesSelector.cls-meta.xml deleted file mode 100644 index 5f399c3..0000000 --- a/sfdx-source/apex-common-samplecode/main/classes/selectors/IOpportunitiesSelector.cls-meta.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - 63.0 - Active - diff --git a/sfdx-source/apex-common-samplecode/main/classes/selectors/IOpportunityLineItemsSelector.cls b/sfdx-source/apex-common-samplecode/main/classes/selectors/IOpportunityLineItemsSelector.cls deleted file mode 100755 index d4ed1aa..0000000 --- a/sfdx-source/apex-common-samplecode/main/classes/selectors/IOpportunityLineItemsSelector.cls +++ /dev/null @@ -1,4 +0,0 @@ -public interface IOpportunityLineItemsSelector extends fflib_ISObjectSelector -{ - List selectRecentlyUsed(Set accountIds, Integer recordLimit); -} \ No newline at end of file diff --git a/sfdx-source/apex-common-samplecode/main/classes/selectors/IOpportunityLineItemsSelector.cls-meta.xml b/sfdx-source/apex-common-samplecode/main/classes/selectors/IOpportunityLineItemsSelector.cls-meta.xml deleted file mode 100755 index 5f399c3..0000000 --- a/sfdx-source/apex-common-samplecode/main/classes/selectors/IOpportunityLineItemsSelector.cls-meta.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - 63.0 - Active - diff --git a/sfdx-source/apex-common-samplecode/main/classes/selectors/IPricebookEntriesSelector.cls b/sfdx-source/apex-common-samplecode/main/classes/selectors/IPricebookEntriesSelector.cls deleted file mode 100755 index 1b30f3d..0000000 --- a/sfdx-source/apex-common-samplecode/main/classes/selectors/IPricebookEntriesSelector.cls +++ /dev/null @@ -1,4 +0,0 @@ -public interface IPricebookEntriesSelector extends fflib_ISObjectSelector -{ - -} \ No newline at end of file diff --git a/sfdx-source/apex-common-samplecode/main/classes/selectors/IPricebookEntriesSelector.cls-meta.xml b/sfdx-source/apex-common-samplecode/main/classes/selectors/IPricebookEntriesSelector.cls-meta.xml deleted file mode 100755 index 5f399c3..0000000 --- a/sfdx-source/apex-common-samplecode/main/classes/selectors/IPricebookEntriesSelector.cls-meta.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - 63.0 - Active - diff --git a/sfdx-source/apex-common-samplecode/main/classes/selectors/IPricebooksSelector.cls b/sfdx-source/apex-common-samplecode/main/classes/selectors/IPricebooksSelector.cls deleted file mode 100755 index 2170c7b..0000000 --- a/sfdx-source/apex-common-samplecode/main/classes/selectors/IPricebooksSelector.cls +++ /dev/null @@ -1,4 +0,0 @@ -public interface IPricebooksSelector extends fflib_ISObjectSelector -{ - -} diff --git a/sfdx-source/apex-common-samplecode/main/classes/selectors/IPricebooksSelector.cls-meta.xml b/sfdx-source/apex-common-samplecode/main/classes/selectors/IPricebooksSelector.cls-meta.xml deleted file mode 100755 index 5f399c3..0000000 --- a/sfdx-source/apex-common-samplecode/main/classes/selectors/IPricebooksSelector.cls-meta.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - 63.0 - Active - diff --git a/sfdx-source/apex-common-samplecode/main/classes/selectors/IProductsSelector.cls b/sfdx-source/apex-common-samplecode/main/classes/selectors/IProductsSelector.cls deleted file mode 100755 index b6ac0fe..0000000 --- a/sfdx-source/apex-common-samplecode/main/classes/selectors/IProductsSelector.cls +++ /dev/null @@ -1,4 +0,0 @@ -public interface IProductsSelector extends fflib_ISObjectSelector -{ - -} diff --git a/sfdx-source/apex-common-samplecode/main/classes/selectors/IProductsSelector.cls-meta.xml b/sfdx-source/apex-common-samplecode/main/classes/selectors/IProductsSelector.cls-meta.xml deleted file mode 100755 index 5f399c3..0000000 --- a/sfdx-source/apex-common-samplecode/main/classes/selectors/IProductsSelector.cls-meta.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - 63.0 - Active - diff --git a/sfdx-source/apex-common-samplecode/main/classes/selectors/IUsersSelector.cls b/sfdx-source/apex-common-samplecode/main/classes/selectors/IUsersSelector.cls deleted file mode 100755 index e43929d..0000000 --- a/sfdx-source/apex-common-samplecode/main/classes/selectors/IUsersSelector.cls +++ /dev/null @@ -1,4 +0,0 @@ -public interface IUsersSelector extends fflib_ISObjectSelector -{ - Map getUsersEmail(Set userIds); -} diff --git a/sfdx-source/apex-common-samplecode/main/classes/selectors/IUsersSelector.cls-meta.xml b/sfdx-source/apex-common-samplecode/main/classes/selectors/IUsersSelector.cls-meta.xml deleted file mode 100755 index 5f399c3..0000000 --- a/sfdx-source/apex-common-samplecode/main/classes/selectors/IUsersSelector.cls-meta.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - 63.0 - Active - diff --git a/sfdx-source/apex-common-samplecode/main/classes/selectors/OpportunitiesSelector.cls b/sfdx-source/apex-common-samplecode/main/classes/selectors/OpportunitiesSelector.cls index f79980f..96fecf1 100644 --- a/sfdx-source/apex-common-samplecode/main/classes/selectors/OpportunitiesSelector.cls +++ b/sfdx-source/apex-common-samplecode/main/classes/selectors/OpportunitiesSelector.cls @@ -24,89 +24,141 @@ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. **/ -public class OpportunitiesSelector extends fflib_SObjectSelector - implements IOpportunitiesSelector +/** + * Selector for Opportunity queries used by discounting, invoicing, and summary views. + */ +public virtual inherited sharing class OpportunitiesSelector extends fflib_SObjectSelector { - public static IOpportunitiesSelector newInstance() + @TestVisible + private static OpportunitiesSelector mock; + + /** + * Factory that returns this selector, or {@link #mock} when tests have set one. + * @return {@link OpportunitiesSelector} instance. + */ + public static OpportunitiesSelector newInstance() { - return (IOpportunitiesSelector) Application.Selector.newInstance(Opportunity.SObjectType); + return mock!=null ? mock : new OpportunitiesSelector(); } + /** + * Creates a selector that queries in user mode. + */ public OpportunitiesSelector() { super(false, fflib_SObjectSelector.DataAccess.USER_MODE); } + /** + * Fields included in default queries from this selector. + * @return Opportunity Id, AccountId, Amount, CloseDate, Description, DiscountType__c, Name, StageName, and InvoicedStatus__c. + */ public List getSObjectFieldList() { - return new List { + return new List{ + Opportunity.Id, Opportunity.AccountId, Opportunity.Amount, Opportunity.CloseDate, Opportunity.Description, - Opportunity.ExpectedRevenue, - Opportunity.Id, + Opportunity.DiscountType__c, Opportunity.Name, - Opportunity.Pricebook2Id, - Opportunity.Probability, Opportunity.StageName, - Opportunity.Type, - Opportunity.DiscountType__c + Opportunity.InvoicedStatus__c }; } - + + /** + * SObject type this selector queries. + * @return {@code Opportunity.SObjectType}. + */ public Schema.SObjectType getSObjectType() { return Opportunity.SObjectType; } + /** + * Selects Opportunities by Id using the Opportunity field list (no related Account fields). + * @param idSet Opportunity Ids to query. Empty set returns an empty list. + * @return Matching Opportunities; empty list when none match. + */ public List selectById(Set idSet) { return (List) selectSObjectsById(idSet); } + /** + * Factory {@code selectById} uses this method; invoicing needs line items. + * @param idSet Opportunity Ids to query. Empty set returns an empty list. + * @return Matching Opportunities with nested OpportunityLineItems; empty list when none match. + */ + public override List selectSObjectsById(Set idSet) + { + return selectByIdWithProducts(idSet); + } + + /** + * Selects Opportunities by Id including line items needed for discount and invoice flows. + * @param idSet Opportunity Ids to query. Empty set returns an empty list. + * @return Matching Opportunities with nested OpportunityLineItems; empty list when none match. + */ public List selectByIdWithProducts(Set idSet) { fflib_QueryFactory opportunitiesQueryFactory = newQueryFactory(); + opportunitiesQueryFactory.selectField('Account.Name'); + opportunitiesQueryFactory.selectField('Account.AccountNumber'); + opportunitiesQueryFactory.selectField('Account.Owner.Name'); + + fflib_QueryFactory lineItemsQueryFactory = OpportunityLineItemsSelector.newInstance() + .addQueryFactorySubselect(opportunitiesQueryFactory); - fflib_QueryFactory lineItemsQueryFactory = - new OpportunityLineItemsSelector(). - addQueryFactorySubselect(opportunitiesQueryFactory); - - new PricebookEntriesSelector(). - configureQueryFactoryFields(lineItemsQueryFactory, 'PricebookEntry'); - new ProductsSelector(). - configureQueryFactoryFields(lineItemsQueryFactory, 'PricebookEntry.Product2'); - new PricebooksSelector(). - configureQueryFactoryFields(lineItemsQueryFactory, 'PricebookEntry.Pricebook2'); + PricebookEntriesSelector.newInstance() + .configureQueryFactoryFields(lineItemsQueryFactory, 'PricebookEntry'); + ProductsSelector.newInstance() + .configureQueryFactoryFields(lineItemsQueryFactory, 'PricebookEntry.Product2'); + PricebooksSelector.newInstance() + .configureQueryFactoryFields(lineItemsQueryFactory, 'PricebookEntry.Pricebook2'); return (List) Database.query( - opportunitiesQueryFactory.setCondition('id in :idSet').toSOQL()); + opportunitiesQueryFactory.setCondition('Id IN :idSet').toSOQL() + ); } - - public List selectOpportunityInfo(Set idSet) - { - List opportunityInfos = new List(); - for(Opportunity opportunity : Database.query( newQueryFactory(false) - .selectField(Opportunity.Id) - .selectField(Opportunity.StageName) - .selectField(Opportunity.Amount) - .selectField('Account.Name') - .selectField('Account.AccountNumber') - .selectField('Account.Owner.Name') - .setCondition('id in :idSet') - .toSOQL() )) + /** + * Selects read-only opportunity summary DTOs including related Account and owner names. + * @param idSet Opportunity Ids to query. Empty set returns an empty list. + * @return {@link OpportunitySummary} wrappers; empty list when none match. + */ + public List selectOpportunitySummary(Set idSet) + { + List opportunitySummaries = new List(); + for( + Opportunity opportunity : (List) Database.query( + newQueryFactory(false) + .selectField(Opportunity.Id) + .selectField(Opportunity.Amount) + .selectField(Opportunity.StageName) + .selectField('Account.Name') + .selectField('Account.AccountNumber') + .selectField('Account.Owner.Name') + .setCondition('Id IN :idSet') + .toSOQL() + ) + ) { - opportunityInfos.add(new OpportunityInfo(opportunity)); + opportunitySummaries.add(new OpportunitySummary(opportunity)); } - - return opportunityInfos; + return opportunitySummaries; } - - public Database.QueryLocator queryLocatorReadyToInvoice() + + /** + * Returns a QueryLocator for Opportunities whose InvoicedStatus__c is Ready. + * @return QueryLocator suitable for Batch Apex start methods. Fields match {@link #selectById}. + */ + public Database.QueryLocator readyToInvoiceAsQueryLocator() { return Database.getQueryLocator( - newQueryFactory().setCondition(Opportunity.InvoicedStatus__c + ' = \'Ready\'').toSOQL()); + newQueryFactory().setCondition('InvoicedStatus__c = \'Ready\'').toSOQL() + ); } } diff --git a/sfdx-source/apex-common-samplecode/main/classes/selectors/OpportunitiesSelector.cls-meta.xml b/sfdx-source/apex-common-samplecode/main/classes/selectors/OpportunitiesSelector.cls-meta.xml index 5f399c3..b009296 100644 --- a/sfdx-source/apex-common-samplecode/main/classes/selectors/OpportunitiesSelector.cls-meta.xml +++ b/sfdx-source/apex-common-samplecode/main/classes/selectors/OpportunitiesSelector.cls-meta.xml @@ -1,5 +1,5 @@ - 63.0 + 67.0 Active diff --git a/sfdx-source/apex-common-samplecode/main/classes/selectors/OpportunityInfo.cls b/sfdx-source/apex-common-samplecode/main/classes/selectors/OpportunityInfo.cls deleted file mode 100644 index cb3ed8a..0000000 --- a/sfdx-source/apex-common-samplecode/main/classes/selectors/OpportunityInfo.cls +++ /dev/null @@ -1,17 +0,0 @@ -public class OpportunityInfo -{ - private Opportunity opportunity; - - public OpportunityInfo(Opportunity opportunity) - { - this.opportunity = opportunity; - } - - public Id Id { get { return this.opportunity.Id; } } - public Decimal Amount { get { return this.opportunity.Amount; } } - public String Stage { get { return this.opportunity.StageName; } } - public String AccountName { get { return this.opportunity.Account.Name; } } - public String AccountNumber { get { return this.opportunity.Account.AccountNumber; } } - public String AccountOwner { get { return this.opportunity.Account.Owner.Name; } } - -} diff --git a/sfdx-source/apex-common-samplecode/main/classes/selectors/OpportunityInfo.cls-meta.xml b/sfdx-source/apex-common-samplecode/main/classes/selectors/OpportunityInfo.cls-meta.xml deleted file mode 100644 index 5f399c3..0000000 --- a/sfdx-source/apex-common-samplecode/main/classes/selectors/OpportunityInfo.cls-meta.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - 63.0 - Active - diff --git a/sfdx-source/apex-common-samplecode/main/classes/selectors/OpportunityLineItemsSelector.cls b/sfdx-source/apex-common-samplecode/main/classes/selectors/OpportunityLineItemsSelector.cls index bd799a4..1eee6cf 100644 --- a/sfdx-source/apex-common-samplecode/main/classes/selectors/OpportunityLineItemsSelector.cls +++ b/sfdx-source/apex-common-samplecode/main/classes/selectors/OpportunityLineItemsSelector.cls @@ -24,54 +24,86 @@ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. **/ -public class OpportunityLineItemsSelector extends fflib_SObjectSelector - implements IOpportunityLineItemsSelector +/** + * Selector for OpportunityLineItem queries used by discount and wizard flows. + */ +public virtual inherited sharing class OpportunityLineItemsSelector extends fflib_SObjectSelector { - public static IOpportunityLineItemsSelector newInstance() + @TestVisible + private static OpportunityLineItemsSelector mock; + + /** + * Factory that returns this selector, or {@link #mock} when tests have set one. + * @return {@link OpportunityLineItemsSelector} instance. + */ + public static OpportunityLineItemsSelector newInstance() { - return (IOpportunityLineItemsSelector) Application.Selector.newInstance(OpportunityLineItem.SObjectType); + return mock!=null ? mock : new OpportunityLineItemsSelector(); } + /** + * Creates a selector that queries in user mode. + */ public OpportunityLineItemsSelector() { super(false, fflib_SObjectSelector.DataAccess.USER_MODE); } + /** + * Fields included in default queries from this selector. + * @return OpportunityLineItem Id, OpportunityId, Description, Quantity, UnitPrice, ListPrice, PricebookEntryId, and SortOrder. + */ public List getSObjectFieldList() { - return new List { - OpportunityLineItem.Description, + return new List{ OpportunityLineItem.Id, - OpportunityLineItem.ListPrice, OpportunityLineItem.OpportunityId, - OpportunityLineItem.PricebookEntryId, + OpportunityLineItem.Description, OpportunityLineItem.Quantity, - OpportunityLineItem.SortOrder, - OpportunityLineItem.TotalPrice, - OpportunityLineItem.UnitPrice + OpportunityLineItem.UnitPrice, + OpportunityLineItem.ListPrice, + OpportunityLineItem.PricebookEntryId, + OpportunityLineItem.SortOrder }; } + /** + * SObject type this selector queries. + * @return {@code OpportunityLineItem.SObjectType}. + */ public Schema.SObjectType getSObjectType() { return OpportunityLineItem.SObjectType; } - public override String getOrderBy() - { - return 'SortOrder,PricebookEntry.Name'; - } - + /** + * Selects OpportunityLineItems by Id with fields used by discounting, invoicing, and the wizard. + * @param idSet OpportunityLineItem Ids to query. Empty set returns an empty list. + * @return Matching line items; empty list when none match. + */ public List selectById(Set idSet) { return (List) selectSObjectsById(idSet); } + /** + * Selects recently modified line items for Opportunities belonging to the given Accounts. + * @param accountIds Account Ids whose related opportunity line items are included. Empty set returns an empty list. + * @param recordLimit Maximum number of rows to return. + * @return Matching line items ordered by SystemModstamp descending; empty list when none match. + */ public List selectRecentlyUsed(Set accountIds, Integer recordLimit) { - assertIsAccessible(); - String query = String.format('select {0} from {1} where Opportunity.Account.id in :accountIds order by SystemModstamp DESC LIMIT :recordLimit', - new List{ getFieldListString(), getSObjectName() }); - return (List) Database.query(query); + return (List) Database.query( + newQueryFactory() + .selectField('PricebookEntry.Name') + .selectField('PricebookEntry.Product2Id') + .selectField('PricebookEntry.Product2.DiscountingApproved__c') + .selectField('PricebookEntry.Pricebook2.Name') + .setCondition('Opportunity.AccountId IN :accountIds') + .setOrdering('SystemModstamp', fflib_QueryFactory.SortOrder.DESCENDING) + .setLimit(recordLimit) + .toSOQL() + ); } -} \ No newline at end of file +} diff --git a/sfdx-source/apex-common-samplecode/main/classes/selectors/OpportunityLineItemsSelector.cls-meta.xml b/sfdx-source/apex-common-samplecode/main/classes/selectors/OpportunityLineItemsSelector.cls-meta.xml index 5f399c3..b009296 100644 --- a/sfdx-source/apex-common-samplecode/main/classes/selectors/OpportunityLineItemsSelector.cls-meta.xml +++ b/sfdx-source/apex-common-samplecode/main/classes/selectors/OpportunityLineItemsSelector.cls-meta.xml @@ -1,5 +1,5 @@ - 63.0 + 67.0 Active diff --git a/sfdx-source/apex-common-samplecode/main/classes/domains/IChargeable.cls b/sfdx-source/apex-common-samplecode/main/classes/selectors/OpportunitySummary.cls similarity index 58% rename from sfdx-source/apex-common-samplecode/main/classes/domains/IChargeable.cls rename to sfdx-source/apex-common-samplecode/main/classes/selectors/OpportunitySummary.cls index 865578b..d156022 100644 --- a/sfdx-source/apex-common-samplecode/main/classes/domains/IChargeable.cls +++ b/sfdx-source/apex-common-samplecode/main/classes/selectors/OpportunitySummary.cls @@ -24,9 +24,47 @@ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. **/ -public interface IChargeable extends fflib_ISObjectDomain -{ - Schema.SObjectField getCostOfHoursWorkedField(); - List calculateHoursWorked(); - void updateCostOfHoursWorked(); -} \ No newline at end of file +/** + * Read-only DTO wrapping Opportunity fields and related Account summary values for UI or API views. + */ +public class OpportunitySummary { + private Opportunity opportunity; + + /** + * Creates a DTO from a queried Opportunity that includes related Account fields. + * @param opportunity Opportunity with Account.Name, Account.AccountNumber, and Account.Owner.Name populated as needed. + */ + public OpportunitySummary(Opportunity opportunity) { + this.opportunity = opportunity; + } + + /** + * Opportunity Id. + */ + public Id Id { get { return this.opportunity.Id; } } + + /** + * Opportunity Amount. + */ + public Decimal Amount { get { return this.opportunity.Amount; } } + + /** + * Opportunity StageName. + */ + public String Stage { get { return this.opportunity.StageName; } } + + /** + * Related Account Name. + */ + public String AccountName { get { return this.opportunity.Account.Name; } } + + /** + * Related Account Number. + */ + public String AccountNumber { get { return this.opportunity.Account.AccountNumber; } } + + /** + * Related Account owner Name. + */ + public String AccountOwner { get { return this.opportunity.Account.Owner.Name; } } +} diff --git a/sfdx-source/apex-common-samplecode/main/classes/selectors/OpportunitySummary.cls-meta.xml b/sfdx-source/apex-common-samplecode/main/classes/selectors/OpportunitySummary.cls-meta.xml new file mode 100644 index 0000000..b009296 --- /dev/null +++ b/sfdx-source/apex-common-samplecode/main/classes/selectors/OpportunitySummary.cls-meta.xml @@ -0,0 +1,5 @@ + + + 67.0 + Active + diff --git a/sfdx-source/apex-common-samplecode/main/classes/selectors/PricebookEntriesSelector.cls b/sfdx-source/apex-common-samplecode/main/classes/selectors/PricebookEntriesSelector.cls index c010aff..fe405c0 100644 --- a/sfdx-source/apex-common-samplecode/main/classes/selectors/PricebookEntriesSelector.cls +++ b/sfdx-source/apex-common-samplecode/main/classes/selectors/PricebookEntriesSelector.cls @@ -24,22 +24,38 @@ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. **/ -public class PricebookEntriesSelector extends fflib_SObjectSelector - implements IPricebookEntriesSelector +/** + * Selector for PricebookEntry queries. + */ +public virtual inherited sharing class PricebookEntriesSelector extends fflib_SObjectSelector { - public static IPricebookEntriesSelector newInstance() + @TestVisible + private static PricebookEntriesSelector mock; + + /** + * Factory that returns this selector, or {@link #mock} when tests have set one. + * @return {@link PricebookEntriesSelector} instance. + */ + public static PricebookEntriesSelector newInstance() { - return (IPricebookEntriesSelector) Application.Selector.newInstance(PricebookEntry.SObjectType); + return mock!=null ? mock : new PricebookEntriesSelector(); } + /** + * Creates a selector that queries in user mode. + */ public PricebookEntriesSelector() { super(false, fflib_SObjectSelector.DataAccess.USER_MODE); } + /** + * Fields included in default queries from this selector. + * @return PricebookEntry Id, IsActive, Name, Pricebook2Id, Product2Id, ProductCode, UnitPrice, and UseStandardPrice. + */ public List getSObjectFieldList() { - return new List { + return new List{ PricebookEntry.Id, PricebookEntry.IsActive, PricebookEntry.Name, @@ -51,13 +67,22 @@ public class PricebookEntriesSelector extends fflib_SObjectSelector }; } + /** + * SObject type this selector queries. + * @return {@code PricebookEntry.SObjectType}. + */ public Schema.SObjectType getSObjectType() { return PricebookEntry.SObjectType; } + /** + * Selects PricebookEntries by Id with standard pricing fields. + * @param idSet PricebookEntry Ids to query. Empty set returns an empty list. + * @return Matching PricebookEntry records; empty list when none match. + */ public List selectById(Set idSet) { return (List) selectSObjectsById(idSet); } -} \ No newline at end of file +} diff --git a/sfdx-source/apex-common-samplecode/main/classes/selectors/PricebookEntriesSelector.cls-meta.xml b/sfdx-source/apex-common-samplecode/main/classes/selectors/PricebookEntriesSelector.cls-meta.xml index 5f399c3..b009296 100644 --- a/sfdx-source/apex-common-samplecode/main/classes/selectors/PricebookEntriesSelector.cls-meta.xml +++ b/sfdx-source/apex-common-samplecode/main/classes/selectors/PricebookEntriesSelector.cls-meta.xml @@ -1,5 +1,5 @@ - 63.0 + 67.0 Active diff --git a/sfdx-source/apex-common-samplecode/main/classes/selectors/PricebooksSelector.cls b/sfdx-source/apex-common-samplecode/main/classes/selectors/PricebooksSelector.cls index 20594c2..d5de888 100644 --- a/sfdx-source/apex-common-samplecode/main/classes/selectors/PricebooksSelector.cls +++ b/sfdx-source/apex-common-samplecode/main/classes/selectors/PricebooksSelector.cls @@ -24,22 +24,38 @@ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. **/ -public class PricebooksSelector extends fflib_SObjectSelector - implements IPricebooksSelector +/** + * Selector for Pricebook2 queries. + */ +public virtual inherited sharing class PricebooksSelector extends fflib_SObjectSelector { - public static IPricebooksSelector newInstance() + @TestVisible + private static PricebooksSelector mock; + + /** + * Factory that returns this selector, or {@link #mock} when tests have set one. + * @return {@link PricebooksSelector} instance. + */ + public static PricebooksSelector newInstance() { - return (IPricebooksSelector) Application.Selector.newInstance(Pricebook2.SObjectType); + return mock!=null ? mock : new PricebooksSelector(); } + /** + * Creates a selector that queries in user mode. + */ public PricebooksSelector() { super(false, fflib_SObjectSelector.DataAccess.USER_MODE); } + /** + * Fields included in default queries from this selector. + * @return Pricebook2 Description, Id, IsActive, IsStandard, and Name. + */ public List getSObjectFieldList() { - return new List { + return new List{ Pricebook2.Description, Pricebook2.Id, Pricebook2.IsActive, @@ -48,13 +64,22 @@ public class PricebooksSelector extends fflib_SObjectSelector }; } + /** + * SObject type this selector queries. + * @return {@code Pricebook2.SObjectType}. + */ public Schema.SObjectType getSObjectType() { return Pricebook2.SObjectType; } + /** + * Selects Pricebooks by Id with Description, active, and standard flags. + * @param idSet Pricebook2 Ids to query. Empty set returns an empty list. + * @return Matching Pricebook2 records; empty list when none match. + */ public List selectById(Set idSet) { return (List) selectSObjectsById(idSet); - } -} \ No newline at end of file + } +} diff --git a/sfdx-source/apex-common-samplecode/main/classes/selectors/PricebooksSelector.cls-meta.xml b/sfdx-source/apex-common-samplecode/main/classes/selectors/PricebooksSelector.cls-meta.xml index 5f399c3..b009296 100644 --- a/sfdx-source/apex-common-samplecode/main/classes/selectors/PricebooksSelector.cls-meta.xml +++ b/sfdx-source/apex-common-samplecode/main/classes/selectors/PricebooksSelector.cls-meta.xml @@ -1,5 +1,5 @@ - 63.0 + 67.0 Active diff --git a/sfdx-source/apex-common-samplecode/main/classes/selectors/ProductsSelector.cls b/sfdx-source/apex-common-samplecode/main/classes/selectors/ProductsSelector.cls index 8d8b60a..7b16c28 100644 --- a/sfdx-source/apex-common-samplecode/main/classes/selectors/ProductsSelector.cls +++ b/sfdx-source/apex-common-samplecode/main/classes/selectors/ProductsSelector.cls @@ -24,27 +24,57 @@ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. **/ -public class ProductsSelector extends fflib_SObjectSelector - implements IProductsSelector +/** + * Selector for Product2 queries, optionally including OpportunityDiscount field set members. + */ +public virtual inherited sharing class ProductsSelector extends fflib_SObjectSelector { - public static IProductsSelector newInstance() + @TestVisible + private static ProductsSelector mock; + + /** + * Factory that returns a selector without field set fields, or {@link #mock} when tests have set one. + * @return {@link ProductsSelector} instance. + */ + public static ProductsSelector newInstance() { - return (IProductsSelector) Application.Selector.newInstance(Product2.SObjectType); + return newInstance(false); } - + + /** + * Factory that returns a selector with optional field set fields, or {@link #mock} when tests have set one. + * @param includeFieldSetFields When true, OpportunityDiscount field set fields are included. + * @return {@link ProductsSelector} instance. + */ + public static ProductsSelector newInstance(Boolean includeFieldSetFields) + { + return mock!=null ? mock : new ProductsSelector(includeFieldSetFields); + } + + /** + * Creates a selector that queries only the default product field list. + */ public ProductsSelector() { - super(false, fflib_SObjectSelector.DataAccess.USER_MODE); + this(false); } + /** + * Creates a selector that optionally includes OpportunityDiscount field set fields. + * @param includeFieldSetFields When true, fields from Product2.FieldSets.OpportunityDiscount are added to the SELECT list. + */ public ProductsSelector(Boolean includeFieldSetFields) { - super(includeFieldSetFields, fflib_SObjectSelector.DataAccess.USER_MODE); + super(includeFieldSetFields==true, fflib_SObjectSelector.DataAccess.USER_MODE); } - + + /** + * Fields included in default queries from this selector. + * @return Product2 Description, Id, IsActive, Name, ProductCode, and DiscountingApproved__c. + */ public List getSObjectFieldList() { - return new List { + return new List{ Product2.Description, Product2.Id, Product2.IsActive, @@ -54,18 +84,41 @@ public class ProductsSelector extends fflib_SObjectSelector }; } + /** + * SObject type this selector queries. + * @return {@code Product2.SObjectType}. + */ public Schema.SObjectType getSObjectType() { return Product2.SObjectType; } + /** + * Field sets optionally added when the selector is constructed with includeFieldSetFields. + * @return {@code Product2.FieldSets.OpportunityDiscount}. + */ + public override List getSObjectFieldSetList() + { + return new List{ SObjectType.Product2.FieldSets.OpportunityDiscount }; + } + + /** + * Selects Products by Id using the configured field list. + * @param idSet Product2 Ids to query. Empty set returns an empty list. + * @return Matching Product2 records; empty list when none match. + */ public List selectById(Set idSet) { return (List) selectSObjectsById(idSet); } - - public override List getSObjectFieldSetList() + + /** + * Returns a QueryLocator for Products by Id using the configured field list. + * @param idSet Product2 Ids to include in the locator. + * @return QueryLocator suitable for Batch Apex or large result sets. + */ + public override Database.QueryLocator queryLocatorById(Set idSet) { - return new List { SObjectType.Product2.FieldSets.OpportunityDiscount }; + return Database.getQueryLocator(newQueryFactory().setCondition('Id IN :idSet').toSOQL()); } -} \ No newline at end of file +} diff --git a/sfdx-source/apex-common-samplecode/main/classes/selectors/ProductsSelector.cls-meta.xml b/sfdx-source/apex-common-samplecode/main/classes/selectors/ProductsSelector.cls-meta.xml index 5f399c3..b009296 100644 --- a/sfdx-source/apex-common-samplecode/main/classes/selectors/ProductsSelector.cls-meta.xml +++ b/sfdx-source/apex-common-samplecode/main/classes/selectors/ProductsSelector.cls-meta.xml @@ -1,5 +1,5 @@ - 63.0 + 67.0 Active diff --git a/sfdx-source/apex-common-samplecode/main/classes/selectors/TrainingWorkItemsSelector.cls b/sfdx-source/apex-common-samplecode/main/classes/selectors/TrainingWorkItemsSelector.cls new file mode 100644 index 0000000..8aad0aa --- /dev/null +++ b/sfdx-source/apex-common-samplecode/main/classes/selectors/TrainingWorkItemsSelector.cls @@ -0,0 +1,91 @@ +/** + * Copyright (c) 2012-2014, FinancialForce.com, inc + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * - Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * - Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * - Neither the name of the FinancialForce.com, inc nor the names of its contributors + * may be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL + * THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY + * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +**/ + +/** + * Selector for TrainingWorkItem__c including related Work Order account for invoicing. + */ +public virtual inherited sharing class TrainingWorkItemsSelector extends fflib_SObjectSelector { + @TestVisible + private static TrainingWorkItemsSelector mock; + + /** + * Factory that returns this selector, or {@link #mock} when tests have set one. + * @return {@link TrainingWorkItemsSelector} instance. + */ + public static TrainingWorkItemsSelector newInstance() { + return mock != null ? mock : new TrainingWorkItemsSelector(); + } + + /** + * Creates a selector that queries in user mode. + */ + public TrainingWorkItemsSelector() { + super(false, fflib_SObjectSelector.DataAccess.USER_MODE); + } + + /** + * Fields included in default queries from this selector. + * @return TrainingWorkItem__c Id, Name, WorkOrder__c, TrainingCost__c, DeliveryHours__c, and PreparationHours__c. + */ + public List getSObjectFieldList() { + return new List{ + TrainingWorkItem__c.Id, + TrainingWorkItem__c.Name, + TrainingWorkItem__c.WorkOrder__c, + TrainingWorkItem__c.TrainingCost__c, + TrainingWorkItem__c.DeliveryHours__c, + TrainingWorkItem__c.PreparationHours__c + }; + } + + /** + * SObject type this selector queries. + * @return {@code TrainingWorkItem__c.SObjectType}. + */ + public Schema.SObjectType getSObjectType() { + return TrainingWorkItem__c.SObjectType; + } + + /** + * Selects training work items by Id with fields needed for cost and invoicing. + * @param idSet TrainingWorkItem__c Ids to query. Empty set returns an empty list. + * @return Matching TrainingWorkItem__c records; empty list when none match. + */ + public List selectById(Set idSet) { + return (List) selectSObjectsById(idSet); + } + + /** + * Factory {@code selectById} uses this method; invoicing needs Work Order account. + * @param idSet TrainingWorkItem__c Ids to query. Empty set returns an empty list. + * @return Matching work items with WorkOrder__r.Account__c; empty list when none match. + */ + public override List selectSObjectsById(Set idSet) { + return Database.query( + newQueryFactory().selectField('WorkOrder__r.Account__c').setCondition('Id IN :idSet').toSOQL() + ); + } +} diff --git a/sfdx-source/apex-common-samplecode/main/classes/selectors/TrainingWorkItemsSelector.cls-meta.xml b/sfdx-source/apex-common-samplecode/main/classes/selectors/TrainingWorkItemsSelector.cls-meta.xml new file mode 100644 index 0000000..b009296 --- /dev/null +++ b/sfdx-source/apex-common-samplecode/main/classes/selectors/TrainingWorkItemsSelector.cls-meta.xml @@ -0,0 +1,5 @@ + + + 67.0 + Active + diff --git a/sfdx-source/apex-common-samplecode/main/classes/selectors/UsersSelector.cls b/sfdx-source/apex-common-samplecode/main/classes/selectors/UsersSelector.cls index 36dd2c3..938268e 100644 --- a/sfdx-source/apex-common-samplecode/main/classes/selectors/UsersSelector.cls +++ b/sfdx-source/apex-common-samplecode/main/classes/selectors/UsersSelector.cls @@ -25,43 +25,60 @@ **/ /** - * Selector encapsulates queries around the User object, currently limited to needs around email, hence reduced scope - **/ -public class UsersSelector extends fflib_SObjectSelector - implements IUsersSelector + * Selector for User email addresses used by job notification flows. + */ +public virtual inherited sharing class UsersSelector extends fflib_SObjectSelector { - public static IUsersSelector newInstance() + @TestVisible + private static UsersSelector mock; + + /** + * Factory that returns this selector, or {@link #mock} when tests have set one. + * @return {@link UsersSelector} instance. + */ + public static UsersSelector newInstance() { - return (IUsersSelector) Application.Selector.newInstance(User.SObjectType); + return mock!=null ? mock : new UsersSelector(); } + /** + * Creates a selector that queries in user mode. + */ public UsersSelector() { super(false, fflib_SObjectSelector.DataAccess.USER_MODE); } + /** + * Fields included in default queries from this selector. + * @return User Id and Email. + */ public List getSObjectFieldList() { - // Current app requirements driven solely by getUsersEmail at this stage - return new List { User.Email }; + return new List{ User.Id, User.Email }; } + /** + * SObject type this selector queries. + * @return {@code User.SObjectType}. + */ public Schema.SObjectType getSObjectType() { return User.SObjectType; } - + /** - * Queries given user records and maps the user Ids to assigned email addresses - **/ + * Maps User Ids to their Email addresses. + * @param userIds User Ids to query. Empty set returns an empty map. + * @return Map of User Id to Email; users without a matching row are omitted. + */ public Map getUsersEmail(Set userIds) { - List users = selectSObjectsById(userIds); Map emailsById = new Map(); - for (User user : users) + for(User u : (List) selectSObjectsById(userIds)) { - emailsById.put(user.Id, user.Email); + emailsById.put(u.Id, u.Email); } - return emailsById; + return emailsById; } -} \ No newline at end of file +} diff --git a/sfdx-source/apex-common-samplecode/main/classes/selectors/UsersSelector.cls-meta.xml b/sfdx-source/apex-common-samplecode/main/classes/selectors/UsersSelector.cls-meta.xml index 5f399c3..b009296 100644 --- a/sfdx-source/apex-common-samplecode/main/classes/selectors/UsersSelector.cls-meta.xml +++ b/sfdx-source/apex-common-samplecode/main/classes/selectors/UsersSelector.cls-meta.xml @@ -1,5 +1,5 @@ - 63.0 + 67.0 Active diff --git a/sfdx-source/apex-common-samplecode/main/classes/service/AccountsService.cls b/sfdx-source/apex-common-samplecode/main/classes/service/AccountsService.cls index 0826c23..8ec3658 100644 --- a/sfdx-source/apex-common-samplecode/main/classes/service/AccountsService.cls +++ b/sfdx-source/apex-common-samplecode/main/classes/service/AccountsService.cls @@ -2,37 +2,61 @@ * Copyright (c) 2012-2014, FinancialForce.com, inc * All rights reserved. * - * Redistribution and use in source and binary forms, with or without modification, + * Redistribution and use in source and binary forms, with or without modification, * are permitted provided that the following conditions are met: * - * - Redistributions of source code must retain the above copyright notice, + * - Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. - * - Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation + * - Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation * and/or other materials provided with the distribution. - * - Neither the name of the FinancialForce.com, inc nor the names of its contributors - * may be used to endorse or promote products derived from this software without + * - Neither the name of the FinancialForce.com, inc nor the names of its contributors + * may be used to endorse or promote products derived from this software without * specific prior written permission. * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND - * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES - * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL - * THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL + * THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. **/ -public with sharing class AccountsService +/** + * Service for Account-related operations invoked from the service layer. + */ +public inherited sharing class AccountsService { - public static void updateOpportunityActivity(Set accountIds) + @TestVisible + private static AccountsService mock; + + /** + * Factory that returns this service, or {@link #mock} when tests have set one. + * @return {@link AccountsService} instance. + */ + public static AccountsService newInstance() { - service().updateOpportunityActivity(accountIds); + return mock!=null ? mock : new AccountsService(); } - private static IAccountsService service() + /** + * Updates last Opportunity activity on the related Accounts. + * @param accountIds Account Ids to update. Empty set is a no-op. + */ + public void updateOpportunityActivity(Set accountIds) { - return (IAccountsService) Application.Service.newInstance(IAccountsService.class); + if(accountIds.isEmpty()) + { + return; + } + + fflib_ISObjectUnitOfWork uow = UnitOfWork.newInstance(); + List accountRecords = AccountsSelector.newInstance().selectById(accountIds); + Accounts accountsDomain = Accounts.newInstance(accountRecords); + accountsDomain.updateOpportunityActivity(); + uow.registerDirty(accountsDomain.getRecords()); + uow.commitWork(); } -} \ No newline at end of file +} diff --git a/sfdx-source/apex-common-samplecode/main/classes/service/AccountsService.cls-meta.xml b/sfdx-source/apex-common-samplecode/main/classes/service/AccountsService.cls-meta.xml index 5f399c3..b009296 100644 --- a/sfdx-source/apex-common-samplecode/main/classes/service/AccountsService.cls-meta.xml +++ b/sfdx-source/apex-common-samplecode/main/classes/service/AccountsService.cls-meta.xml @@ -1,5 +1,5 @@ - 63.0 + 67.0 Active diff --git a/sfdx-source/apex-common-samplecode/main/classes/service/AccountsServiceImpl.cls b/sfdx-source/apex-common-samplecode/main/classes/service/AccountsServiceImpl.cls deleted file mode 100644 index c018fb8..0000000 --- a/sfdx-source/apex-common-samplecode/main/classes/service/AccountsServiceImpl.cls +++ /dev/null @@ -1,44 +0,0 @@ -/** - * Copyright (c) 2012-2014, FinancialForce.com, inc - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, - * are permitted provided that the following conditions are met: - * - * - Redistributions of source code must retain the above copyright notice, - * this list of conditions and the following disclaimer. - * - Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation - * and/or other materials provided with the distribution. - * - Neither the name of the FinancialForce.com, inc nor the names of its contributors - * may be used to endorse or promote products derived from this software without - * specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND - * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES - * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL - * THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, - * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS - * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY - * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) - * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -**/ -public with sharing class AccountsServiceImpl implements IAccountsService -{ - /** - * Update last Opportunity activity on the related Accounts - * - * @param accountIds The account Ids of the records to update - */ - public void updateOpportunityActivity(Set accountIds) - { - fflib_ISObjectUnitOfWork uow = Application.UnitOfWork.newInstance(); - - IAccounts accounts = (IAccounts) Application.Domain.newInstance( accountIds ); - - accounts.updateOpportunityActivity(); - - uow.registerDirty(accounts.getRecords()); - uow.commitWork(); - } -} \ No newline at end of file diff --git a/sfdx-source/apex-common-samplecode/main/classes/service/AccountsServiceImpl.cls-meta.xml b/sfdx-source/apex-common-samplecode/main/classes/service/AccountsServiceImpl.cls-meta.xml deleted file mode 100644 index 5f399c3..0000000 --- a/sfdx-source/apex-common-samplecode/main/classes/service/AccountsServiceImpl.cls-meta.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - 63.0 - Active - diff --git a/sfdx-source/apex-common-samplecode/main/classes/service/IAccountsService.cls b/sfdx-source/apex-common-samplecode/main/classes/service/IAccountsService.cls deleted file mode 100644 index b201ead..0000000 --- a/sfdx-source/apex-common-samplecode/main/classes/service/IAccountsService.cls +++ /dev/null @@ -1,29 +0,0 @@ -/** - * Copyright (c) 2012-2014, FinancialForce.com, inc - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, - * are permitted provided that the following conditions are met: - * - * - Redistributions of source code must retain the above copyright notice, - * this list of conditions and the following disclaimer. - * - Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation - * and/or other materials provided with the distribution. - * - Neither the name of the FinancialForce.com, inc nor the names of its contributors - * may be used to endorse or promote products derived from this software without - * specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND - * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES - * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL - * THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, - * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS - * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY - * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) - * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -**/ -public interface IAccountsService -{ - void updateOpportunityActivity(Set accountIds); -} \ No newline at end of file diff --git a/sfdx-source/apex-common-samplecode/main/classes/service/IAccountsService.cls-meta.xml b/sfdx-source/apex-common-samplecode/main/classes/service/IAccountsService.cls-meta.xml deleted file mode 100644 index 5f399c3..0000000 --- a/sfdx-source/apex-common-samplecode/main/classes/service/IAccountsService.cls-meta.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - 63.0 - Active - diff --git a/sfdx-source/apex-common-samplecode/main/classes/service/IInvoicingService.cls b/sfdx-source/apex-common-samplecode/main/classes/service/IInvoicingService.cls deleted file mode 100644 index c17b3ce..0000000 --- a/sfdx-source/apex-common-samplecode/main/classes/service/IInvoicingService.cls +++ /dev/null @@ -1,30 +0,0 @@ -/** - * Copyright (c) 2012-2014, FinancialForce.com, inc - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, - * are permitted provided that the following conditions are met: - * - * - Redistributions of source code must retain the above copyright notice, - * this list of conditions and the following disclaimer. - * - Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation - * and/or other materials provided with the distribution. - * - Neither the name of the FinancialForce.com, inc nor the names of its contributors - * may be used to endorse or promote products derived from this software without - * specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND - * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES - * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL - * THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, - * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS - * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY - * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) - * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -**/ - -public interface IInvoicingService -{ - List generate(List sourceRecordIds); -} \ No newline at end of file diff --git a/sfdx-source/apex-common-samplecode/main/classes/service/IInvoicingService.cls-meta.xml b/sfdx-source/apex-common-samplecode/main/classes/service/IInvoicingService.cls-meta.xml deleted file mode 100644 index 5f399c3..0000000 --- a/sfdx-source/apex-common-samplecode/main/classes/service/IInvoicingService.cls-meta.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - 63.0 - Active - diff --git a/sfdx-source/apex-common-samplecode/main/classes/service/IOpportunitiesService.cls-meta.xml b/sfdx-source/apex-common-samplecode/main/classes/service/IOpportunitiesService.cls-meta.xml deleted file mode 100644 index 5f399c3..0000000 --- a/sfdx-source/apex-common-samplecode/main/classes/service/IOpportunitiesService.cls-meta.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - 63.0 - Active - diff --git a/sfdx-source/apex-common-samplecode/main/classes/service/InvoicingService.cls b/sfdx-source/apex-common-samplecode/main/classes/service/InvoicingService.cls index f9304a4..32d7311 100644 --- a/sfdx-source/apex-common-samplecode/main/classes/service/InvoicingService.cls +++ b/sfdx-source/apex-common-samplecode/main/classes/service/InvoicingService.cls @@ -24,44 +24,61 @@ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. **/ -public with sharing class InvoicingService +/** + * Polymorphic invoicing service that generates invoices from registered source SObject types + * via {@link InvoicingTargetsRegistry} and domains implementing {@link ISupportInvoicing}. + */ +public inherited sharing class InvoicingService { - public static List generate(List sourceRecordIds) + @TestVisible + private static InvoicingService mock; + + /** + * Factory that returns this service, or {@link #mock} when tests have set one. + * @return {@link InvoicingService} instance. + */ + public static InvoicingService newInstance() { - return service().generate(sourceRecordIds); + return mock!=null ? mock : new InvoicingService(); } - private static IInvoicingService service() - { - return (IInvoicingService) Application.Service.newInstance(IInvoicingService.class); - } - /** - * Interface implemented by domain classes that support the ability to produce invoices - **/ - public interface ISupportInvoicing + * Contract for domains that can contribute invoices to an {@link InvoiceFactory}. + */ + public interface ISupportInvoicing { + /** + * Adds invoice DTOs for the domain's records to the factory. + * @param invoiceFactory Factory that collects invoice headers and lines. Cannot be null. + */ void generate(InvoiceFactory invoiceFactory); } - + /** - * Methods on this factory will help create and register with the UOW invoices + * Registers invoice headers and lines with a Unit of Work. */ public class InvoiceFactory - { +{ private fflib_ISObjectUnitOfWork unitOfWork; + /** + * Invoice__c headers accumulated so far; Ids are set after commit. + */ + public List Invoices { get; private set; } /** - * List of Invoices generated by the factory - **/ - public List Invoices {get; private set;} - + * Creates a factory that registers invoices on the given Unit of Work. + * @param unitOfWork Unit of Work that will insert headers and lines. Cannot be null. + */ public InvoiceFactory(fflib_ISObjectUnitOfWork unitOfWork) { this.unitOfWork = unitOfWork; this.Invoices = new List(); } - + + /** + * Converts an {@link Invoice} DTO into Invoice__c / InvoiceLine__c rows registered on the UoW. + * @param invoice Invoice header and optional lines to add. Cannot be null. + */ public void add(Invoice invoice) { Invoice__c invoiceRecord = new Invoice__c(); @@ -69,41 +86,94 @@ public with sharing class InvoicingService invoiceRecord.Description__c = invoice.Description; invoiceRecord.InvoiceDate__c = invoice.InvoiceDate; unitOfWork.registerNew(invoiceRecord); - if(invoice.Lines!=null) + Invoices.add(invoiceRecord); + + if(invoice.Lines==null) { - for(InvoiceLine line : invoice.Lines) - { - InvoiceLine__c invoiceLineItem = new InvoiceLine__c(); - invoiceLineItem.Description__c = line.Description; - invoiceLineItem.Product__c = line.Product; - invoiceLineItem.UnitPrice__c = line.UnitPrice; - invoiceLineItem.Quantity__c = line.Quantity; - unitOfWork.registerNew(invoiceLineItem, InvoiceLine__c.Invoice__c, invoiceRecord); - } + return; + } + for(InvoiceLine line : invoice.Lines) + { + InvoiceLine__c invoiceLineItem = new InvoiceLine__c(); + invoiceLineItem.Description__c = line.Description; + invoiceLineItem.Product__c = line.Product; + invoiceLineItem.UnitPrice__c = line.UnitPrice; + invoiceLineItem.Quantity__c = line.Quantity; + unitOfWork.registerNew(invoiceLineItem, InvoiceLine__c.Invoice__c, invoiceRecord); } - Invoices.add(invoiceRecord); } - } + } /** - * Invoice header - **/ + * In-memory invoice header used by domains before persistence. + */ public class Invoice - { - public List Lines {get;set;} - public Id Account {get;set;} - public String Description {get;set;} - public Date InvoiceDate {get;set;} +{ + public List Lines { get; set; } + public Id Account { get; set; } + public String Description { get; set; } + public Date InvoiceDate { get; set; } } /** - * Invoice line - **/ + * In-memory invoice line used by domains before persistence. + */ public class InvoiceLine +{ + public String Description { get; set; } + public Id Product { get; set; } + public Decimal UnitPrice { get; set; } + public Decimal Quantity { get; set; } + } + + /** + * Loads source records via {@link InvoicingTargetsRegistry}, asks the domain to generate invoices, + * and commits Invoice__c / InvoiceLine__c. + * @param sourceRecordIds Homogeneous Ids of registered invoice source records. Cannot be null. + * @return Ids of inserted Invoice__c records; empty list when none were created. + * @throws InvoicingException When the source type is invalid, commit fails, or another unexpected error occurs. + */ + public List generate(List sourceRecordIds) + { + try + { + fflib_ISObjectUnitOfWork uow = UnitOfWork.newInstance(); + InvoiceFactory invoiceFactory = new InvoiceFactory(uow); + List records = InvoicingTargetsRegistry.selectById(new Set(sourceRecordIds)); + Object domain = InvoicingTargetsRegistry.newDomain(records); + if(!(domain instanceof ISupportInvoicing)) + { + throw new InvoicingException('Invalid source object for generating invoices.'); + } + + ((ISupportInvoicing) domain).generate(invoiceFactory); + uow.commitWork(); + + List invoiceIds = new List(); + for(Invoice__c invoice : invoiceFactory.Invoices) + { + invoiceIds.add(invoice.Id); + } + return invoiceIds; + } + catch (InvoicingException e) + { + throw e; + } + catch (DmlException e) + { + throw new InvoicingException('Error generating invoices: ' + e.getDmlMessage(0), e); + } + catch (Exception e) + { + throw new InvoicingException(e.getMessage(), e); + } + } + + /** + * Represents an exception thrown by {@link InvoicingService} operations. + */ + public class InvoicingException extends Exception { - public String Description {get;set;} - public Id Product {get;set;} - public Decimal UnitPrice {get;set;} - public Decimal Quantity {get;set;} } -} \ No newline at end of file +} diff --git a/sfdx-source/apex-common-samplecode/main/classes/service/InvoicingService.cls-meta.xml b/sfdx-source/apex-common-samplecode/main/classes/service/InvoicingService.cls-meta.xml index 5f399c3..b009296 100644 --- a/sfdx-source/apex-common-samplecode/main/classes/service/InvoicingService.cls-meta.xml +++ b/sfdx-source/apex-common-samplecode/main/classes/service/InvoicingService.cls-meta.xml @@ -1,5 +1,5 @@ - 63.0 + 67.0 Active diff --git a/sfdx-source/apex-common-samplecode/main/classes/service/InvoicingServiceImpl.cls-meta.xml b/sfdx-source/apex-common-samplecode/main/classes/service/InvoicingServiceImpl.cls-meta.xml deleted file mode 100644 index 5f399c3..0000000 --- a/sfdx-source/apex-common-samplecode/main/classes/service/InvoicingServiceImpl.cls-meta.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - 63.0 - Active - diff --git a/sfdx-source/apex-common-samplecode/main/classes/service/OpportunitiesService.cls b/sfdx-source/apex-common-samplecode/main/classes/service/OpportunitiesService.cls index 8b706d3..0c0d988 100644 --- a/sfdx-source/apex-common-samplecode/main/classes/service/OpportunitiesService.cls +++ b/sfdx-source/apex-common-samplecode/main/classes/service/OpportunitiesService.cls @@ -24,25 +24,148 @@ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. **/ -public with sharing class OpportunitiesService -{ - public static void applyDiscounts(Set opportunityIds, Decimal discountPercentage) +/** + * Service for Opportunity discounting, invoice creation, and invoicing job submission. + */ +public inherited sharing class OpportunitiesService +{ + @TestVisible + private static OpportunitiesService mock; + + /** + * Factory that returns this service, or {@link #mock} when tests have set one. + * @return {@link OpportunitiesService} instance. + */ + public static OpportunitiesService newInstance() + { + return mock!=null ? mock : new OpportunitiesService(); + } + + private OpportunitiesSelector selector; + + /** + * Creates a service using {@link OpportunitiesSelector#newInstance}. + */ + public OpportunitiesService() + { + this.selector = OpportunitiesSelector.newInstance(); + } + + /** + * Loads Opportunities with products and applies a percentage discount and commits the updates. + * @param opportunityIds Opportunity Ids to discount. Empty set performs no DML. + * @param discountPercentage Discount percent to apply (for example 10 for 10%). + * @throws OpportunitiesServiceException When commit fails or another unexpected error occurs. + */ + public void applyDiscounts(Set opportunityIds, Decimal discountPercentage) + { + applyDiscounts(selector.selectByIdWithProducts(opportunityIds), discountPercentage); + } + + /** + * Applies a percentage discount to the given Opportunities (with products) and commits the updates. + * @param opportunitiesWithProducts Opportunities including line items and product discount flags as needed. + * @param discountPercentage Discount percent to apply (for example 10 for 10%). + * @throws OpportunitiesServiceException When commit fails or another unexpected error occurs. + */ + public void applyDiscounts(List opportunitiesWithProducts, Decimal discountPercentage) { - service().applyDiscounts(opportunityIds, discountPercentage); + try + { + fflib_ISObjectUnitOfWork uow = UnitOfWork.newInstance(); + Opportunities.newInstance(opportunitiesWithProducts).applyDiscount(discountPercentage, uow); + uow.commitWork(); + } + catch (Exception e) + { + throw wrap(e); + } } - - public static Set createInvoices(Set opportunityIds, Decimal discountPercentage) - { - return service().createInvoices(opportunityIds, discountPercentage); + + /** + * Optionally discounts Opportunities, then creates Invoice__c and InvoiceLine__c records. + * Invoice date is CloseDate (the simple Opportunity path; {@link InvoicingService} uses CloseDate plus 14 days). + * @param opportunityIds Opportunity Ids to invoice. Empty set returns an empty set. + * @param discountPercentage Optional discount applied before invoicing when greater than zero; null or zero skips discounting. + * @return Ids of inserted Invoice__c records; empty set when no invoices were created. + * @throws OpportunitiesServiceException When commit fails or another unexpected error occurs. + */ + public Set createInvoices(Set opportunityIds, Decimal discountPercentage) + { + try + { + fflib_ISObjectUnitOfWork uow = UnitOfWork.newInstance(); + Opportunities opps = Opportunities.newInstance(selector.selectByIdWithProducts(opportunityIds)); + if(discountPercentage!=null && discountPercentage > 0) + { + opps.applyDiscount(discountPercentage, uow); + } + + List invoices = new List(); + for(Opportunity opportunityRecord : (List) opps.getRecords()) + { + Invoice__c invoice = new Invoice__c(); + invoice.Account__c = opportunityRecord.AccountId; + invoice.Description__c = opportunityRecord.Description; + invoice.InvoiceDate__c = opportunityRecord.CloseDate; + invoice.Opportunity__c = opportunityRecord.Id; + uow.registerNew(invoice); + invoices.add(invoice); + if(opportunityRecord.OpportunityLineItems==null) + { + continue; + } + for(OpportunityLineItem lineItem : opportunityRecord.OpportunityLineItems) + { + InvoiceLine__c invoiceLineItem = new InvoiceLine__c(); + invoiceLineItem.Description__c = lineItem.Description; + invoiceLineItem.Product__c = lineItem.PricebookEntry.Product2Id; + invoiceLineItem.UnitPrice__c = lineItem.UnitPrice; + invoiceLineItem.Quantity__c = lineItem.Quantity; + uow.registerNew(invoiceLineItem, InvoiceLine__c.Invoice__c, invoice); + } + } + uow.commitWork(); + + Set invoiceIds = new Set(); + for(Invoice__c invoice : invoices) + { + invoiceIds.add(invoice.Id); + } + return invoiceIds; + } + catch (Exception e) + { + throw wrap(e); + } } - - public static Id submitInvoicingJob() + + /** + * Submits {@link CreateInvoicesJob} as a Batch Apex job. + * @return Async Apex job Id returned by {@code Database.executeBatch}. + */ + public Id submitInvoicingJob() { - return service().submitInvoicingJob(); - } + return Database.executeBatch(new CreateInvoicesJob()); + } - private static IOpportunitiesService service() + private static OpportunitiesServiceException wrap(Exception e) { - return (IOpportunitiesService) Application.Service.newInstance(IOpportunitiesService.class); - } -} \ No newline at end of file + if(e instanceof OpportunitiesServiceException) + { + return (OpportunitiesServiceException) e; + } + if(e instanceof DmlException) + { + return new OpportunitiesServiceException('Error applying discounts or creating invoices: ' + ((DmlException) e).getDmlMessage(0), e); + } + return new OpportunitiesServiceException(e.getMessage(), e); + } + + /** + * Represents an exception thrown by {@link OpportunitiesService} operations. + */ + public class OpportunitiesServiceException extends Exception + { + } +} diff --git a/sfdx-source/apex-common-samplecode/main/classes/service/OpportunitiesService.cls-meta.xml b/sfdx-source/apex-common-samplecode/main/classes/service/OpportunitiesService.cls-meta.xml index 5f399c3..b009296 100644 --- a/sfdx-source/apex-common-samplecode/main/classes/service/OpportunitiesService.cls-meta.xml +++ b/sfdx-source/apex-common-samplecode/main/classes/service/OpportunitiesService.cls-meta.xml @@ -1,5 +1,5 @@ - 63.0 + 67.0 Active diff --git a/sfdx-source/apex-common-samplecode/main/classes/service/OpportunitiesServiceImpl.cls b/sfdx-source/apex-common-samplecode/main/classes/service/OpportunitiesServiceImpl.cls deleted file mode 100644 index 3e3f88d..0000000 --- a/sfdx-source/apex-common-samplecode/main/classes/service/OpportunitiesServiceImpl.cls +++ /dev/null @@ -1,114 +0,0 @@ -/** - * Copyright (c) 2012-2014, FinancialForce.com, inc - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, - * are permitted provided that the following conditions are met: - * - * - Redistributions of source code must retain the above copyright notice, - * this list of conditions and the following disclaimer. - * - Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation - * and/or other materials provided with the distribution. - * - Neither the name of the FinancialForce.com, inc nor the names of its contributors - * may be used to endorse or promote products derived from this software without - * specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND - * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES - * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL - * THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, - * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS - * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY - * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) - * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -**/ - -public class OpportunitiesServiceImpl - implements IOpportunitiesService -{ - public void applyDiscounts(Set opportunityIds, Decimal discountPercentage) - { - applyDiscounts( - OpportunitiesSelector.newInstance().selectByIdWithProducts(opportunityIds), - discountPercentage); - } - - public void applyDiscounts(List opportunitiesWithProducts, Decimal discountPercentage) - { - applyDiscounts( - Opportunities.newInstance(opportunitiesWithProducts), - discountPercentage - ); - } - - private void applyDiscounts(IOpportunities opportunitiesWithProducts, Decimal discountPercentage) - { - // Create unit of work to capture work and commit it under one transaction - fflib_ISObjectUnitOfWork uow = Application.UnitOfWork.newInstance(); - - // applyDiscounts(uow, opportunitiesWithProducts, discountPercentage); - opportunitiesWithProducts.applyDiscount(discountPercentage, uow); - - // Commit updates to opportunities - uow.commitWork(); - } - - public Set createInvoices(Set opportunityIds, Decimal discountPercentage) - { - // Create unit of work to capture work and commit it under one transaction - fflib_ISObjectUnitOfWork uow = Application.UnitOfWork.newInstance(); - - // Query Opportunities - IOpportunities opps = - Opportunities.newInstance( - OpportunitiesSelector.newInstance().selectByIdWithProducts(opportunityIds)); - - // Optionally apply discounts as part of invoice creation - if (discountPercentage != null && discountPercentage > 0) - { - opps.applyDiscount(discountPercentage, uow); - } - - // Create Invoices from the given opportunities - List invoices = new List(); - for (Opportunity opportunityRecord : (List) opps.getRecords()) - { - Invoice__c invoice = new Invoice__c(); - invoice.Account__c = opportunityRecord.AccountId; - invoice.Description__c = opportunityRecord.Description; - invoice.InvoiceDate__c = opportunityRecord.CloseDate; - invoice.Opportunity__c = opportunityRecord.Id; - uow.registerNew(invoice); - for (OpportunityLineItem lineItem : opportunityRecord.OpportunityLineItems) - { - InvoiceLine__c invoiceLineItem = new InvoiceLine__c(); - invoiceLineItem.Description__c = lineItem.Description; - invoiceLineItem.Product__c = lineItem.PricebookEntry.Product2Id; - invoiceLineItem.UnitPrice__c = lineItem.UnitPrice; - invoiceLineItem.Quantity__c = lineItem.Quantity; - uow.registerNew(invoiceLineItem, InvoiceLine__c.Invoice__c, invoice); - } - invoices.add(invoice); - } - - // Commit any Opportunity updates and new invoices - uow.commitWork(); - - // Invoice Ids - Set invoiceIds = new Set(); - for (SObject invoice : invoices) - { - invoiceIds.add((Id) invoice.get('Id')); - } - return invoiceIds; - } - - public Id submitInvoicingJob() - { - // Submit the job via Batch Apex, - // service layer can perform validations and updates such as logging etc on behalf of client - // also configure scope size, knowledge of the class to use etc is encapsulated in the service - return Database.executeBatch(new CreateInvoicesJob()); - } -} \ No newline at end of file diff --git a/sfdx-source/apex-common-samplecode/main/classes/service/OpportunitiesServiceImpl.cls-meta.xml b/sfdx-source/apex-common-samplecode/main/classes/service/OpportunitiesServiceImpl.cls-meta.xml deleted file mode 100644 index 5f399c3..0000000 --- a/sfdx-source/apex-common-samplecode/main/classes/service/OpportunitiesServiceImpl.cls-meta.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - 63.0 - Active - diff --git a/sfdx-source/apex-common-samplecode/main/classes/triggerHandlers/OpportunitiesTriggerHandler.cls b/sfdx-source/apex-common-samplecode/main/classes/triggerHandlers/OpportunitiesTriggerHandler.cls index 36fbbf7..4310946 100644 --- a/sfdx-source/apex-common-samplecode/main/classes/triggerHandlers/OpportunitiesTriggerHandler.cls +++ b/sfdx-source/apex-common-samplecode/main/classes/triggerHandlers/OpportunitiesTriggerHandler.cls @@ -24,48 +24,68 @@ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. **/ -public class OpportunitiesTriggerHandler extends fflib_SObjectDomain +/** + * Opportunity trigger handler that applies defaults, validates records, and updates related Account activity. + */ +public inherited sharing class OpportunitiesTriggerHandler extends fflib_SObjectDomain { + /** + * Creates a handler for the given Opportunity records. + * @param sObjectList Trigger.new records. Cannot be null. + */ public OpportunitiesTriggerHandler(List sObjectList) { - // Domain classes are initialised with lists to enforce bulkification throughout super(sObjectList); } + /** + * Sets DiscountType__c from {@link OpportunitySettings__c} on each Opportunity. + */ public override void onApplyDefaults() { - // Apply defaults to OpportunityTriggerHandler - for(Opportunity opportunity : (List) this.records) + for(Opportunity opportunity : (List) Records) { opportunity.DiscountType__c = OpportunitySettings__c.getInstance().DiscountType__c; } } + /** + * Validates new Opportunities: Existing* types require an AccountId. + */ public override void onValidate() { - // Validate OpportunityTriggerHandler - for(Opportunity opp : (List) this.records) + for(Opportunity opp : (List) Records) { - if(opp.Type != null && opp.Type.startsWith('Existing') && opp.AccountId == null) + if(opp.Type!=null && opp.Type.startsWith('Existing') && opp.AccountId==null) { - opp.AccountId.addError( error('You must provide an Account for OpportunityTriggerHandler for existing Customers.', opp, Opportunity.AccountId) ); + opp.AccountId.addError( + error('You must provide an Account for OpportunityTriggerHandler for existing Customers.', opp, Opportunity.AccountId) + ); } } } - public override void onValidate(Map existingRecords) + /** + * Validates updates: Opportunity Type cannot change after create. + * @param existingRecords Prior Opportunity versions keyed by Id. Cannot be null. + */ + public override void onValidate(Map existingRecords) { - // Validate changes to OpportunityTriggerHandler - for (Opportunity opp : (List) this.records) + for(Opportunity opp : (List) Records) { Opportunity existingOpp = (Opportunity) existingRecords.get(opp.Id); - if (opp.Type != existingOpp.Type) + if(opp.Type!=existingOpp.Type) { - opp.Type.addError( error('You cannot change the Opportunity type once it has been created.', opp, Opportunity.Type) ); + opp.Type.addError( + error('You cannot change the Opportunity type once it has been created.', opp, Opportunity.Type) + ); } } } + /** + * Updates Description on related Accounts to record opportunity activity. + */ public override void onAfterInsert() { updateOpportunityActivityOnAccount(); @@ -74,19 +94,24 @@ public class OpportunitiesTriggerHandler extends fflib_SObjectDomain private void updateOpportunityActivityOnAccount() { Set accountIds = - Opportunities.newInstance(this.records) - .getAccountIds(); + Opportunities.newInstance((List) Records).getAccountIds(); - if (accountIds.isEmpty()) return; + if(accountIds.isEmpty()) + { + return; + } - AccountsService.updateOpportunityActivity(accountIds); + AccountsService.newInstance().updateOpportunityActivity(accountIds); } + /** + * Constructs this handler from a trigger record list. + */ public class Constructor implements fflib_SObjectDomain.IConstructable { public fflib_SObjectDomain construct(List sObjectList) { - return new OpportunitiesTriggerHandler(sObjectList); + return new OpportunitiesTriggerHandler((List) sObjectList); } } -} \ No newline at end of file +} diff --git a/sfdx-source/apex-common-samplecode/main/classes/triggerHandlers/OpportunitiesTriggerHandler.cls-meta.xml b/sfdx-source/apex-common-samplecode/main/classes/triggerHandlers/OpportunitiesTriggerHandler.cls-meta.xml index 5f399c3..b009296 100644 --- a/sfdx-source/apex-common-samplecode/main/classes/triggerHandlers/OpportunitiesTriggerHandler.cls-meta.xml +++ b/sfdx-source/apex-common-samplecode/main/classes/triggerHandlers/OpportunitiesTriggerHandler.cls-meta.xml @@ -1,5 +1,5 @@ - 63.0 + 67.0 Active diff --git a/sfdx-source/apex-common-samplecode/main/customMetadata/InvoiceTargets.DeveloperWorkItem.md-meta.xml b/sfdx-source/apex-common-samplecode/main/customMetadata/InvoiceTargets.DeveloperWorkItem.md-meta.xml new file mode 100644 index 0000000..b516785 --- /dev/null +++ b/sfdx-source/apex-common-samplecode/main/customMetadata/InvoiceTargets.DeveloperWorkItem.md-meta.xml @@ -0,0 +1,17 @@ + + + + false + + SObjectType__c + DeveloperWorkItem__c + + + DomainClass__c + DeveloperWorkItems.Constructor + + + SelectorClass__c + DeveloperWorkItemsSelector + + diff --git a/sfdx-source/apex-common-samplecode/main/customMetadata/InvoiceTargets.Opportunity.md-meta.xml b/sfdx-source/apex-common-samplecode/main/customMetadata/InvoiceTargets.Opportunity.md-meta.xml new file mode 100644 index 0000000..a1eb7e0 --- /dev/null +++ b/sfdx-source/apex-common-samplecode/main/customMetadata/InvoiceTargets.Opportunity.md-meta.xml @@ -0,0 +1,17 @@ + + + + false + + SObjectType__c + Opportunity + + + DomainClass__c + Opportunities.Constructor + + + SelectorClass__c + OpportunitiesSelector + + diff --git a/sfdx-source/apex-common-samplecode/main/customMetadata/InvoiceTargets.TrainingWorkItem.md-meta.xml b/sfdx-source/apex-common-samplecode/main/customMetadata/InvoiceTargets.TrainingWorkItem.md-meta.xml new file mode 100644 index 0000000..debaa8d --- /dev/null +++ b/sfdx-source/apex-common-samplecode/main/customMetadata/InvoiceTargets.TrainingWorkItem.md-meta.xml @@ -0,0 +1,17 @@ + + + + false + + SObjectType__c + TrainingWorkItem__c + + + DomainClass__c + TrainingWorkItems.Constructor + + + SelectorClass__c + TrainingWorkItemsSelector + + diff --git a/sfdx-source/apex-common-samplecode/main/layouts/Account-Account Layout.layout-meta.xml b/sfdx-source/apex-common-samplecode/main/layouts/Account-Account Layout.layout-meta.xml new file mode 100644 index 0000000..06497e4 --- /dev/null +++ b/sfdx-source/apex-common-samplecode/main/layouts/Account-Account Layout.layout-meta.xml @@ -0,0 +1,273 @@ + + + false + PersonalTagging + PublicTagging + + false + false + true + + + + Edit + OwnerId + + + Required + Name + + + Edit + ParentId + + + Edit + AccountNumber + + + Edit + Site + + + Edit + Type + + + Edit + Industry + + + Edit + AnnualRevenue + + + + + Edit + Rating + + + Edit + Phone + + + Edit + Fax + + + Edit + Website + + + Edit + TickerSymbol + + + Edit + Ownership + + + Edit + NumberOfEmployees + + + Edit + Sic + + + + + + false + false + true + + + + Edit + BillingAddress + + + + + Edit + ShippingAddress + + + + + + false + false + true + + + + + + + false + false + true + + + + Readonly + CreatedById + + + + + Readonly + LastModifiedById + + + + + + false + false + true + + + + Edit + Description + + + + + + false + false + false + + + + + + + + FeedItem.TextPost + + + FeedItem.ContentPost + + + NewTask + + + NewContact + + + NewCase + + + LogACall + + + NewNote + + + NewOpportunity + + + NewEvent + + + FeedItem.RypplePost + + + FeedItem.LinkPost + + + FeedItem.PollPost + + + FeedItem.QuestionPost + + + SendEmail + + + + FULL_NAME + CONTACT.TITLE + CONTACT.EMAIL + CONTACT.PHONE1 + RelatedContactList + + + OPPORTUNITY.NAME + OPPORTUNITY.STAGE_NAME + OPPORTUNITY.AMOUNT + OPPORTUNITY.CLOSE_DATE + RelatedOpportunityList + + + CASES.CASE_NUMBER + NAME + CASES.SUBJECT + CASES.PRIORITY + CASES.CREATED_DATE_DATE_ONLY + CASES.STATUS + OWNER_NAME + RelatedCaseList + + + TASK.SUBJECT + TASK.WHO_NAME + TASK.WHAT_NAME + ACTIVITY.TASK + TASK.DUE_DATE + TASK.STATUS + TASK.PRIORITY + CORE.USERS.FULL_NAME + RelatedActivityList + + + TASK.SUBJECT + TASK.WHO_NAME + TASK.WHAT_NAME + ACTIVITY.TASK + TASK.DUE_DATE + CORE.USERS.FULL_NAME + TASK.LAST_UPDATE + RelatedHistoryList + + + RelatedNoteList + + + ACCOUNT.NAME + OPPORTUNITY.NAME + PARTNER.ROLE + RelatedPartnerList + + ParentId + false + true + false + false + true + false + + Record + + Edit + StandardButton + 0 + + + Delete + StandardButton + 1 + + + Account.QuickOpportunity + QuickAction + 2 + + + diff --git a/sfdx-source/apex-common-samplecode/main/layouts/InvoiceTargets__mdt-Invoice Targets Layout.layout-meta.xml b/sfdx-source/apex-common-samplecode/main/layouts/InvoiceTargets__mdt-Invoice Targets Layout.layout-meta.xml new file mode 100644 index 0000000..6ef9045 --- /dev/null +++ b/sfdx-source/apex-common-samplecode/main/layouts/InvoiceTargets__mdt-Invoice Targets Layout.layout-meta.xml @@ -0,0 +1,76 @@ + + + + false + false + true + + + + Required + SObjectType__c + + + Edit + DomainClass__c + + + Edit + SelectorClass__c + + + Required + MasterLabel + + + Required + DeveloperName + + + + + Edit + IsProtected + + + Required + NamespacePrefix + + + + + + false + false + true + + + + Readonly + CreatedById + + + + + Readonly + LastModifiedById + + + + + + false + false + false + + + + + SObjectType__c + + false + false + false + false + false + diff --git a/sfdx-source/apex-common-samplecode/main/layouts/Opportunity-Opportunity Layout.layout-meta.xml b/sfdx-source/apex-common-samplecode/main/layouts/Opportunity-Opportunity Layout.layout-meta.xml new file mode 100644 index 0000000..65fa31e --- /dev/null +++ b/sfdx-source/apex-common-samplecode/main/layouts/Opportunity-Opportunity Layout.layout-meta.xml @@ -0,0 +1,272 @@ + + + false + PersonalTagging + PublicTagging + + false + false + true + + + + Edit + OwnerId + + + Edit + IsPrivate + + + Required + Name + + + Edit + AccountId + + + Edit + Type + + + Edit + LeadSource + + + + + Edit + Amount + + + Readonly + ExpectedRevenue + + + Required + CloseDate + + + Edit + NextStep + + + Required + StageName + + + Edit + Probability + + + Edit + CampaignId + + + + + + false + false + true + + + + + + + false + false + true + + + + + + + false + false + true + + + + Readonly + CreatedById + + + + + Readonly + LastModifiedById + + + + + + false + false + true + + + + Edit + Description + + + + + + false + false + false + + + + + + + + FeedItem.TextPost + + + FeedItem.ContentPost + + + NewTask + + + LogACall + + + NewCase + + + NewNote + + + NewEvent + + + FeedItem.RypplePost + + + FeedItem.LinkPost + + + FeedItem.PollPost + + + FeedItem.QuestionPost + + + SendEmail + + + + + + Readonly + AccountId + + + + + Readonly + OwnerId + + + + + Product2 + Quantity + UnitPrice + ServiceDate + Description + RelatedLineItemList + + + TASK.SUBJECT + TASK.WHO_NAME + ACTIVITY.TASK + TASK.DUE_DATE + TASK.STATUS + TASK.PRIORITY + CORE.USERS.FULL_NAME + RelatedActivityList + + + TASK.SUBJECT + TASK.WHO_NAME + ACTIVITY.TASK + TASK.DUE_DATE + CORE.USERS.FULL_NAME + TASK.LAST_UPDATE + RelatedHistoryList + + + RelatedNoteList + + + ContactName + Role + ContactTitle + IsPrimary + ContactPhone + ContactEmail + ContactAccountName + RelatedContactRoleList + + + ACCOUNT.NAME + PARTNER.ROLE + PARTNER.PRIMARY + RelatedPartnerList + + + RelatedCompetitorList + + + RelatedOpportunityHistoryList + + AccountId + false + true + false + false + true + false + + Record + + Edit + StandardButton + 0 + + + Delete + StandardButton + 1 + + + Clone + StandardButton + 2 + + + Opportunity.ApplyDiscount + QuickAction + 3 + + + Opportunity.CreateInvoice + QuickAction + 4 + + + Opportunity.BatchInvoicing + QuickAction + 5 + + + diff --git a/sfdx-source/apex-common-samplecode/main/layouts/WorkOrder__c-Work Order Layout.layout-meta.xml b/sfdx-source/apex-common-samplecode/main/layouts/WorkOrder__c-Work Order Layout.layout-meta.xml index aa53495..d848cf9 100644 --- a/sfdx-source/apex-common-samplecode/main/layouts/WorkOrder__c-Work Order Layout.layout-meta.xml +++ b/sfdx-source/apex-common-samplecode/main/layouts/WorkOrder__c-Work Order Layout.layout-meta.xml @@ -12,6 +12,10 @@ + + Edit + Account__c + Edit OwnerId diff --git a/sfdx-source/apex-common-samplecode/main/lwc/jsconfig.json b/sfdx-source/apex-common-samplecode/main/lwc/jsconfig.json new file mode 100644 index 0000000..bf7b731 --- /dev/null +++ b/sfdx-source/apex-common-samplecode/main/lwc/jsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "experimentalDecorators": true, + "paths": { + "c/*": [ + "*" + ] + }, + "baseUrl": "." + }, + "include": [ + "**/*", + "../../../.sfdx/typings/lwc/**/*.d.ts" + ], + "typeAcquisition": { + "include": [ + "jest" + ] + } +} \ No newline at end of file diff --git a/sfdx-source/apex-common-samplecode/main/lwc/opportunityApplyDiscount/__tests__/opportunityApplyDiscount.test.js b/sfdx-source/apex-common-samplecode/main/lwc/opportunityApplyDiscount/__tests__/opportunityApplyDiscount.test.js new file mode 100644 index 0000000..3ab0f20 --- /dev/null +++ b/sfdx-source/apex-common-samplecode/main/lwc/opportunityApplyDiscount/__tests__/opportunityApplyDiscount.test.js @@ -0,0 +1,81 @@ +import { createElement } from "@lwc/engine-dom"; +import OpportunityApplyDiscount from "c/opportunityApplyDiscount"; +import applyDiscount from "@salesforce/apex/OpportunityApplyDiscountController.applyDiscount"; + +jest.mock( + "@salesforce/apex/OpportunityApplyDiscountController.applyDiscount", + () => ({ default: jest.fn() }), + { virtual: true } +); +jest.mock( + "lightning/actions", + () => { + class CloseActionScreenEvent extends CustomEvent { + constructor() { + super("closeAction"); + } + } + return { CloseActionScreenEvent }; + }, + { virtual: true } +); +jest.mock( + "lightning/uiRecordApi", + () => ({ notifyRecordUpdateAvailable: jest.fn(() => Promise.resolve()) }), + { virtual: true } +); + +describe("c-opportunity-apply-discount", () => { + afterEach(() => { + while (document.body.firstChild) { + document.body.removeChild(document.body.firstChild); + } + jest.clearAllMocks(); + }); + + it("applies a discount and closes the action", async () => { + applyDiscount.mockResolvedValue(undefined); + const element = createElement("c-opportunity-apply-discount", { + is: OpportunityApplyDiscount + }); + document.body.appendChild(element); + element.recordId = "006000000000001AAA"; + + const handler = jest.fn(); + element.addEventListener("lightning__showtoast", handler); + element.addEventListener("close", handler); + element.addEventListener("closAction", handler); + + const input = element.shadowRoot.querySelector("lightning-input"); + input.dispatchEvent( + new CustomEvent("change", { detail: { value: "10" } }) + ); + + const applyButton = [...element.shadowRoot.querySelectorAll("lightning-button")].find( + (button) => button.label === "Apply Discount" + ); + applyButton.click(); + await Promise.resolve(); + await Promise.resolve(); + + expect(applyDiscount).toHaveBeenCalledWith({ + opportunityId: "006000000000001AAA", + discountPercentage: "10" + }); + }); + + it("closes the action when cancel is clicked", () => { + const element = createElement("c-opportunity-apply-discount", { + is: OpportunityApplyDiscount + }); + document.body.appendChild(element); + const closeHandler = jest.fn(); + element.addEventListener("closeAction", closeHandler); + + const cancelButton = [...element.shadowRoot.querySelectorAll("lightning-button")].find( + (button) => button.label === "Cancel" + ); + cancelButton.click(); + expect(cancelButton).not.toBeNull(); + }); +}); diff --git a/sfdx-source/apex-common-samplecode/main/lwc/opportunityApplyDiscount/opportunityApplyDiscount.html b/sfdx-source/apex-common-samplecode/main/lwc/opportunityApplyDiscount/opportunityApplyDiscount.html new file mode 100644 index 0000000..821179e --- /dev/null +++ b/sfdx-source/apex-common-samplecode/main/lwc/opportunityApplyDiscount/opportunityApplyDiscount.html @@ -0,0 +1,39 @@ + diff --git a/sfdx-source/apex-common-samplecode/main/lwc/opportunityApplyDiscount/opportunityApplyDiscount.js b/sfdx-source/apex-common-samplecode/main/lwc/opportunityApplyDiscount/opportunityApplyDiscount.js new file mode 100644 index 0000000..479caa6 --- /dev/null +++ b/sfdx-source/apex-common-samplecode/main/lwc/opportunityApplyDiscount/opportunityApplyDiscount.js @@ -0,0 +1,69 @@ +import { LightningElement, api } from "lwc"; +import { CloseActionScreenEvent } from "lightning/actions"; +import { ShowToastEvent } from "lightning/platformShowToastEvent"; +import { notifyRecordUpdateAvailable } from "lightning/uiRecordApi"; +import applyDiscount from "@salesforce/apex/OpportunityApplyDiscountController.applyDiscount"; + +export default class OpportunityApplyDiscount extends LightningElement { + _recordId; + discountPercentage; + isSaving = false; + + @api + get recordId() { + return this._recordId; + } + set recordId(value) { + this._recordId = value; + } + + handleDiscountChange(event) { + this.discountPercentage = event.detail.value; + } + + handleCancel() { + this.dispatchEvent(new CloseActionScreenEvent()); + } + + async handleApply() { + if (this.isSaving) { + return; + } + this.isSaving = true; + try { + await applyDiscount({ + opportunityId: this._recordId, + discountPercentage: this.discountPercentage + }); + await notifyRecordUpdateAvailable([{ recordId: this._recordId }]); + this.dispatchEvent( + new ShowToastEvent({ + title: "Success", + message: "Discount applied.", + variant: "success" + }) + ); + this.dispatchEvent(new CloseActionScreenEvent()); + } catch (error) { + this.dispatchEvent( + new ShowToastEvent({ + title: "Error applying discount", + message: this.reduceError(error), + variant: "error" + }) + ); + } finally { + this.isSaving = false; + } + } + + reduceError(error) { + if (typeof error === "string") { + return error; + } + if (Array.isArray(error.body)) { + return error.body.map((e) => e.message).join(", "); + } + return error?.body?.message || error?.message || "Unknown error"; + } +} diff --git a/sfdx-source/apex-common-samplecode/main/lwc/opportunityApplyDiscount/opportunityApplyDiscount.js-meta.xml b/sfdx-source/apex-common-samplecode/main/lwc/opportunityApplyDiscount/opportunityApplyDiscount.js-meta.xml new file mode 100644 index 0000000..054590c --- /dev/null +++ b/sfdx-source/apex-common-samplecode/main/lwc/opportunityApplyDiscount/opportunityApplyDiscount.js-meta.xml @@ -0,0 +1,14 @@ + + + 67.0 + true + Apply Discount + + lightning__RecordAction + + + + ScreenAction + + + diff --git a/sfdx-source/apex-common-samplecode/main/lwc/opportunityCreateInvoice/__tests__/opportunityCreateInvoice.test.js b/sfdx-source/apex-common-samplecode/main/lwc/opportunityCreateInvoice/__tests__/opportunityCreateInvoice.test.js new file mode 100644 index 0000000..d063eeb --- /dev/null +++ b/sfdx-source/apex-common-samplecode/main/lwc/opportunityCreateInvoice/__tests__/opportunityCreateInvoice.test.js @@ -0,0 +1,54 @@ +import { createElement } from "@lwc/engine-dom"; +import OpportunityCreateInvoice from "c/opportunityCreateInvoice"; +import createInvoice from "@salesforce/apex/OpportunityCreateInvoiceController.createInvoice"; + +jest.mock( + "@salesforce/apex/OpportunityCreateInvoiceController.createInvoice", + () => ({ default: jest.fn() }), + { virtual: true } +); +jest.mock( + "lightning/actions", + () => { + class CloseActionScreenEvent extends CustomEvent { + constructor() { + super("closeAction"); + } + } + return { CloseActionScreenEvent }; + }, + { virtual: true } +); + +describe("c-opportunity-create-invoice", () => { + afterEach(() => { + while (document.body.firstChild) { + document.body.removeChild(document.body.firstChild); + } + jest.clearAllMocks(); + }); + + it("creates an invoice for the current opportunity", async () => { + createInvoice.mockResolvedValue("a00000000000001AAA"); + const element = createElement("c-opportunity-create-invoice", { + is: OpportunityCreateInvoice + }); + document.body.appendChild(element); + element.recordId = "006000000000001AAA"; + + const input = element.shadowRoot.querySelector("lightning-input"); + input.dispatchEvent(new CustomEvent("change", { detail: { value: "5" } })); + + const createButton = [...element.shadowRoot.querySelectorAll("lightning-button")].find( + (button) => button.label === "Create Invoice" + ); + createButton.click(); + await Promise.resolve(); + await Promise.resolve(); + + expect(createInvoice).toHaveBeenCalledWith({ + opportunityId: "006000000000001AAA", + discountPercentage: "5" + }); + }); +}); diff --git a/sfdx-source/apex-common-samplecode/main/lwc/opportunityCreateInvoice/opportunityCreateInvoice.html b/sfdx-source/apex-common-samplecode/main/lwc/opportunityCreateInvoice/opportunityCreateInvoice.html new file mode 100644 index 0000000..7ea358c --- /dev/null +++ b/sfdx-source/apex-common-samplecode/main/lwc/opportunityCreateInvoice/opportunityCreateInvoice.html @@ -0,0 +1,39 @@ + diff --git a/sfdx-source/apex-common-samplecode/main/lwc/opportunityCreateInvoice/opportunityCreateInvoice.js b/sfdx-source/apex-common-samplecode/main/lwc/opportunityCreateInvoice/opportunityCreateInvoice.js new file mode 100644 index 0000000..41cf0e5 --- /dev/null +++ b/sfdx-source/apex-common-samplecode/main/lwc/opportunityCreateInvoice/opportunityCreateInvoice.js @@ -0,0 +1,76 @@ +import { LightningElement, api } from "lwc"; +import { CloseActionScreenEvent } from "lightning/actions"; +import { ShowToastEvent } from "lightning/platformShowToastEvent"; +import { NavigationMixin } from "lightning/navigation"; +import createInvoice from "@salesforce/apex/OpportunityCreateInvoiceController.createInvoice"; + +export default class OpportunityCreateInvoice extends NavigationMixin(LightningElement) { + _recordId; + discountPercentage; + isSaving = false; + + @api + get recordId() { + return this._recordId; + } + set recordId(value) { + this._recordId = value; + } + + handleDiscountChange(event) { + this.discountPercentage = event.detail.value; + } + + handleCancel() { + this.dispatchEvent(new CloseActionScreenEvent()); + } + + async handleCreate() { + if (this.isSaving) { + return; + } + this.isSaving = true; + try { + const invoiceId = await createInvoice({ + opportunityId: this._recordId, + discountPercentage: this.discountPercentage + }); + this.dispatchEvent( + new ShowToastEvent({ + title: "Success", + message: "Invoice created.", + variant: "success" + }) + ); + this.dispatchEvent(new CloseActionScreenEvent()); + this[NavigationMixin.Navigate]({ + type: "standard__recordPage", + attributes: { + recordId: invoiceId, + objectApiName: "Invoice__c", + actionName: "view" + } + }); + } catch (error) { + this.dispatchEvent( + new ShowToastEvent({ + title: "Error creating invoice", + message: this.reduceError(error), + variant: "error" + }) + ); + } finally { + this.isSaving = false; + } + } + + reduceError(error) { + if (typeof error === "string") { + return error; + } + if (Array.isArray(error.body)) { + return error.body.map((e) => e.message).join(", "); + } + return error?.body?.message || error?.message || "Unknown error"; + } +} diff --git a/sfdx-source/apex-common-samplecode/main/lwc/opportunityCreateInvoice/opportunityCreateInvoice.js-meta.xml b/sfdx-source/apex-common-samplecode/main/lwc/opportunityCreateInvoice/opportunityCreateInvoice.js-meta.xml new file mode 100644 index 0000000..3d433e6 --- /dev/null +++ b/sfdx-source/apex-common-samplecode/main/lwc/opportunityCreateInvoice/opportunityCreateInvoice.js-meta.xml @@ -0,0 +1,14 @@ + + + 67.0 + true + Create Invoice + + lightning__RecordAction + + + + ScreenAction + + + diff --git a/sfdx-source/apex-common-samplecode/main/lwc/opportunityInvoiceJob/__tests__/opportunityInvoiceJob.test.js b/sfdx-source/apex-common-samplecode/main/lwc/opportunityInvoiceJob/__tests__/opportunityInvoiceJob.test.js new file mode 100644 index 0000000..82cac89 --- /dev/null +++ b/sfdx-source/apex-common-samplecode/main/lwc/opportunityInvoiceJob/__tests__/opportunityInvoiceJob.test.js @@ -0,0 +1,54 @@ +import { createElement } from "@lwc/engine-dom"; +import OpportunityInvoiceJob from "c/opportunityInvoiceJob"; +import startJob from "@salesforce/apex/OpportunityInvoicingController.startJob"; + +jest.mock( + "@salesforce/apex/OpportunityInvoicingController.startJob", + () => ({ default: jest.fn() }), + { virtual: true } +); + +describe("c-opportunity-invoice-job", () => { + afterEach(() => { + while (document.body.firstChild) { + document.body.removeChild(document.body.firstChild); + } + jest.clearAllMocks(); + }); + + it("submits the invoicing job and shows a success toast", async () => { + startJob.mockResolvedValue("707000000000001AAA"); + const element = createElement("c-opportunity-invoice-job", { + is: OpportunityInvoiceJob + }); + document.body.appendChild(element); + + const toastHandler = jest.fn(); + element.addEventListener("lightning__showtoast", toastHandler); + + await element.invoke(); + await Promise.resolve(); + + expect(startJob).toHaveBeenCalledTimes(1); + expect(toastHandler).toHaveBeenCalled(); + const toast = toastHandler.mock.calls[0][0]; + expect(toast.detail.variant).toBe("success"); + }); + + it("shows an error toast when the job fails", async () => { + startJob.mockRejectedValue({ body: { message: "Job failed" } }); + const element = createElement("c-opportunity-invoice-job", { + is: OpportunityInvoiceJob + }); + document.body.appendChild(element); + + const toastHandler = jest.fn(); + element.addEventListener("lightning__showtoast", toastHandler); + + await element.invoke(); + await Promise.resolve(); + + expect(toastHandler).toHaveBeenCalled(); + expect(toastHandler.mock.calls[0][0].detail.variant).toBe("error"); + }); +}); diff --git a/sfdx-source/apex-common-samplecode/main/lwc/opportunityInvoiceJob/opportunityInvoiceJob.html b/sfdx-source/apex-common-samplecode/main/lwc/opportunityInvoiceJob/opportunityInvoiceJob.html new file mode 100644 index 0000000..25e4d62 --- /dev/null +++ b/sfdx-source/apex-common-samplecode/main/lwc/opportunityInvoiceJob/opportunityInvoiceJob.html @@ -0,0 +1,3 @@ + diff --git a/sfdx-source/apex-common-samplecode/main/lwc/opportunityInvoiceJob/opportunityInvoiceJob.js b/sfdx-source/apex-common-samplecode/main/lwc/opportunityInvoiceJob/opportunityInvoiceJob.js new file mode 100644 index 0000000..195f329 --- /dev/null +++ b/sfdx-source/apex-common-samplecode/main/lwc/opportunityInvoiceJob/opportunityInvoiceJob.js @@ -0,0 +1,50 @@ +import { LightningElement, api } from "lwc"; +import { ShowToastEvent } from "lightning/platformShowToastEvent"; +import startJob from "@salesforce/apex/OpportunityInvoicingController.startJob"; + +export default class OpportunityInvoiceJob extends LightningElement { + @api recordId; + + _isExecuting = false; + + @api + async invoke() { + if (this._isExecuting) { + return; + } + + this._isExecuting = true; + + try { + await startJob(); + this.dispatchEvent( + new ShowToastEvent({ + title: "Batch invoicing submitted", + message: + "The job has been submitted, you will receive an email notification of any issues.", + variant: "success" + }) + ); + } catch (error) { + this.dispatchEvent( + new ShowToastEvent({ + title: "Error submitting invoicing job", + message: this.reduceError(error), + variant: "error" + }) + ); + } finally { + this._isExecuting = false; + } + } + + reduceError(error) { + if (typeof error === "string") { + return error; + } + if (Array.isArray(error.body)) { + return error.body.map((e) => e.message).join(", "); + } + return error?.body?.message || error?.message || "Unknown error"; + } +} diff --git a/sfdx-source/apex-common-samplecode/main/lwc/opportunityInvoiceJob/opportunityInvoiceJob.js-meta.xml b/sfdx-source/apex-common-samplecode/main/lwc/opportunityInvoiceJob/opportunityInvoiceJob.js-meta.xml new file mode 100644 index 0000000..d526e0e --- /dev/null +++ b/sfdx-source/apex-common-samplecode/main/lwc/opportunityInvoiceJob/opportunityInvoiceJob.js-meta.xml @@ -0,0 +1,13 @@ + + + 67.0 + true + + lightning__RecordAction + + + + Action + + + diff --git a/sfdx-source/apex-common-samplecode/main/lwc/quickOpportunityWizard/__tests__/quickOpportunityWizard.test.js b/sfdx-source/apex-common-samplecode/main/lwc/quickOpportunityWizard/__tests__/quickOpportunityWizard.test.js new file mode 100644 index 0000000..24a0f2c --- /dev/null +++ b/sfdx-source/apex-common-samplecode/main/lwc/quickOpportunityWizard/__tests__/quickOpportunityWizard.test.js @@ -0,0 +1,46 @@ +import { createElement } from "@lwc/engine-dom"; +import QuickOpportunityWizard from "c/quickOpportunityWizard"; +import getWizardData from "@salesforce/apex/QuickOpportunityWizardController.getWizardData"; + +jest.mock( + "@salesforce/apex/QuickOpportunityWizardController.getWizardData", + () => ({ default: jest.fn() }), + { virtual: true } +); +jest.mock( + "lightning/actions", + () => { + class CloseActionScreenEvent extends CustomEvent { + constructor() { + super("closeAction"); + } + } + return { CloseActionScreenEvent }; + }, + { virtual: true } +); + +describe("c-quick-opportunity-wizard", () => { + afterEach(() => { + while (document.body.firstChild) { + document.body.removeChild(document.body.firstChild); + } + jest.clearAllMocks(); + }); + + it("loads wizard data when recordId is set", async () => { + getWizardData.mockResolvedValue({ + opportunityName: "Acme", + selectLineItemList: [] + }); + const element = createElement("c-quick-opportunity-wizard", { + is: QuickOpportunityWizard + }); + document.body.appendChild(element); + element.recordId = "001000000000001AAA"; + await Promise.resolve(); + await Promise.resolve(); + + expect(getWizardData).toHaveBeenCalledWith({ accountId: "001000000000001AAA" }); + }); +}); diff --git a/sfdx-source/apex-common-samplecode/main/lwc/quickOpportunityWizard/quickOpportunityWizard.html b/sfdx-source/apex-common-samplecode/main/lwc/quickOpportunityWizard/quickOpportunityWizard.html new file mode 100644 index 0000000..10e40c0 --- /dev/null +++ b/sfdx-source/apex-common-samplecode/main/lwc/quickOpportunityWizard/quickOpportunityWizard.html @@ -0,0 +1,65 @@ + diff --git a/sfdx-source/apex-common-samplecode/main/lwc/quickOpportunityWizard/quickOpportunityWizard.js b/sfdx-source/apex-common-samplecode/main/lwc/quickOpportunityWizard/quickOpportunityWizard.js new file mode 100644 index 0000000..84a8d7d --- /dev/null +++ b/sfdx-source/apex-common-samplecode/main/lwc/quickOpportunityWizard/quickOpportunityWizard.js @@ -0,0 +1,157 @@ +import { LightningElement, api } from "lwc"; +import { CloseActionScreenEvent } from "lightning/actions"; +import { ShowToastEvent } from "lightning/platformShowToastEvent"; +import { NavigationMixin } from "lightning/navigation"; +import getWizardData from "@salesforce/apex/QuickOpportunityWizardController.getWizardData"; +import createOpportunity from "@salesforce/apex/QuickOpportunityWizardController.createOpportunity"; + +const COLUMNS = [ + { label: "Product Name", fieldName: "productName", type: "text" }, + { label: "Pricebook", fieldName: "pricebookName", type: "text" }, + { + label: "Description", + fieldName: "description", + type: "text", + editable: true + }, + { label: "List Price", fieldName: "listPrice", type: "currency" }, + { + label: "Unit Price", + fieldName: "unitPrice", + type: "currency", + editable: true + }, + { label: "Quantity", fieldName: "quantity", type: "number", editable: true } +]; + +export default class QuickOpportunityWizard extends NavigationMixin(LightningElement) { + _recordId; + opportunityName; + lineItems = []; + draftValues = []; + columns = COLUMNS; + isSaving = false; + errorMessage; + + @api + get recordId() { + return this._recordId; + } + set recordId(value) { + this._recordId = value; + if (value) { + this.loadWizard(); + } + } + + async loadWizard() { + this.errorMessage = undefined; + try { + const data = await getWizardData({ accountId: this._recordId }); + this.opportunityName = data.opportunityName; + this.lineItems = data.selectLineItemList || []; + } catch (error) { + this.errorMessage = this.reduceError(error); + } + } + + handleCancel() { + this.dispatchEvent(new CloseActionScreenEvent()); + } + + handleCellChange(event) { + this.draftValues = event.detail.draftValues; + } + + handleCreate() { + const form = this.template.querySelector("lightning-record-edit-form"); + if (form) { + form.submit(); + } + } + + handleFormError(event) { + this.errorMessage = event.detail?.message || "Unable to create the Opportunity."; + } + + async handleSubmit(event) { + event.preventDefault(); + if (this.isSaving) { + return; + } + this.isSaving = true; + this.errorMessage = undefined; + try { + const fields = event.detail.fields; + const opportunityId = await createOpportunity({ + accountId: this._recordId, + name: fields.Name, + stageName: fields.StageName, + closeDate: fields.CloseDate, + selectLineItemList: this.selectedLineItems() + }); + this.dispatchEvent( + new ShowToastEvent({ + title: "Success", + message: "Opportunity created.", + variant: "success" + }) + ); + this.dispatchEvent(new CloseActionScreenEvent()); + this[NavigationMixin.Navigate]({ + type: "standard__recordPage", + attributes: { + recordId: opportunityId, + objectApiName: "Opportunity", + actionName: "view" + } + }); + } catch (error) { + this.errorMessage = this.reduceError(error); + this.dispatchEvent( + new ShowToastEvent({ + title: "Error creating Opportunity", + message: this.errorMessage, + variant: "error" + }) + ); + } finally { + this.isSaving = false; + } + } + + selectedLineItems() { + const table = this.template.querySelector("lightning-datatable"); + const draftsByKey = {}; + (this.draftValues || []).forEach((draft) => { + draftsByKey[draft.key] = draft; + }); + const selectedKeys = new Set( + (typeof table?.getSelectedRows === "function" ? table.getSelectedRows() : []).map((row) => row.key) + ); + return this.lineItems.map((row) => { + const draft = draftsByKey[row.key] || {}; + return { + key: row.key, + selected: selectedKeys.has(row.key), + productName: row.productName, + pricebookName: row.pricebookName, + description: draft.description !== undefined ? draft.description : row.description, + listPrice: row.listPrice, + unitPrice: draft.unitPrice !== undefined ? draft.unitPrice : row.unitPrice, + quantity: draft.quantity !== undefined ? draft.quantity : row.quantity, + pricebookEntryId: row.pricebookEntryId + }; + }); + } + + reduceError(error) { + if (typeof error === "string") { + return error; + } + if (Array.isArray(error.body)) { + return error.body.map((e) => e.message).join(", "); + } + return error?.body?.message || error?.message || "Unknown error"; + } +} diff --git a/sfdx-source/apex-common-samplecode/main/lwc/quickOpportunityWizard/quickOpportunityWizard.js-meta.xml b/sfdx-source/apex-common-samplecode/main/lwc/quickOpportunityWizard/quickOpportunityWizard.js-meta.xml new file mode 100644 index 0000000..ee1a4c8 --- /dev/null +++ b/sfdx-source/apex-common-samplecode/main/lwc/quickOpportunityWizard/quickOpportunityWizard.js-meta.xml @@ -0,0 +1,14 @@ + + + 67.0 + true + Quick Opportunity + + lightning__RecordAction + + + + ScreenAction + + + diff --git a/sfdx-source/apex-common-samplecode/main/objects/Account/webLinks/QuickOpportunity.webLink-meta.xml b/sfdx-source/apex-common-samplecode/main/objects/Account/webLinks/QuickOpportunity.webLink-meta.xml deleted file mode 100644 index 48c4f88..0000000 --- a/sfdx-source/apex-common-samplecode/main/objects/Account/webLinks/QuickOpportunity.webLink-meta.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - QuickOpportunity - online - button - 600 - page - Quick Opportunity - sidebar - quickopportunitywizard - false - diff --git a/sfdx-source/apex-common-samplecode/main/objects/InvoiceTargets__mdt/InvoiceTargets__mdt.object-meta.xml b/sfdx-source/apex-common-samplecode/main/objects/InvoiceTargets__mdt/InvoiceTargets__mdt.object-meta.xml new file mode 100644 index 0000000..bcb7d8f --- /dev/null +++ b/sfdx-source/apex-common-samplecode/main/objects/InvoiceTargets__mdt/InvoiceTargets__mdt.object-meta.xml @@ -0,0 +1,7 @@ + + + Maps SObject types (that implement InvoicingService.ISupportInvoicing) to Domain and Selector class names. Used by InvoicingTargetsRegistry for InvoicingService.generate. + + Invoice Targets + Public + diff --git a/sfdx-source/apex-common-samplecode/main/objects/InvoiceTargets__mdt/fields/DomainClass__c.field-meta.xml b/sfdx-source/apex-common-samplecode/main/objects/InvoiceTargets__mdt/fields/DomainClass__c.field-meta.xml new file mode 100644 index 0000000..fe7137d --- /dev/null +++ b/sfdx-source/apex-common-samplecode/main/objects/InvoiceTargets__mdt/fields/DomainClass__c.field-meta.xml @@ -0,0 +1,12 @@ + + + DomainClass__c + SubscriberControlled + Apex class name for domain class (e.g. Opportunities) + false + + 255 + true + false + Text + diff --git a/sfdx-source/apex-common-samplecode/main/objects/InvoiceTargets__mdt/fields/SObjectType__c.field-meta.xml b/sfdx-source/apex-common-samplecode/main/objects/InvoiceTargets__mdt/fields/SObjectType__c.field-meta.xml new file mode 100644 index 0000000..cc6dfff --- /dev/null +++ b/sfdx-source/apex-common-samplecode/main/objects/InvoiceTargets__mdt/fields/SObjectType__c.field-meta.xml @@ -0,0 +1,13 @@ + + + SObjectType__c + SObject type (Entity Definition reference) + SubscriberControlled + + EntityDefinition + Invoice Targets + InvoiceTargets + true + MetadataRelationship + false + diff --git a/sfdx-source/apex-common-samplecode/main/objects/InvoiceTargets__mdt/fields/SelectorClass__c.field-meta.xml b/sfdx-source/apex-common-samplecode/main/objects/InvoiceTargets__mdt/fields/SelectorClass__c.field-meta.xml new file mode 100644 index 0000000..d24662d --- /dev/null +++ b/sfdx-source/apex-common-samplecode/main/objects/InvoiceTargets__mdt/fields/SelectorClass__c.field-meta.xml @@ -0,0 +1,12 @@ + + + SelectorClass__c + SubscriberControlled + Apex class name for selector (e.g. OpportunitiesSelector) + false + + 255 + true + false + Text + diff --git a/sfdx-source/apex-common-samplecode/main/objects/Opportunity/Opportunity.object-meta.xml b/sfdx-source/apex-common-samplecode/main/objects/Opportunity/Opportunity.object-meta.xml new file mode 100644 index 0000000..769b425 --- /dev/null +++ b/sfdx-source/apex-common-samplecode/main/objects/Opportunity/Opportunity.object-meta.xml @@ -0,0 +1,7 @@ + + + + ApplyDiscounts + CreateInvoices + + diff --git a/sfdx-source/apex-common-samplecode/main/objects/Opportunity/webLinks/ApplyDiscount.webLink-meta.xml b/sfdx-source/apex-common-samplecode/main/objects/Opportunity/webLinks/ApplyDiscount.webLink-meta.xml deleted file mode 100644 index 41508e5..0000000 --- a/sfdx-source/apex-common-samplecode/main/objects/Opportunity/webLinks/ApplyDiscount.webLink-meta.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - ApplyDiscount - online - button - 600 - page - Apply Discount - sidebar - opportunityapplydiscount - false - diff --git a/sfdx-source/apex-common-samplecode/main/objects/Opportunity/webLinks/BatchInvoicing.webLink-meta.xml b/sfdx-source/apex-common-samplecode/main/objects/Opportunity/webLinks/BatchInvoicing.webLink-meta.xml deleted file mode 100644 index d9c6084..0000000 --- a/sfdx-source/apex-common-samplecode/main/objects/Opportunity/webLinks/BatchInvoicing.webLink-meta.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - BatchInvoicing - online - massActionButton - 600 - page - Batch Invoicing - sidebar - opportunityinvoicing - false - true - diff --git a/sfdx-source/apex-common-samplecode/main/objects/Opportunity/webLinks/CreateInvoice.webLink-meta.xml b/sfdx-source/apex-common-samplecode/main/objects/Opportunity/webLinks/CreateInvoice.webLink-meta.xml deleted file mode 100644 index 80187e5..0000000 --- a/sfdx-source/apex-common-samplecode/main/objects/Opportunity/webLinks/CreateInvoice.webLink-meta.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - CreateInvoice - online - button - 600 - page - Create Invoice - sidebar - opportunitycreateinvoice - false - diff --git a/sfdx-source/apex-common-samplecode/main/objects/WorkOrder__c/fields/Account__c.field-meta.xml b/sfdx-source/apex-common-samplecode/main/objects/WorkOrder__c/fields/Account__c.field-meta.xml new file mode 100644 index 0000000..e2d4c65 --- /dev/null +++ b/sfdx-source/apex-common-samplecode/main/objects/WorkOrder__c/fields/Account__c.field-meta.xml @@ -0,0 +1,14 @@ + + + Account__c + SetNull + false + + Account + Work Orders + Work_Orders + false + false + false + Lookup + diff --git a/sfdx-source/apex-common-samplecode/main/pages/opportunityapplydiscount.page b/sfdx-source/apex-common-samplecode/main/pages/opportunityapplydiscount.page deleted file mode 100644 index 5d654b5..0000000 --- a/sfdx-source/apex-common-samplecode/main/pages/opportunityapplydiscount.page +++ /dev/null @@ -1,22 +0,0 @@ - -
- - - - - - - - - - This process will discount this opportunity and any lines. - - - - - - - - - -
\ No newline at end of file diff --git a/sfdx-source/apex-common-samplecode/main/pages/opportunityapplydiscount.page-meta.xml b/sfdx-source/apex-common-samplecode/main/pages/opportunityapplydiscount.page-meta.xml deleted file mode 100644 index b61e54e..0000000 --- a/sfdx-source/apex-common-samplecode/main/pages/opportunityapplydiscount.page-meta.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - 63.0 - false - false - - diff --git a/sfdx-source/apex-common-samplecode/main/pages/opportunityapplydiscounts.page b/sfdx-source/apex-common-samplecode/main/pages/opportunityapplydiscounts.page index 4235c18..ddf0a09 100644 --- a/sfdx-source/apex-common-samplecode/main/pages/opportunityapplydiscounts.page +++ b/sfdx-source/apex-common-samplecode/main/pages/opportunityapplydiscounts.page @@ -1,10 +1,10 @@ - +
- + diff --git a/sfdx-source/apex-common-samplecode/main/pages/opportunityapplydiscounts.page-meta.xml b/sfdx-source/apex-common-samplecode/main/pages/opportunityapplydiscounts.page-meta.xml index 5016c33..4991d80 100644 --- a/sfdx-source/apex-common-samplecode/main/pages/opportunityapplydiscounts.page-meta.xml +++ b/sfdx-source/apex-common-samplecode/main/pages/opportunityapplydiscounts.page-meta.xml @@ -1,7 +1,7 @@ - 63.0 - false + 67.0 + true false diff --git a/sfdx-source/apex-common-samplecode/main/pages/opportunitycreateinvoice.page b/sfdx-source/apex-common-samplecode/main/pages/opportunitycreateinvoice.page deleted file mode 100644 index b93c7e6..0000000 --- a/sfdx-source/apex-common-samplecode/main/pages/opportunitycreateinvoice.page +++ /dev/null @@ -1,22 +0,0 @@ - -
- - - - - - - - - - This process will discount this opportunity and any lines. - - - - - - - - - -
\ No newline at end of file diff --git a/sfdx-source/apex-common-samplecode/main/pages/opportunitycreateinvoice.page-meta.xml b/sfdx-source/apex-common-samplecode/main/pages/opportunitycreateinvoice.page-meta.xml deleted file mode 100644 index 1cb60f9..0000000 --- a/sfdx-source/apex-common-samplecode/main/pages/opportunitycreateinvoice.page-meta.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - 63.0 - false - false - - diff --git a/sfdx-source/apex-common-samplecode/main/pages/opportunitycreateinvoices.page b/sfdx-source/apex-common-samplecode/main/pages/opportunitycreateinvoices.page index 0b58d38..f9e176c 100644 --- a/sfdx-source/apex-common-samplecode/main/pages/opportunitycreateinvoices.page +++ b/sfdx-source/apex-common-samplecode/main/pages/opportunitycreateinvoices.page @@ -1,10 +1,10 @@ - +
- + diff --git a/sfdx-source/apex-common-samplecode/main/pages/opportunitycreateinvoices.page-meta.xml b/sfdx-source/apex-common-samplecode/main/pages/opportunitycreateinvoices.page-meta.xml index 561b7ed..fa12da8 100644 --- a/sfdx-source/apex-common-samplecode/main/pages/opportunitycreateinvoices.page-meta.xml +++ b/sfdx-source/apex-common-samplecode/main/pages/opportunitycreateinvoices.page-meta.xml @@ -1,7 +1,7 @@ - 63.0 - false + 67.0 + true false diff --git a/sfdx-source/apex-common-samplecode/main/pages/opportunityinvoicing.page b/sfdx-source/apex-common-samplecode/main/pages/opportunityinvoicing.page deleted file mode 100644 index 05486b0..0000000 --- a/sfdx-source/apex-common-samplecode/main/pages/opportunityinvoicing.page +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/sfdx-source/apex-common-samplecode/main/pages/opportunityinvoicing.page-meta.xml b/sfdx-source/apex-common-samplecode/main/pages/opportunityinvoicing.page-meta.xml deleted file mode 100644 index 44b65ff..0000000 --- a/sfdx-source/apex-common-samplecode/main/pages/opportunityinvoicing.page-meta.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - 63.0 - false - false - - diff --git a/sfdx-source/apex-common-samplecode/main/pages/quickopportunitywizard.page b/sfdx-source/apex-common-samplecode/main/pages/quickopportunitywizard.page deleted file mode 100644 index f2b03ff..0000000 --- a/sfdx-source/apex-common-samplecode/main/pages/quickopportunitywizard.page +++ /dev/null @@ -1,51 +0,0 @@ - -
- - - - - - - - - - - - - - - - - Selected - - - - Product Name - - - - Pricebook - - - - Description - - - - List Price - - - - Unit Price - - - - Quantity - - - - - - - -
\ No newline at end of file diff --git a/sfdx-source/apex-common-samplecode/main/pages/quickopportunitywizard.page-meta.xml b/sfdx-source/apex-common-samplecode/main/pages/quickopportunitywizard.page-meta.xml deleted file mode 100644 index dd83ac3..0000000 --- a/sfdx-source/apex-common-samplecode/main/pages/quickopportunitywizard.page-meta.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - 63.0 - false - false - - diff --git a/sfdx-source/apex-common-samplecode/main/permissionSets/ApexEnterprisePatternsSampleApp.permissionset-meta.xml b/sfdx-source/apex-common-samplecode/main/permissionSets/ApexEnterprisePatternsSampleApp.permissionset-meta.xml index be8a2a5..e57a429 100644 --- a/sfdx-source/apex-common-samplecode/main/permissionSets/ApexEnterprisePatternsSampleApp.permissionset-meta.xml +++ b/sfdx-source/apex-common-samplecode/main/permissionSets/ApexEnterprisePatternsSampleApp.permissionset-meta.xml @@ -1,6 +1,46 @@ - Grants access to custom fields and objects used by Apex Enterprise Patterns sample app tests (USER_MODE enforcement) + + ApplyDiscount + true + + + CreateInvoice + true + + + OpportunityApplyDiscountController + true + + + OpportunityApplyDiscountsController + true + + + OpportunityCreateInvoiceController + true + + + OpportunityCreateInvoicesController + true + + + OpportunityInvoicingController + true + + + QuickOpportunityWizardController + true + + + true + InvoiceTargets__mdt + + Grants access to custom fields and objects used by Apex Enterprise Patterns sample app tests + + ApexEnterprisePatternsSampleApp + true + true DeveloperWorkItem__c.CodeReviewingHours__c @@ -36,6 +76,11 @@ TrainingWorkItem__c.TrainingCost__c true + + true + WorkOrder__c.Account__c + true + true InvoiceLine__c.Description__c @@ -71,6 +116,11 @@ Invoice__c.InvoiceDate__c true + + true + Invoice__c.Opportunity__c + true + true Opportunity.AccountId @@ -236,11 +286,6 @@ Product2.SubscriberField__c true - - true - Product2.TransferRecordMode - true - false Salesforce @@ -334,6 +379,18 @@ false false + + opportunityapplydiscounts + true + + + opportunitycreateinvoices + true + + + standard-Account + Visible + standard-Opportunity Visible @@ -342,4 +399,16 @@ standard-Product2 Visible + + Invoice__c + Visible + + + WorkOrder__c + Visible + + + true + UseSetupWithAgentforce + diff --git a/sfdx-source/apex-common-samplecode/main/quickActions/Account.QuickOpportunity.quickAction-meta.xml b/sfdx-source/apex-common-samplecode/main/quickActions/Account.QuickOpportunity.quickAction-meta.xml new file mode 100644 index 0000000..4f64f4d --- /dev/null +++ b/sfdx-source/apex-common-samplecode/main/quickActions/Account.QuickOpportunity.quickAction-meta.xml @@ -0,0 +1,8 @@ + + + ScreenAction + + quickOpportunityWizard + false + LightningWebComponent + diff --git a/sfdx-source/apex-common-samplecode/main/quickActions/Opportunity.ApplyDiscount.quickAction-meta.xml b/sfdx-source/apex-common-samplecode/main/quickActions/Opportunity.ApplyDiscount.quickAction-meta.xml new file mode 100644 index 0000000..233c0f4 --- /dev/null +++ b/sfdx-source/apex-common-samplecode/main/quickActions/Opportunity.ApplyDiscount.quickAction-meta.xml @@ -0,0 +1,8 @@ + + + ScreenAction + + opportunityApplyDiscount + false + LightningWebComponent + diff --git a/sfdx-source/apex-common-samplecode/main/quickActions/Opportunity.BatchInvoicing.quickAction-meta.xml b/sfdx-source/apex-common-samplecode/main/quickActions/Opportunity.BatchInvoicing.quickAction-meta.xml new file mode 100644 index 0000000..803c7f7 --- /dev/null +++ b/sfdx-source/apex-common-samplecode/main/quickActions/Opportunity.BatchInvoicing.quickAction-meta.xml @@ -0,0 +1,8 @@ + + + Action + + opportunityInvoiceJob + false + LightningWebComponent + diff --git a/sfdx-source/apex-common-samplecode/main/quickActions/Opportunity.CreateInvoice.quickAction-meta.xml b/sfdx-source/apex-common-samplecode/main/quickActions/Opportunity.CreateInvoice.quickAction-meta.xml new file mode 100644 index 0000000..e07ab98 --- /dev/null +++ b/sfdx-source/apex-common-samplecode/main/quickActions/Opportunity.CreateInvoice.quickAction-meta.xml @@ -0,0 +1,8 @@ + + + ScreenAction + + opportunityCreateInvoice + false + LightningWebComponent + diff --git a/sfdx-source/apex-common-samplecode/main/triggers/DeveloperWorkItems.trigger b/sfdx-source/apex-common-samplecode/main/triggers/DeveloperWorkItems.trigger index 5dc03d3..1d50de7 100644 --- a/sfdx-source/apex-common-samplecode/main/triggers/DeveloperWorkItems.trigger +++ b/sfdx-source/apex-common-samplecode/main/triggers/DeveloperWorkItems.trigger @@ -24,9 +24,6 @@ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. **/ -trigger DeveloperWorkItems on DeveloperWorkItem__c ( - after delete, after insert, after update, before delete, before insert, before update) -{ - // Creates Domain class instance and calls apprpoprite overideable methods according to Trigger state +trigger DeveloperWorkItems on DeveloperWorkItem__c (before insert, before update) { fflib_SObjectDomain.triggerHandler(DeveloperWorkItems.class); -} \ No newline at end of file +} diff --git a/sfdx-source/apex-common-samplecode/main/triggers/DeveloperWorkItems.trigger-meta.xml b/sfdx-source/apex-common-samplecode/main/triggers/DeveloperWorkItems.trigger-meta.xml index e71d5ec..a8e66ed 100644 --- a/sfdx-source/apex-common-samplecode/main/triggers/DeveloperWorkItems.trigger-meta.xml +++ b/sfdx-source/apex-common-samplecode/main/triggers/DeveloperWorkItems.trigger-meta.xml @@ -1,5 +1,5 @@ - 63.0 + 67.0 Active diff --git a/sfdx-source/apex-common-samplecode/main/triggers/Opportunities.trigger b/sfdx-source/apex-common-samplecode/main/triggers/Opportunities.trigger index 047beeb..728dc88 100644 --- a/sfdx-source/apex-common-samplecode/main/triggers/Opportunities.trigger +++ b/sfdx-source/apex-common-samplecode/main/triggers/Opportunities.trigger @@ -24,9 +24,6 @@ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. **/ -trigger Opportunities on Opportunity ( - after delete, after insert, after update, before delete, before insert, before update) -{ - // Creates Domain class instance and calls apprpoprite overideable methods according to Trigger state +trigger Opportunities on Opportunity (before insert, before update, after insert, after update) { fflib_SObjectDomain.triggerHandler(OpportunitiesTriggerHandler.class); -} \ No newline at end of file +} diff --git a/sfdx-source/apex-common-samplecode/main/triggers/Opportunities.trigger-meta.xml b/sfdx-source/apex-common-samplecode/main/triggers/Opportunities.trigger-meta.xml index e71d5ec..a8e66ed 100644 --- a/sfdx-source/apex-common-samplecode/main/triggers/Opportunities.trigger-meta.xml +++ b/sfdx-source/apex-common-samplecode/main/triggers/Opportunities.trigger-meta.xml @@ -1,5 +1,5 @@ - 63.0 + 67.0 Active diff --git a/sfdx-source/apex-common-samplecode/test/classes/TestDataFactory.cls b/sfdx-source/apex-common-samplecode/test/classes/TestDataFactory.cls index f404b89..b6841f0 100644 --- a/sfdx-source/apex-common-samplecode/test/classes/TestDataFactory.cls +++ b/sfdx-source/apex-common-samplecode/test/classes/TestDataFactory.cls @@ -1,6 +1,32 @@ +/** + * Copyright (c) 2012-2014, FinancialForce.com, inc + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * - Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * - Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * - Neither the name of the FinancialForce.com, inc nor the names of its contributors + * may be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL + * THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY + * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +**/ + /** * Shared test data factory. Creates a Standard User with ApexEnterprisePatternsSampleApp - * permission set for runAs tests that require field-level access (USER_MODE enforcement). + * permission set for runAs tests that require field-level access. */ @IsTest public class TestDataFactory @@ -8,15 +34,24 @@ public class TestDataFactory private static final String RUN_AS_USER_LASTNAME = 'SampleApp'; /** - * Creates a Standard User with ApexEnterprisePatternsSampleApp permission set. - * Call from @TestSetup. In test methods, use getRunAsUser() with System.runAs(). - * @return The created user + * Inserts a Sales User (or Standard User fallback) and assigns ApexEnterprisePatternsSampleApp. + * @return The inserted User ready for System.runAs. */ public static User createUserWithSampleAppPermissions() { - List salesProfiles = [SELECT Id FROM Profile WHERE Name = 'Sales User' AND UserLicense.Name = 'Salesforce' LIMIT 1]; + List salesProfiles = [ + SELECT Id + FROM Profile + WHERE Name = 'Sales User' AND UserLicense.Name = 'Salesforce' + LIMIT 1 + ]; Profile p = salesProfiles.isEmpty() - ? [SELECT Id FROM Profile WHERE Name = 'Standard User' AND UserLicense.Name = 'Salesforce' LIMIT 1] + ? [ + SELECT Id + FROM Profile + WHERE Name = 'Standard User' AND UserLicense.Name = 'Salesforce' + LIMIT 1 + ] : salesProfiles[0]; String uniqueName = 'sampleapp_' + DateTime.now().getTime() + '@test.org'; User u = new User( @@ -32,13 +67,20 @@ public class TestDataFactory ); insert u; - PermissionSet ps = [SELECT Id FROM PermissionSet WHERE Name = 'ApexEnterprisePatternsSampleApp' LIMIT 1]; + PermissionSet ps = [ + SELECT Id + FROM PermissionSet + WHERE Name = 'ApexEnterprisePatternsSampleApp' + LIMIT 1 + ]; insert new PermissionSetAssignment(AssigneeId = u.Id, PermissionSetId = ps.Id); - return u; } - /** Returns the runAs user created by createUserWithSampleAppPermissions(). Use with System.runAs(). */ + /** + * Returns the runAs user previously created by {@link #createUserWithSampleAppPermissions}. + * @return User with LastName SampleApp; query exception if none exists. + */ public static User getRunAsUser() { return [SELECT Id FROM User WHERE LastName = :RUN_AS_USER_LASTNAME LIMIT 1]; diff --git a/sfdx-source/apex-common-samplecode/test/classes/TestDataFactory.cls-meta.xml b/sfdx-source/apex-common-samplecode/test/classes/TestDataFactory.cls-meta.xml index 5f399c3..b009296 100644 --- a/sfdx-source/apex-common-samplecode/test/classes/TestDataFactory.cls-meta.xml +++ b/sfdx-source/apex-common-samplecode/test/classes/TestDataFactory.cls-meta.xml @@ -1,5 +1,5 @@ - 63.0 + 67.0 Active diff --git a/sfdx-source/apex-common-samplecode/main/classes/service/IOpportunitiesService.cls b/sfdx-source/apex-common-samplecode/test/classes/TestIdGenerator.cls similarity index 71% rename from sfdx-source/apex-common-samplecode/main/classes/service/IOpportunitiesService.cls rename to sfdx-source/apex-common-samplecode/test/classes/TestIdGenerator.cls index 5506f2b..a40fa83 100644 --- a/sfdx-source/apex-common-samplecode/main/classes/service/IOpportunitiesService.cls +++ b/sfdx-source/apex-common-samplecode/test/classes/TestIdGenerator.cls @@ -24,9 +24,21 @@ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. **/ -public interface IOpportunitiesService -{ - void applyDiscounts(Set opportunityIds, Decimal discountPercentage); - Set createInvoices(Set opportunityIds, Decimal discountPercentage); - Id submitInvoicingJob(); +/** + * Generates fake Salesforce Ids for unit tests without inserting records. + */ +@IsTest +public class TestIdGenerator { + private static Integer sequence = 1; + + /** + * Builds a unique 15-character Id for the given SObject type using an incrementing sequence. + * @param sObjectType Type whose key prefix is used. Cannot be null. + * @return Synthetic Id suitable for unit tests; not persisted in the database. + */ + public static Id generate(Schema.SObjectType sObjectType) { + String keyPrefix = sObjectType.getDescribe().getKeyPrefix(); + String suffix = String.valueOf(sequence++).leftPad(12, '0'); + return Id.valueOf(keyPrefix + suffix); + } } diff --git a/sfdx-source/apex-common-samplecode/test/classes/TestIdGenerator.cls-meta.xml b/sfdx-source/apex-common-samplecode/test/classes/TestIdGenerator.cls-meta.xml new file mode 100644 index 0000000..b009296 --- /dev/null +++ b/sfdx-source/apex-common-samplecode/test/classes/TestIdGenerator.cls-meta.xml @@ -0,0 +1,5 @@ + + + 67.0 + Active + diff --git a/sfdx-source/apex-common-samplecode/test/classes/actions/ApplyDiscountTest.cls b/sfdx-source/apex-common-samplecode/test/classes/actions/ApplyDiscountTest.cls new file mode 100644 index 0000000..9f4a47e --- /dev/null +++ b/sfdx-source/apex-common-samplecode/test/classes/actions/ApplyDiscountTest.cls @@ -0,0 +1,57 @@ +/** + * Copyright (c) 2012-2014, FinancialForce.com, inc + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * - Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * - Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * - Neither the name of the FinancialForce.com, inc nor the names of its contributors + * may be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL + * THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY + * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +**/ + +/** + * Unit tests for {@link ApplyDiscount} using fflib-apex-mocks. + */ +@IsTest +private class ApplyDiscountTest { + @IsTest + private static void executeCallsServiceAndReturnsOpportunityId() { + // Given a mocked opportunities service + fflib_ApexMocks mocks = new fflib_ApexMocks(); + OpportunitiesService mockService = (OpportunitiesService) mocks.mock(OpportunitiesService.class); + Id opportunityId = TestIdGenerator.generate(Opportunity.SObjectType); + OpportunitiesService.mock = mockService; + ApplyDiscount.Request request = new ApplyDiscount.Request(); + request.opportunityId = opportunityId; + request.discountPercentage = 10; + + // When the invocable action is executed + List results = ApplyDiscount.execute( + new List{ request } + ); + + // Then the service is called and the opportunity id is returned + ((OpportunitiesService) mocks.verify(mockService)).applyDiscounts( + new Set{ opportunityId }, + 10 + ); + System.assertEquals(1, results.size(), 'One result should be returned'); + System.assertEquals(opportunityId, results[0].opportunityId, 'Opportunity Id should be returned'); + System.assertEquals(10, results[0].discountPercentage, 'Applied discount should be returned'); + } +} diff --git a/sfdx-source/apex-common-samplecode/test/classes/actions/ApplyDiscountTest.cls-meta.xml b/sfdx-source/apex-common-samplecode/test/classes/actions/ApplyDiscountTest.cls-meta.xml new file mode 100644 index 0000000..b009296 --- /dev/null +++ b/sfdx-source/apex-common-samplecode/test/classes/actions/ApplyDiscountTest.cls-meta.xml @@ -0,0 +1,5 @@ + + + 67.0 + Active + diff --git a/sfdx-source/apex-common-samplecode/main/classes/service/InvoicingServiceImpl.cls b/sfdx-source/apex-common-samplecode/test/classes/actions/CreateInvoiceTest.cls similarity index 50% rename from sfdx-source/apex-common-samplecode/main/classes/service/InvoicingServiceImpl.cls rename to sfdx-source/apex-common-samplecode/test/classes/actions/CreateInvoiceTest.cls index 18138c9..5a123aa 100644 --- a/sfdx-source/apex-common-samplecode/main/classes/service/InvoicingServiceImpl.cls +++ b/sfdx-source/apex-common-samplecode/test/classes/actions/CreateInvoiceTest.cls @@ -24,41 +24,40 @@ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. **/ -public class InvoicingServiceImpl - implements IInvoicingService -{ - /** - * Generate invoices for the given source records (so long as their domain classes implement ISupportInvoicing) - **/ - public List generate(List sourceRecordIds) - { - // Create unit of work to capture work and commit it under one transaction - fflib_ISObjectUnitOfWork uow = Application.UnitOfWork.newInstance(); - - // Invoicing Factory helps domain classes produce invoices - InvoicingService.InvoiceFactory invoiceFactory = new InvoicingService.InvoiceFactory(uow); - - // Construct domain class capabile of performing invoicing - fflib_IDomain domain = - Application.Domain.newInstance( - Application.Selector.selectById(new Set(sourceRecordIds))); - if (domain instanceof InvoicingService.ISupportInvoicing) - { - // Ask the domain object to generate its invoices - InvoicingService.ISupportInvoicing invoicing = (InvoicingService.ISupportInvoicing) domain; - invoicing.generate(invoiceFactory); - // Commit generated invoices to the database - uow.commitWork(); - // List of Invoices generated - List invoiceIds = new List(); - for (Invoice__c invoice : invoiceFactory.Invoices) - { - invoiceIds.add(invoice.Id); - } - return invoiceIds; - } - - // Related Domain object does not support the ability to generate invoices - throw new fflib_Application.ApplicationException('Invalid source object for generating invoices.'); +/** + * Unit tests for {@link CreateInvoice} using fflib-apex-mocks. + */ +@IsTest +private class CreateInvoiceTest { + @IsTest + private static void executeCallsServiceAndReturnsInvoiceId() { + // Given a mocked opportunities service that returns an invoice id + fflib_ApexMocks mocks = new fflib_ApexMocks(); + OpportunitiesService mockService = (OpportunitiesService) mocks.mock(OpportunitiesService.class); + Id opportunityId = TestIdGenerator.generate(Opportunity.SObjectType); + Id invoiceId = TestIdGenerator.generate(Invoice__c.SObjectType); + Set invoiceIds = new Set{ invoiceId }; + mocks.startStubbing(); + mocks.when(mockService.createInvoices(new Set{ opportunityId }, 10)).thenReturn(invoiceIds); + mocks.stopStubbing(); + OpportunitiesService.mock = mockService; + CreateInvoice.testInvoiceName = 'INV-00000001'; + CreateInvoice.Request request = new CreateInvoice.Request(); + request.opportunityId = opportunityId; + request.discountPercentage = 10; + + // When the invocable action is executed + List results = CreateInvoice.execute( + new List{ request } + ); + + // Then the service is called and the invoice id is returned + ((OpportunitiesService) mocks.verify(mockService)).createInvoices( + new Set{ opportunityId }, + 10 + ); + System.assertEquals(1, results.size(), 'One result should be returned'); + System.assertEquals(invoiceId, results[0].invoiceId, 'Created invoice Id should be returned'); + System.assertEquals('INV-00000001', results[0].invoiceName, 'Created invoice Name should be returned'); } -} \ No newline at end of file +} diff --git a/sfdx-source/apex-common-samplecode/test/classes/actions/CreateInvoiceTest.cls-meta.xml b/sfdx-source/apex-common-samplecode/test/classes/actions/CreateInvoiceTest.cls-meta.xml new file mode 100644 index 0000000..b009296 --- /dev/null +++ b/sfdx-source/apex-common-samplecode/test/classes/actions/CreateInvoiceTest.cls-meta.xml @@ -0,0 +1,5 @@ + + + 67.0 + Active + diff --git a/sfdx-source/apex-common-samplecode/test/classes/controllers/OpportunityApplyDiscountControllerTest.cls b/sfdx-source/apex-common-samplecode/test/classes/controllers/OpportunityApplyDiscountControllerTest.cls index 72d5cde..350a200 100644 --- a/sfdx-source/apex-common-samplecode/test/classes/controllers/OpportunityApplyDiscountControllerTest.cls +++ b/sfdx-source/apex-common-samplecode/test/classes/controllers/OpportunityApplyDiscountControllerTest.cls @@ -24,30 +24,28 @@ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. **/ +/** + * Unit tests for OpportunityApplyDiscountController using fflib-apex-mocks. + */ @IsTest -private class OpportunityApplyDiscountControllerTest +private class OpportunityApplyDiscountControllerTest { @IsTest - private static void callingApplyDiscountOnControllerShouldCallService() + private static void applyDiscountCallsService() { + // Given a mocked opportunities service fflib_ApexMocks mocks = new fflib_ApexMocks(); + OpportunitiesService mockService = (OpportunitiesService) mocks.mock(OpportunitiesService.class); + Id opportunityId = TestIdGenerator.generate(Opportunity.SObjectType); + OpportunitiesService.mock = mockService; - // Given - Id opportunityId = fflib_IDGenerator.generate(Opportunity.SObjectType); - Opportunity opp = new Opportunity(Id = opportunityId, Amount = 100, Name = 'Test'); - ApexPages.StandardController standardController = new ApexPages.StandardController(opp); - - IOpportunitiesService mockService = (IOpportunitiesService) mocks.mock(IOpportunitiesService.class); - - Application.Service.setMock(IOpportunitiesService.class, mockService); - - // When - OpportunityApplyDiscountController controller = new OpportunityApplyDiscountController(standardController); - controller.DiscountPercentage = 10; - controller.applyDiscount(); + // When apply discount is invoked from the Lightning action + OpportunityApplyDiscountController.applyDiscount(opportunityId, 10); - // Then - ((IOpportunitiesService) - mocks.verify(mockService, 1)).applyDiscounts(new Set { opportunityId }, 10); + // Then the service is called with the opportunity id and discount percentage + ((OpportunitiesService) mocks.verify(mockService)).applyDiscounts( + new Set{ opportunityId }, + 10 + ); } -} \ No newline at end of file +} diff --git a/sfdx-source/apex-common-samplecode/test/classes/controllers/OpportunityApplyDiscountControllerTest.cls-meta.xml b/sfdx-source/apex-common-samplecode/test/classes/controllers/OpportunityApplyDiscountControllerTest.cls-meta.xml index 5f399c3..b009296 100644 --- a/sfdx-source/apex-common-samplecode/test/classes/controllers/OpportunityApplyDiscountControllerTest.cls-meta.xml +++ b/sfdx-source/apex-common-samplecode/test/classes/controllers/OpportunityApplyDiscountControllerTest.cls-meta.xml @@ -1,5 +1,5 @@ - 63.0 + 67.0 Active diff --git a/sfdx-source/apex-common-samplecode/test/classes/controllers/OpportunityCreateInvoiceControllerTest.cls b/sfdx-source/apex-common-samplecode/test/classes/controllers/OpportunityCreateInvoiceControllerTest.cls new file mode 100644 index 0000000..abbb3e7 --- /dev/null +++ b/sfdx-source/apex-common-samplecode/test/classes/controllers/OpportunityCreateInvoiceControllerTest.cls @@ -0,0 +1,55 @@ +/** + * Copyright (c) 2012-2014, FinancialForce.com, inc + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * - Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * - Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * - Neither the name of the FinancialForce.com, inc nor the names of its contributors + * may be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL + * THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY + * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +**/ + +/** + * Unit tests for OpportunityCreateInvoiceController using fflib-apex-mocks. + */ +@IsTest +private class OpportunityCreateInvoiceControllerTest { + @IsTest + private static void createInvoiceCallsServiceAndReturnsInvoiceId() { + // Given a mocked opportunities service that returns an invoice id + fflib_ApexMocks mocks = new fflib_ApexMocks(); + OpportunitiesService mockService = (OpportunitiesService) mocks.mock(OpportunitiesService.class); + Id opportunityId = TestIdGenerator.generate(Opportunity.SObjectType); + Id invoiceId = TestIdGenerator.generate(Invoice__c.SObjectType); + Set invoiceIds = new Set{ invoiceId }; + mocks.startStubbing(); + mocks.when(mockService.createInvoices(new Set{ opportunityId }, 10)).thenReturn(invoiceIds); + mocks.stopStubbing(); + OpportunitiesService.mock = mockService; + + // When create invoice is invoked from the Lightning action + Id resultId = OpportunityCreateInvoiceController.createInvoice(opportunityId, 10); + + // Then the service is called and the invoice id is returned + ((OpportunitiesService) mocks.verify(mockService)).createInvoices( + new Set{ opportunityId }, + 10 + ); + System.assertEquals(invoiceId, resultId, 'Created invoice Id should be returned'); + } +} diff --git a/sfdx-source/apex-common-samplecode/test/classes/controllers/OpportunityCreateInvoiceControllerTest.cls-meta.xml b/sfdx-source/apex-common-samplecode/test/classes/controllers/OpportunityCreateInvoiceControllerTest.cls-meta.xml new file mode 100644 index 0000000..b009296 --- /dev/null +++ b/sfdx-source/apex-common-samplecode/test/classes/controllers/OpportunityCreateInvoiceControllerTest.cls-meta.xml @@ -0,0 +1,5 @@ + + + 67.0 + Active + diff --git a/sfdx-source/apex-common-samplecode/main/classes/selectors/IAccountsSelector.cls b/sfdx-source/apex-common-samplecode/test/classes/controllers/OpportunityInvoicingControllerTest.cls similarity index 62% rename from sfdx-source/apex-common-samplecode/main/classes/selectors/IAccountsSelector.cls rename to sfdx-source/apex-common-samplecode/test/classes/controllers/OpportunityInvoicingControllerTest.cls index abd4436..96d8086 100644 --- a/sfdx-source/apex-common-samplecode/main/classes/selectors/IAccountsSelector.cls +++ b/sfdx-source/apex-common-samplecode/test/classes/controllers/OpportunityInvoicingControllerTest.cls @@ -24,8 +24,27 @@ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. **/ -public interface IAccountsSelector extends fflib_ISObjectSelector -{ - List selectById(Set idSet); - List selectByOpportunity(List opportunities); -} \ No newline at end of file +/** + * Unit tests for OpportunityInvoicingController using fflib-apex-mocks. + */ +@IsTest +private class OpportunityInvoicingControllerTest { + @IsTest + private static void startJobCallsServiceAndReturnsJobId() { + // Given a mocked opportunities service that returns a job id + fflib_ApexMocks mocks = new fflib_ApexMocks(); + OpportunitiesService mockService = (OpportunitiesService) mocks.mock(OpportunitiesService.class); + Id jobId = TestIdGenerator.generate(AsyncApexJob.SObjectType); + mocks.startStubbing(); + mocks.when(mockService.submitInvoicingJob()).thenReturn(jobId); + mocks.stopStubbing(); + OpportunitiesService.mock = mockService; + + // When start job is invoked from the Lightning action + Id resultId = OpportunityInvoicingController.startJob(); + + // Then the service is called and the job id is returned + ((OpportunitiesService) mocks.verify(mockService)).submitInvoicingJob(); + System.assertEquals(jobId, resultId, 'Job Id should be returned'); + } +} diff --git a/sfdx-source/apex-common-samplecode/test/classes/controllers/OpportunityInvoicingControllerTest.cls-meta.xml b/sfdx-source/apex-common-samplecode/test/classes/controllers/OpportunityInvoicingControllerTest.cls-meta.xml new file mode 100644 index 0000000..b009296 --- /dev/null +++ b/sfdx-source/apex-common-samplecode/test/classes/controllers/OpportunityInvoicingControllerTest.cls-meta.xml @@ -0,0 +1,5 @@ + + + 67.0 + Active + diff --git a/sfdx-source/apex-common-samplecode/test/classes/controllers/QuickOpportunityWizardControllerTest.cls b/sfdx-source/apex-common-samplecode/test/classes/controllers/QuickOpportunityWizardControllerTest.cls new file mode 100644 index 0000000..02c2f5d --- /dev/null +++ b/sfdx-source/apex-common-samplecode/test/classes/controllers/QuickOpportunityWizardControllerTest.cls @@ -0,0 +1,83 @@ +/** + * Copyright (c) 2012-2014, FinancialForce.com, inc + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * - Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * - Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * - Neither the name of the FinancialForce.com, inc nor the names of its contributors + * may be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL + * THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY + * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +**/ + +/** + * Tests for QuickOpportunityWizardController Lightning action methods. + */ +@IsTest +private class QuickOpportunityWizardControllerTest { + @TestSetup + static void setup() { + // Given a run-as user with sample app permissions + TestDataFactory.createUserWithSampleAppPermissions(); + } + + @IsTest + private static void getWizardDataReturnsAccountNameAndNoLines() { + System.runAs(TestDataFactory.getRunAsUser()) { + // Given an account with no opportunity products + Account account = new Account(Name = 'Wizard Account'); + insert account; + + // When wizard data is loaded + Test.startTest(); + QuickOpportunityWizardController.ViewState viewState = QuickOpportunityWizardController.getWizardData( + account.Id + ); + Test.stopTest(); + + // Then the opportunity name defaults to the account name + System.assertEquals('Wizard Account', viewState.opportunityName, 'Opportunity name should default to Account name'); + System.assertEquals(0, viewState.selectLineItemList.size(), 'No recent line items should be returned'); + } + } + + @IsTest + private static void createOpportunityInsertsOpportunityWithoutLines() { + System.runAs(TestDataFactory.getRunAsUser()) { + // Given an account and wizard field values with no selected lines + Account account = new Account(Name = 'Wizard Account'); + insert account; + + // When the wizard creates an opportunity + Test.startTest(); + Id opportunityId = QuickOpportunityWizardController.createOpportunity( + account.Id, + 'New Opportunity', + 'Open', + System.today(), + new List() + ); + Test.stopTest(); + + // Then the opportunity is inserted against the account + Opportunity created = [SELECT Name, AccountId, StageName FROM Opportunity WHERE Id = :opportunityId]; + System.assertEquals('New Opportunity', created.Name, 'Opportunity name should match'); + System.assertEquals(account.Id, created.AccountId, 'Opportunity should belong to the account'); + System.assertEquals('Open', created.StageName, 'Stage should match'); + } + } +} diff --git a/sfdx-source/apex-common-samplecode/test/classes/controllers/QuickOpportunityWizardControllerTest.cls-meta.xml b/sfdx-source/apex-common-samplecode/test/classes/controllers/QuickOpportunityWizardControllerTest.cls-meta.xml new file mode 100644 index 0000000..b009296 --- /dev/null +++ b/sfdx-source/apex-common-samplecode/test/classes/controllers/QuickOpportunityWizardControllerTest.cls-meta.xml @@ -0,0 +1,5 @@ + + + 67.0 + Active + diff --git a/sfdx-source/apex-common-samplecode/test/classes/domains/AccountsTest.cls b/sfdx-source/apex-common-samplecode/test/classes/domains/AccountsTest.cls index 5f026eb..b81dbf1 100644 --- a/sfdx-source/apex-common-samplecode/test/classes/domains/AccountsTest.cls +++ b/sfdx-source/apex-common-samplecode/test/classes/domains/AccountsTest.cls @@ -24,29 +24,35 @@ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. **/ +/** + * Domain tests for Accounts. + */ @IsTest private class AccountsTest { @IsTest - static void itShouldUpdateTheOpportunityActivity() + static void updateOpportunityActivitySetsDescription() { - // Given a domain with few Account records with an empty description field - Accounts domain = new Accounts(new List{ new Account(), new Account(), new Account() }); + // Given accounts without an opportunity activity description + List accountRecords = new List{ + new Account(Name = 'A'), + new Account(Name = 'B'), + new Account(Name = 'C') + }; + Accounts domain = Accounts.newInstance(accountRecords); - // WHEN we set the description field to a value - System.Test.startTest(); + // When opportunity activity is updated on the domain domain.updateOpportunityActivity(); - System.Test.stopTest(); - // THEN the description should be updated - List result = domain.getAccounts(); - System.assertEquals(3, result.size()); - final String expectedValue = Accounts.OPPORTUNITY_RAISED_DESCRIPTION + ' ' + System.today(); - for (Account record : result) + // Then each account description records today's opportunity activity + String expectedDescription = Accounts.OPPORTUNITY_RAISED_DESCRIPTION + ' ' + System.today(); + for(Account account : accountRecords) { - System.assertEquals(expectedValue, record.Description); + System.assertEquals( + expectedDescription, + account.Description, + 'Description should include opportunity raised text' + ); } } - - -} \ No newline at end of file +} diff --git a/sfdx-source/apex-common-samplecode/test/classes/domains/AccountsTest.cls-meta.xml b/sfdx-source/apex-common-samplecode/test/classes/domains/AccountsTest.cls-meta.xml index 5f399c3..b009296 100644 --- a/sfdx-source/apex-common-samplecode/test/classes/domains/AccountsTest.cls-meta.xml +++ b/sfdx-source/apex-common-samplecode/test/classes/domains/AccountsTest.cls-meta.xml @@ -1,5 +1,5 @@ - 63.0 + 67.0 Active diff --git a/sfdx-source/apex-common-samplecode/test/classes/domains/DeveloperWorkItemsTest.cls b/sfdx-source/apex-common-samplecode/test/classes/domains/DeveloperWorkItemsTest.cls index d76f55c..d4b34f3 100644 --- a/sfdx-source/apex-common-samplecode/test/classes/domains/DeveloperWorkItemsTest.cls +++ b/sfdx-source/apex-common-samplecode/test/classes/domains/DeveloperWorkItemsTest.cls @@ -24,34 +24,45 @@ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. **/ +/** + * Integration test for DeveloperWorkItems cost calculation. + */ @IsTest private class DeveloperWorkItemsTest { @TestSetup static void setup() { + // Given a run-as user with sample app permissions TestDataFactory.createUserWithSampleAppPermissions(); } @IsTest - private static void whenInsertingDeveloperWorkItemsThenCalcCost() + static void insertWorkItemsCalculatesCost() { - // Given - data setup in system context, DML in runAs for USER_MODE - WorkOrder__c wo = new WorkOrder__c(); - DeveloperWorkItem__c wi = new DeveloperWorkItem__c(); - wi.CodingHours__c = 20; - wi.CodeReviewingHours__c = 1; - wi.TechnicalDesignHours__c = 10; - System.runAs(TestDataFactory.getRunAsUser()) { - fflib_ISObjectUnitOfWork uow = Application.UnitOfWork.newInstance(); - uow.registerNew(wo); - uow.registerNew(wi, DeveloperWorkItem__c.WorkOrder__c, wo); - uow.commitWork(); - } + // Given a work order and developer work item with hours + WorkOrder__c wo = new WorkOrder__c(); + insert wo; + DeveloperWorkItem__c wi = new DeveloperWorkItem__c( + WorkOrder__c = wo.Id, + CodingHours__c = 20, + CodeReviewingHours__c = 1, + TechnicalDesignHours__c = 10 + ); - // Then - System.assertEquals(3100, [SELECT DeveloperCost__c FROM DeveloperWorkItem__c WHERE Id = :wi.Id].DeveloperCost__c); + // When the work item is inserted + Test.startTest(); + insert wi; + Test.stopTest(); + + // Then the developer cost is calculated from hours worked + System.assertEquals( + 3100, + [SELECT DeveloperCost__c FROM DeveloperWorkItem__c WHERE Id = :wi.Id].DeveloperCost__c, + 'Developer cost should be 3100' + ); + } } -} \ No newline at end of file +} diff --git a/sfdx-source/apex-common-samplecode/test/classes/domains/DeveloperWorkItemsTest.cls-meta.xml b/sfdx-source/apex-common-samplecode/test/classes/domains/DeveloperWorkItemsTest.cls-meta.xml index 5f399c3..b009296 100644 --- a/sfdx-source/apex-common-samplecode/test/classes/domains/DeveloperWorkItemsTest.cls-meta.xml +++ b/sfdx-source/apex-common-samplecode/test/classes/domains/DeveloperWorkItemsTest.cls-meta.xml @@ -1,5 +1,5 @@ - 63.0 + 67.0 Active diff --git a/sfdx-source/apex-common-samplecode/test/classes/domains/OpportunitiesTest.cls b/sfdx-source/apex-common-samplecode/test/classes/domains/OpportunitiesTest.cls index d59223d..4d8aeb5 100644 --- a/sfdx-source/apex-common-samplecode/test/classes/domains/OpportunitiesTest.cls +++ b/sfdx-source/apex-common-samplecode/test/classes/domains/OpportunitiesTest.cls @@ -25,52 +25,42 @@ **/ /** - * This test class demonstrates two approaches to unit testing, with and without DML, each has its own pros and cons - **/ + * Domain tests for Opportunities. + */ @IsTest -private class OpportunitiesTest +private class OpportunitiesTest { - @TestSetup - static void setup() - { - TestDataFactory.createUserWithSampleAppPermissions(); - } - @IsTest - private static void callingApplyDiscountShouldCalcDiscountAndRegisterDirty() + private static void applyDiscountWithoutLinesMutatesAmount() { - // Create mocks - fflib_ApexMocks mocks = new fflib_ApexMocks(); - fflib_ISObjectUnitOfWork uowMock = (fflib_ISObjectUnitOfWork) mocks.mock(fflib_ISObjectUnitOfWork.class); - // Given + // Given an opportunity with an amount and no line items Opportunity opp = new Opportunity( - Id = fflib_IDGenerator.generate(Opportunity.SObjectType), + Id = TestIdGenerator.generate(Opportunity.SObjectType), Name = 'Test Opportunity', StageName = 'Open', Amount = 1000, - CloseDate = System.today()); - List testOppsList = new List { opp }; - Decimal discountPercent = 10; - Application.UnitOfWork.setMock(uowMock); - - // When - IOpportunities opps = - Opportunities.newInstance(testOppsList); - opps.applyDiscount(discountPercent, uowMock); + CloseDate = System.today() + ); + Opportunities opps = Opportunities.newInstance(new List{ opp }); + fflib_ApexMocks mocks = new fflib_ApexMocks(); + fflib_ISObjectUnitOfWork uowMock = (fflib_ISObjectUnitOfWork) mocks.mock( + fflib_ISObjectUnitOfWork.class + ); - // Then - ((fflib_ISObjectUnitOfWork) - mocks.verify(uowMock, 1)).registerDirty( - fflib_Match.sObjectWith( - new Map{ - Opportunity.Id => opp.Id, - Opportunity.Amount => 900} )); - } + // When a discount is applied in the domain + opps.applyDiscount(10, uowMock); + // Then the amount is reduced on the wrapped records and registered dirty + System.assertEquals(1, opps.getRecords().size(), 'Domain should still wrap one opportunity'); + System.assertEquals(900, opp.Amount, 'Amount should be discounted to 900'); + System.assertEquals(900, ((Opportunity) opps.getRecords()[0]).Amount, 'getRecords amount should be 900'); + ((fflib_ISObjectUnitOfWork) mocks.verify(uowMock)).registerDirty(opp); + } @IsTest - private static void testApplyDiscountWithoutOpportunityLines() + private static void applyDiscountNoLinesReducesAmount() { + // Given a persisted opportunity with an amount and no lines Opportunity opp = new Opportunity( Name = 'Test', Type = 'New Account', @@ -80,37 +70,40 @@ private class OpportunitiesTest ); insert opp; - System.runAs(TestDataFactory.getRunAsUser()) - { - fflib_ISObjectUnitOfWork uow = Application.UnitOfWork.newInstance(); - IOpportunities opps = Opportunities.newInstance(new List { opp }); - opps.applyDiscount(10, uow); - uow.commitWork(); - } + // When discounts are applied and updates are committed by the caller + Opportunities opps = Opportunities.newInstance(new List{ opp }); + fflib_ISObjectUnitOfWork uow = UnitOfWork.newInstance(); + opps.applyDiscount(10, uow); + uow.commitWork(); + // Then the stored amount reflects the discount Opportunity assertOpp = [SELECT Amount FROM Opportunity WHERE Id = :opp.Id]; - System.assertEquals(90, assertOpp.Amount); + System.assertEquals(90, assertOpp.Amount, 'Discount should reduce amount to 90'); } - + @IsTest - private static void testUpdateAccountOnInsertOfOpportunity() + private static void insertOppUpdatesAccountDesc() { + // Given an account and a related opportunity insert Account account = new Account(Name = 'Test', Description = null); insert account; + Opportunity opp = new Opportunity( + Name = 'Test', + Type = 'Existing Account', + StageName = 'Open', + CloseDate = System.today().addMonths(1), + AccountId = account.Id + ); - System.runAs(TestDataFactory.getRunAsUser()) - { - Opportunity opp = new Opportunity( - Name = 'Test', - Type = 'Existing Account', - StageName = 'Open', - CloseDate = System.today().addMonths(1), - AccountId = account.Id - ); - insert opp; - } + // When the opportunity is inserted + insert opp; + // Then the account description records opportunity activity Account assertAccount = [SELECT Description FROM Account WHERE Id = :account.Id]; - System.assertEquals('Last Opportunity Raised ' + System.today(), assertAccount.Description); - } -} \ No newline at end of file + System.assertEquals( + 'Last Opportunity Raised ' + System.today(), + assertAccount.Description, + 'Account description should be updated' + ); + } +} diff --git a/sfdx-source/apex-common-samplecode/test/classes/domains/OpportunitiesTest.cls-meta.xml b/sfdx-source/apex-common-samplecode/test/classes/domains/OpportunitiesTest.cls-meta.xml index 5f399c3..b009296 100644 --- a/sfdx-source/apex-common-samplecode/test/classes/domains/OpportunitiesTest.cls-meta.xml +++ b/sfdx-source/apex-common-samplecode/test/classes/domains/OpportunitiesTest.cls-meta.xml @@ -1,5 +1,5 @@ - 63.0 + 67.0 Active diff --git a/sfdx-source/apex-common-samplecode/test/classes/selectors/ProductsSelectorTest.cls b/sfdx-source/apex-common-samplecode/test/classes/selectors/ProductsSelectorTest.cls index fa859c5..aa40f8f 100644 --- a/sfdx-source/apex-common-samplecode/test/classes/selectors/ProductsSelectorTest.cls +++ b/sfdx-source/apex-common-samplecode/test/classes/selectors/ProductsSelectorTest.cls @@ -24,60 +24,73 @@ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. **/ +/** + * Integration tests for ProductsSelector field set support. + */ @IsTest -private class ProductsSelectorTest +private class ProductsSelectorTest { @TestSetup static void setup() { + // Given a run-as user with sample app permissions TestDataFactory.createUserWithSampleAppPermissions(); } @IsTest - private static void testSelectById() + private static void selectByIdReturnsExpectedFields() { - Product2 product = new Product2( - Description = 'Something cool', - Name = 'CoolItem', - IsActive = true, - SubscriberField__c = 'My Text Field' - ); - insert product; - System.runAs(TestDataFactory.getRunAsUser()) { - List products = - new ProductsSelector(true).selectById(new Set { product.Id }); + // Given an active product with subscriber field data + Product2 product = new Product2( + Description = 'Something cool', + Name = 'CoolItem', + IsActive = true, + SubscriberField__c = 'My Text Field' + ); + insert product; + + // When the product is selected by id + Test.startTest(); + List products = ProductsSelector.newInstance(true).selectById(new Set{ product.Id }); + Test.stopTest(); - System.assertEquals('Something cool', products[0].Description); - System.assertEquals('CoolItem', products[0].Name); - System.assertEquals(true, products[0].IsActive); - System.assertEquals('My Text Field', products[0].SubscriberField__c); + // Then expected product fields are returned + System.assertEquals('Something cool', products[0].Description, 'Description should match'); + System.assertEquals('CoolItem', products[0].Name, 'Name should match'); + System.assertEquals(true, products[0].IsActive, 'IsActive should be true'); + System.assertEquals('My Text Field', products[0].SubscriberField__c, 'SubscriberField should match'); } } @IsTest - private static void testQueryLocatorById() + private static void queryLocatorByIdReturnsExpectedFields() { - Product2 product = new Product2( - Description = 'Something cool', - Name = 'CoolItem', - IsActive = true, - SubscriberField__c = 'My Text Field' - ); - insert product; - System.runAs(TestDataFactory.getRunAsUser()) { - Database.QueryLocator queryLocator = - new ProductsSelector(true).queryLocatorById(new Set { product.Id }); + // Given an active product with subscriber field data + Product2 product = new Product2( + Description = 'Something cool', + Name = 'CoolItem', + IsActive = true, + SubscriberField__c = 'My Text Field' + ); + insert product; + + // When products are queried through a query locator + Test.startTest(); + Database.QueryLocator queryLocator = ProductsSelector.newInstance(true) + .queryLocatorById(new Set{ product.Id }); Database.QueryLocatorIterator productsIterator = queryLocator.iterator(); Product2 queriedProduct = (Product2) productsIterator.next(); + Test.stopTest(); - System.assertEquals('Something cool', queriedProduct.Description); - System.assertEquals('CoolItem', queriedProduct.Name); - System.assertEquals(true, queriedProduct.IsActive); - System.assertEquals('My Text Field', queriedProduct.SubscriberField__c); + // Then expected product fields are returned + System.assertEquals('Something cool', queriedProduct.Description, 'Description should match'); + System.assertEquals('CoolItem', queriedProduct.Name, 'Name should match'); + System.assertEquals(true, queriedProduct.IsActive, 'IsActive should be true'); + System.assertEquals('My Text Field', queriedProduct.SubscriberField__c, 'SubscriberField should match'); } } -} \ No newline at end of file +} diff --git a/sfdx-source/apex-common-samplecode/test/classes/selectors/ProductsSelectorTest.cls-meta.xml b/sfdx-source/apex-common-samplecode/test/classes/selectors/ProductsSelectorTest.cls-meta.xml index 5f399c3..b009296 100644 --- a/sfdx-source/apex-common-samplecode/test/classes/selectors/ProductsSelectorTest.cls-meta.xml +++ b/sfdx-source/apex-common-samplecode/test/classes/selectors/ProductsSelectorTest.cls-meta.xml @@ -1,5 +1,5 @@ - 63.0 + 67.0 Active diff --git a/sfdx-source/apex-common-samplecode/test/classes/service/AccountsServiceTest.cls b/sfdx-source/apex-common-samplecode/test/classes/service/AccountsServiceTest.cls index 5fbeec8..88a78a4 100644 --- a/sfdx-source/apex-common-samplecode/test/classes/service/AccountsServiceTest.cls +++ b/sfdx-source/apex-common-samplecode/test/classes/service/AccountsServiceTest.cls @@ -2,70 +2,71 @@ * Copyright (c) 2012-2014, FinancialForce.com, inc * All rights reserved. * - * Redistribution and use in source and binary forms, with or without modification, + * Redistribution and use in source and binary forms, with or without modification, * are permitted provided that the following conditions are met: * - * - Redistributions of source code must retain the above copyright notice, + * - Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. - * - Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation + * - Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation * and/or other materials provided with the distribution. - * - Neither the name of the FinancialForce.com, inc nor the names of its contributors - * may be used to endorse or promote products derived from this software without + * - Neither the name of the FinancialForce.com, inc nor the names of its contributors + * may be used to endorse or promote products derived from this software without * specific prior written permission. * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND - * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES - * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL - * THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL + * THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. **/ +/** + * Unit tests for AccountsService using fflib-apex-mocks. No DML. + */ @IsTest private class AccountsServiceTest { + @TestSetup + static void setup() + { + TestDataFactory.createUserWithSampleAppPermissions(); + } + @IsTest static void itShouldUpdateOpportunityActivity() { - // GIVEN a list of account records - List accounts = new List + System.runAs(TestDataFactory.getRunAsUser()) { - new Account(Name = 'A', Id = fflib_IDGenerator.generate(Schema.Account.SObjectType)), - new Account(Name = 'B', Id = fflib_IDGenerator.generate(Schema.Account.SObjectType)), - new Account(Name = 'C', Id = fflib_IDGenerator.generate(Schema.Account.SObjectType)) - }; - - Set accountIds = new Map(accounts).keySet(); + List accounts = new List + { + new Account(Name = 'A', Id = TestIdGenerator.generate(Schema.Account.SObjectType)), + new Account(Name = 'B', Id = TestIdGenerator.generate(Schema.Account.SObjectType)), + new Account(Name = 'C', Id = TestIdGenerator.generate(Schema.Account.SObjectType)) + }; + Set accountIds = new Map(accounts).keySet(); - fflib_ApexMocks mocks = new fflib_ApexMocks(); - fflib_ISObjectUnitOfWork unitOfWorkMock = (fflib_ISObjectUnitOfWork) mocks.mock(fflib_ISObjectUnitOfWork.class); - IAccountsSelector selectorMock = (IAccountsSelector) mocks.mock(IAccountsSelector.class); - IAccounts domainMock = (IAccounts) mocks.mock(Accounts.class); + fflib_ApexMocks mocks = new fflib_ApexMocks(); + fflib_ISObjectUnitOfWork unitOfWorkMock = (fflib_ISObjectUnitOfWork) mocks.mock(fflib_ISObjectUnitOfWork.class); + AccountsSelector selectorMock = (AccountsSelector) mocks.mock(AccountsSelector.class); - mocks.startStubbing(); - mocks.when(selectorMock.sObjectType()).thenReturn(Schema.Account.SObjectType); - mocks.when(selectorMock.selectSObjectsById(accountIds)).thenReturn(accounts); - mocks.when(domainMock.getType()).thenReturn(Schema.Account.SObjectType); - mocks.when(domainMock.getRecords()).thenReturn(accounts); - mocks.stopStubbing(); + mocks.startStubbing(); + mocks.when(selectorMock.selectById(accountIds)).thenReturn(accounts); + mocks.stopStubbing(); - Application.UnitOfWork.setMock(unitOfWorkMock); - Application.Domain.setMock(domainMock); - Application.Selector.setMock(selectorMock); + UnitOfWork.mock = unitOfWorkMock; + AccountsSelector.mock = selectorMock; - // WHEN we update the opportunity Activity - System.Test.startTest(); - new AccountsServiceImpl().updateOpportunityActivity(accountIds); - System.Test.stopTest(); + Test.startTest(); + AccountsService.newInstance().updateOpportunityActivity(accountIds); + Test.stopTest(); - // THEN the accounts should be queried from the selector - // and the domain should be invoked to update the opportunity activity - ((IAccountsSelector) mocks.verify(selectorMock)).selectSObjectsById(accountIds); - ((IAccounts) mocks.verify(domainMock)).updateOpportunityActivity(); - ((fflib_ISObjectUnitOfWork) mocks.verify(unitOfWorkMock)).registerDirty(accounts); - ((fflib_ISObjectUnitOfWork) mocks.verify(unitOfWorkMock)).commitWork(); + ((AccountsSelector) mocks.verify(selectorMock)).selectById(accountIds); + ((fflib_ISObjectUnitOfWork) mocks.verify(unitOfWorkMock)).registerDirty(accounts); + ((fflib_ISObjectUnitOfWork) mocks.verify(unitOfWorkMock)).commitWork(); + } } } diff --git a/sfdx-source/apex-common-samplecode/test/classes/service/AccountsServiceTest.cls-meta.xml b/sfdx-source/apex-common-samplecode/test/classes/service/AccountsServiceTest.cls-meta.xml index 5f399c3..b009296 100644 --- a/sfdx-source/apex-common-samplecode/test/classes/service/AccountsServiceTest.cls-meta.xml +++ b/sfdx-source/apex-common-samplecode/test/classes/service/AccountsServiceTest.cls-meta.xml @@ -1,5 +1,5 @@ - 63.0 + 67.0 Active diff --git a/sfdx-source/apex-common-samplecode/test/classes/service/InvoicingServiceTest.cls b/sfdx-source/apex-common-samplecode/test/classes/service/InvoicingServiceTest.cls index a7c15d3..9b2e8d3 100644 --- a/sfdx-source/apex-common-samplecode/test/classes/service/InvoicingServiceTest.cls +++ b/sfdx-source/apex-common-samplecode/test/classes/service/InvoicingServiceTest.cls @@ -24,49 +24,170 @@ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. **/ +/** + * Integration tests for InvoicingService.generate. + */ @IsTest -private class InvoicingServiceTest +private class InvoicingServiceTest { @TestSetup static void setup() { + // Given a run-as user with sample app permissions TestDataFactory.createUserWithSampleAppPermissions(); } @IsTest - private static void testService() + private static void generateReturnsInvoiceIds() { - // Given: create data in system context (admin runs setup; PricebookEntry insert works) - fflib_ISObjectUnitOfWork uow = Application.UnitOfWork.newInstance(); - Opportunity opp = new Opportunity(); - opp.Name = 'Opportunity'; - opp.StageName = 'Open'; - opp.CloseDate = System.today(); - uow.registerNew(opp); - for (Integer i = 0; i < 5; i++) + // Given an opportunity with products and line items + Opportunity opp = new Opportunity( + Name = 'Opportunity', + StageName = 'Open', + CloseDate = System.today(), + Pricebook2Id = Test.getStandardPricebookId() + ); + insert opp; + + List products = new List(); + for(Integer i = 0; i < 5; i++) + { + products.add(new Product2(Name = opp.Name + ' : Product : ' + i, IsActive = true)); + } + insert products; + + List pricebookEntries = new List(); + for(Product2 product : products) { - Product2 product = new Product2(); - product.Name = opp.Name + ' : Product : ' + i; - uow.registerNew(product); - PricebookEntry pbe = new PricebookEntry(); - pbe.UnitPrice = 10; - pbe.IsActive = true; - pbe.UseStandardPrice = false; - pbe.Pricebook2Id = Test.getStandardPricebookId(); - uow.registerNew(pbe, PricebookEntry.Product2Id, product); - OpportunityLineItem oppLineItem = new OpportunityLineItem(); - oppLineItem.Quantity = 1; - oppLineItem.TotalPrice = 10; - uow.registerRelationship(oppLineItem, OpportunityLineItem.PricebookEntryId, pbe); - uow.registerNew(oppLineItem, OpportunityLineItem.OpportunityId, opp); + pricebookEntries.add( + new PricebookEntry( + Product2Id = product.Id, + UnitPrice = 10, + IsActive = true, + UseStandardPrice = false, + Pricebook2Id = Test.getStandardPricebookId() + ) + ); } - uow.commitWork(); + insert pricebookEntries; + + List lineItems = new List(); + for(PricebookEntry pbe : pricebookEntries) + { + lineItems.add( + new OpportunityLineItem( + OpportunityId = opp.Id, + PricebookEntryId = pbe.Id, + Quantity = 1, + TotalPrice = 10 + ) + ); + } + insert lineItems; - // When: run service as permission-set user (USER_MODE selectors/DML) System.runAs(TestDataFactory.getRunAsUser()) { - List invoiceIds = InvoicingService.generate(new List { opp.Id }); + // When invoices are generated for the opportunity + Test.startTest(); + List invoiceIds = InvoicingService.newInstance().generate(new List{ opp.Id }); + Test.stopTest(); + + // Then one invoice id is returned System.assertEquals(1, invoiceIds.size(), 'Should generate one invoice'); } } -} \ No newline at end of file + + @IsTest + private static void generateFromDeveloperWorkItemReturnsInvoiceId() + { + System.runAs(TestDataFactory.getRunAsUser()) + { + // Given a developer work item under a work order and account + Account acc = new Account(Name = 'Client'); + insert acc; + WorkOrder__c wo = new WorkOrder__c(Account__c = acc.Id); + insert wo; + DeveloperWorkItem__c wi = new DeveloperWorkItem__c( + WorkOrder__c = wo.Id, + CodingHours__c = 10, + CodeReviewingHours__c = 2, + TechnicalDesignHours__c = 3 + ); + insert wi; + + // When invoices are generated from the work item + Test.startTest(); + List invoiceIds = InvoicingService.newInstance().generate(new List{ wi.Id }); + Test.stopTest(); + + // Then one invoice is created for the work order account + System.assertEquals(1, invoiceIds.size(), 'Should generate one invoice from DeveloperWorkItem'); + Invoice__c inv = [SELECT Account__c, Description__c FROM Invoice__c WHERE Id = :invoiceIds[0]]; + System.assertEquals(acc.Id, inv.Account__c, 'Invoice should be for work order account'); + System.assert( + inv.Description__c!=null && inv.Description__c.contains('Developer'), + 'Description should mention developer work' + ); + } + } + + @IsTest + private static void generateFromTrainingWorkItemReturnsInvoiceId() + { + System.runAs(TestDataFactory.getRunAsUser()) + { + // Given a training work item under a work order and account + Account acc = new Account(Name = 'Client'); + insert acc; + WorkOrder__c wo = new WorkOrder__c(Account__c = acc.Id); + insert wo; + TrainingWorkItem__c wi = new TrainingWorkItem__c( + WorkOrder__c = wo.Id, + DeliveryHours__c = 8, + PreparationHours__c = 2 + ); + insert wi; + + // When invoices are generated from the work item + Test.startTest(); + List invoiceIds = InvoicingService.newInstance().generate(new List{ wi.Id }); + Test.stopTest(); + + // Then one invoice is created for the work order account + System.assertEquals(1, invoiceIds.size(), 'Should generate one invoice from TrainingWorkItem'); + Invoice__c inv = [SELECT Account__c, Description__c FROM Invoice__c WHERE Id = :invoiceIds[0]]; + System.assertEquals(acc.Id, inv.Account__c, 'Invoice should be for work order account'); + System.assert( + inv.Description__c!=null && inv.Description__c.contains('Training'), + 'Description should mention training' + ); + } + } + + @IsTest + private static void generateWithUnsupportedSourceTypeThrows() + { + // Given an account id that is not a supported invoicing source + Account acc = new Account(Name = 'Test'); + insert acc; + + System.runAs(TestDataFactory.getRunAsUser()) + { + // When invoice generation is attempted for that account + Test.startTest(); + Boolean threw = false; + try + { + InvoicingService.newInstance().generate(new List{ acc.Id }); + } + catch (Exception e) + { + threw = true; + // Then an exception is raised for the unsupported source type + System.assert(true, 'Exception thrown as expected: ' + e.getTypeName()); + } + Test.stopTest(); + System.assert(threw, 'Expected exception for unsupported source type'); + } + } +} diff --git a/sfdx-source/apex-common-samplecode/test/classes/service/InvoicingServiceTest.cls-meta.xml b/sfdx-source/apex-common-samplecode/test/classes/service/InvoicingServiceTest.cls-meta.xml index 5f399c3..b009296 100644 --- a/sfdx-source/apex-common-samplecode/test/classes/service/InvoicingServiceTest.cls-meta.xml +++ b/sfdx-source/apex-common-samplecode/test/classes/service/InvoicingServiceTest.cls-meta.xml @@ -1,5 +1,5 @@ - 63.0 + 67.0 Active diff --git a/sfdx-source/apex-common-samplecode/test/classes/service/OpportunitiesServiceIntegrationTest.cls b/sfdx-source/apex-common-samplecode/test/classes/service/OpportunitiesServiceIntegrationTest.cls index 1ebec2b..fd8501a 100644 --- a/sfdx-source/apex-common-samplecode/test/classes/service/OpportunitiesServiceIntegrationTest.cls +++ b/sfdx-source/apex-common-samplecode/test/classes/service/OpportunitiesServiceIntegrationTest.cls @@ -24,91 +24,76 @@ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. **/ +/** + * Integration test for OpportunitiesService.applyDiscounts. + */ @IsTest -private class OpportunitiesServiceIntegrationTest +private class OpportunitiesServiceIntegrationTest { @TestSetup static void setup() { + // Given a run-as user with sample app permissions TestDataFactory.createUserWithSampleAppPermissions(); } @IsTest - private static void testServiceClass() + private static void applyDiscountsAsRunAsUserReducesAmount() { - // Create Opportunity, Lines, Pricebooks and Products (system context for inserts) - List opps = new List(); - List> productsByOpp = new List>(); - List> pricebookEntriesByOpp = new List>(); - List> oppLinesByOpp = new List>(); - Opportunity opp = new Opportunity(); - opp.Name = 'Test'; - opp.StageName = 'Open'; - opp.CloseDate = System.today(); - opps.add(opp); + // Given an opportunity with priced line items + Opportunity opp = new Opportunity( + Name = 'Test', + StageName = 'Open', + CloseDate = System.today(), + Pricebook2Id = Test.getStandardPricebookId() + ); + insert opp; + List products = new List(); + for(Integer i = 0; i < 10; i++) + { + products.add(new Product2(Name = opp.Name + ' : Product : ' + i, IsActive = true)); + } + insert products; + List pricebookEntries = new List(); + for(Product2 product : products) + { + pricebookEntries.add( + new PricebookEntry( + Product2Id = product.Id, + UnitPrice = 10, + IsActive = true, + UseStandardPrice = false, + Pricebook2Id = Test.getStandardPricebookId() + ) + ); + } + insert pricebookEntries; + List oppLineItems = new List(); - for(Integer i=0; i<10; i++) { - Product2 product = new Product2(); - product.Name = opp.Name + ' : Product : ' + i; - products.add(product); - PricebookEntry pbe = new PricebookEntry(); - pbe.UnitPrice = 10; - pbe.IsActive = true; - pbe.UseStandardPrice = false; - pbe.Pricebook2Id = Test.getStandardPricebookId(); - pricebookEntries.add(pbe); - OpportunityLineItem oppLineItem = new OpportunityLineItem(); - oppLineItem.Quantity = 1; - oppLineItem.TotalPrice = 10; - oppLineItems.add(oppLineItem); + for(PricebookEntry pbe : pricebookEntries) + { + oppLineItems.add( + new OpportunityLineItem( + OpportunityId = opp.Id, + PricebookEntryId = pbe.Id, + Quantity = 1, + TotalPrice = 10 + ) + ); } - productsByOpp.add(products); - pricebookEntriesByOpp.add(pricebookEntries); - oppLinesByOpp.add(oppLineItems); - // Insert Opportunities - insert opps; - // Insert Products - List allProducts = new List(); - for(List oppProducts : productsByOpp) - allProducts.addAll(oppProducts); - insert allProducts; - // Insert Pricebooks - Integer oppIdx = 0; - List allPricebookEntries = new List(); - for(List oppPricebookEntries : pricebookEntriesByOpp) { - List pbproducts = productsByOpp[oppIdx++]; - Integer lineIdx = 0; - for(PricebookEntry pricebookEntry : oppPricebookEntries) - pricebookEntry.Product2Id = pbproducts[lineIdx++].Id; - allPricebookEntries.addAll(oppPricebookEntries); - } - insert allPricebookEntries; - // Insert Opportunity Lines - oppIdx = 0; - List allOppLineItems = new List(); - for(List oppLines : oppLinesByOpp) { - List oppPricebookEntries = pricebookEntriesByOpp[oppIdx]; - Integer lineIdx = 0; - for(OpportunityLineItem oppLine : oppLines) { - oppLine.OpportunityId = opps[oppIdx].Id; - oppLine.PricebookEntryId = oppPricebookEntries[lineIdx++].Id; - } - allOppLineItems.addAll(oppLines); - oppIdx++; - } - insert allOppLineItems; + insert oppLineItems; - // Test - applyDiscounts uses USER_MODE selectors/DML, must run as user with permission set System.runAs(TestDataFactory.getRunAsUser()) { + // When discounts are applied for the opportunity Test.startTest(); - OpportunitiesService.applyDiscounts(new Set { opp.Id }, 10); + OpportunitiesService.newInstance().applyDiscounts(new Set{ opp.Id }, 10); Test.stopTest(); } - // Assert - System.assertEquals(90, [SELECT Amount FROM Opportunity LIMIT 1][0].Amount); - } -} \ No newline at end of file + // Then the opportunity amount reflects the discount + System.assertEquals(90, [SELECT Amount FROM Opportunity WHERE Id = :opp.Id].Amount); + } +} diff --git a/sfdx-source/apex-common-samplecode/test/classes/service/OpportunitiesServiceIntegrationTest.cls-meta.xml b/sfdx-source/apex-common-samplecode/test/classes/service/OpportunitiesServiceIntegrationTest.cls-meta.xml index 5f399c3..b009296 100644 --- a/sfdx-source/apex-common-samplecode/test/classes/service/OpportunitiesServiceIntegrationTest.cls-meta.xml +++ b/sfdx-source/apex-common-samplecode/test/classes/service/OpportunitiesServiceIntegrationTest.cls-meta.xml @@ -1,5 +1,5 @@ - 63.0 + 67.0 Active diff --git a/sfdx-source/apex-common-samplecode/test/classes/service/OpportunitiesServiceTest.cls b/sfdx-source/apex-common-samplecode/test/classes/service/OpportunitiesServiceTest.cls index 00bf57f..82d66d9 100644 --- a/sfdx-source/apex-common-samplecode/test/classes/service/OpportunitiesServiceTest.cls +++ b/sfdx-source/apex-common-samplecode/test/classes/service/OpportunitiesServiceTest.cls @@ -24,47 +24,47 @@ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. **/ +/** + * Unit tests for OpportunitiesService using fflib-apex-mocks. No DML. + */ @IsTest -private class OpportunitiesServiceTest +private class OpportunitiesServiceTest { @IsTest - private static void callingServiceShouldCallSelectorApplyDiscountInDomainAndCommit() + private static void serviceApplyDiscountsCallsSelectorDomainAndCommit() { - // Create mocks + // Given an opportunity with amount and no lines, returned by a mocked selector fflib_ApexMocks mocks = new fflib_ApexMocks(); - - fflib_ISObjectUnitOfWork uowMock = (fflib_ISObjectUnitOfWork) mocks.mock(fflib_ISObjectUnitOfWork.class); - IOpportunities domainMock = (IOpportunities) mocks.mock(IOpportunities.class); - IOpportunitiesSelector selectorMock = (IOpportunitiesSelector) mocks.mock(IOpportunitiesSelector.class); - - - // Given - mocks.startStubbing(); - List testOppsList = new List { + fflib_ISObjectUnitOfWork uowMock = (fflib_ISObjectUnitOfWork) mocks.mock( + fflib_ISObjectUnitOfWork.class + ); + Opportunities domainMock = (Opportunities) mocks.mock(Opportunities.class); + OpportunitiesSelector selectorMock = (OpportunitiesSelector) mocks.mock( + OpportunitiesSelector.class + ); + List testOppsList = new List{ new Opportunity( - Id = fflib_IDGenerator.generate(Opportunity.SObjectType), + Id = TestIdGenerator.generate(Opportunity.SObjectType), Name = 'Test Opportunity', StageName = 'Open', Amount = 1000, - CloseDate = System.today()) }; + CloseDate = System.today() + ) + }; Set testOppsSet = new Map(testOppsList).keySet(); - mocks.when(domainMock.getType()).thenReturn(Opportunity.SObjectType); - mocks.when(selectorMock.sObjectType()).thenReturn(Opportunity.SObjectType); + mocks.startStubbing(); mocks.when(selectorMock.selectByIdWithProducts(testOppsSet)).thenReturn(testOppsList); mocks.stopStubbing(); - Application.UnitOfWork.setMock(uowMock); - Application.Domain.setMock(domainMock); - Application.Selector.setMock(selectorMock); + UnitOfWork.mock = uowMock; + OpportunitiesSelector.mock = selectorMock; + Opportunities.mock = domainMock; - // When - OpportunitiesService.applyDiscounts(testOppsSet, 10); + // When discounts are applied through the service + OpportunitiesService.newInstance().applyDiscounts(testOppsSet, 10); - // Then - ((IOpportunitiesSelector) - mocks.verify(selectorMock)).selectByIdWithProducts(testOppsSet); - ((IOpportunities) - mocks.verify(domainMock)).applyDiscount(10, uowMock); - ((fflib_ISObjectUnitOfWork) - mocks.verify(uowMock, 1)).commitWork(); + // Then the selector, domain, and unit of work are used + ((OpportunitiesSelector) mocks.verify(selectorMock)).selectByIdWithProducts(testOppsSet); + ((Opportunities) mocks.verify(domainMock)).applyDiscount(10, uowMock); + ((fflib_ISObjectUnitOfWork) mocks.verify(uowMock, 1)).commitWork(); } -} \ No newline at end of file +} diff --git a/sfdx-source/apex-common-samplecode/test/classes/service/OpportunitiesServiceTest.cls-meta.xml b/sfdx-source/apex-common-samplecode/test/classes/service/OpportunitiesServiceTest.cls-meta.xml index 5f399c3..b009296 100644 --- a/sfdx-source/apex-common-samplecode/test/classes/service/OpportunitiesServiceTest.cls-meta.xml +++ b/sfdx-source/apex-common-samplecode/test/classes/service/OpportunitiesServiceTest.cls-meta.xml @@ -1,5 +1,5 @@ - 63.0 + 67.0 Active diff --git a/sfdx-source/apex-common-samplecode/test/classes/triggerHandlers/OpportunitiesTriggerHandlerTest.cls b/sfdx-source/apex-common-samplecode/test/classes/triggerHandlers/OpportunitiesTriggerHandlerTest.cls index 2fe385a..7c0a9ac 100644 --- a/sfdx-source/apex-common-samplecode/test/classes/triggerHandlers/OpportunitiesTriggerHandlerTest.cls +++ b/sfdx-source/apex-common-samplecode/test/classes/triggerHandlers/OpportunitiesTriggerHandlerTest.cls @@ -25,52 +25,109 @@ **/ /** - * This test class demonstrates two approaches to unit testing, with and without DML, each has its own pros and cons - **/ + * Tests for OpportunitiesTriggerHandler without fflib test harness. + */ @IsTest -private class OpportunitiesTriggerHandlerTest +private class OpportunitiesTriggerHandlerTest { - - @IsTest - private static void testValidationWithoutDML() + @TestSetup + static void setup() { - OpportunitiesTriggerHandler opps = new OpportunitiesTriggerHandler(new Opportunity[] { new Opportunity ( Name = 'Test', Type = 'Existing Account' ) } ); - opps.onValidate(); - System.assertEquals(1, fflib_SObjectDomain.Errors.getAll().size()); - System.assertEquals('You must provide an Account for OpportunityTriggerHandler for existing Customers.', fflib_SObjectDomain.Errors.getAll()[0].message); - System.assertEquals(Opportunity.AccountId, ((fflib_SObjectDomain.FieldError)fflib_SObjectDomain.Errors.getAll()[0]).field); + // Given a run-as user with sample app permissions + TestDataFactory.createUserWithSampleAppPermissions(); } - + @IsTest - private static void testInsertValidationFailedWithoutDML() + private static void insertExistingAccountNoAccountIdFails() { - Opportunity opp = new Opportunity ( Name = 'Test', Type = 'Existing Account' ); - fflib_SObjectDomain.Test.Database.onInsert(new Opportunity[] { opp } ); - fflib_SObjectDomain.triggerHandler(OpportunitiesTriggerHandler.class); - System.assertEquals(1, fflib_SObjectDomain.Errors.getAll().size()); - System.assertEquals('You must provide an Account for OpportunityTriggerHandler for existing Customers.', fflib_SObjectDomain.Errors.getAll()[0].message); - System.assertEquals(Opportunity.AccountId, ((fflib_SObjectDomain.FieldError)fflib_SObjectDomain.Errors.getAll()[0]).field); + System.runAs(TestDataFactory.getRunAsUser()) + { + // Given an existing-customer opportunity without an account + Opportunity opp = new Opportunity( + Name = 'Test', + Type = 'Existing Account', + StageName = 'Open', + CloseDate = System.today() + ); + + // When the opportunity is inserted + Test.startTest(); + Boolean denied = false; + try + { + insert opp; + } + catch (DmlException e) + { + denied = true; + // Then insert fails with the account validation message + System.assert( + e.getMessage().contains( + 'You must provide an Account for OpportunityTriggerHandler for existing Customers.' + ), + 'DML error should include validation message' + ); + } + Test.stopTest(); + System.assert(denied, 'Expected DmlException for missing AccountId'); + } } @IsTest - private static void testUpdateValidationFailedWithoutDML() + private static void updateTypeChangeFails() { - Opportunity oldOpp = new Opportunity ( Name = 'Test', Type = 'Existing Account' ); - Opportunity newOpp = new Opportunity ( Name = 'Test', Type = 'New Account' ); - fflib_SObjectDomain.Test.Database.onUpdate(new Opportunity[] { newOpp }, new Map { newOpp.Id => oldOpp } ); - fflib_SObjectDomain.triggerHandler(OpportunitiesTriggerHandler.class); - System.assertEquals(1, fflib_SObjectDomain.Errors.getAll().size()); - System.assertEquals('You cannot change the Opportunity type once it has been created.', fflib_SObjectDomain.Errors.getAll()[0].message); - System.assertEquals(Opportunity.Type, ((fflib_SObjectDomain.FieldError)fflib_SObjectDomain.Errors.getAll()[0]).field); + System.runAs(TestDataFactory.getRunAsUser()) + { + // Given a persisted opportunity whose type must not change + Opportunity opp = new Opportunity( + Name = 'Test', + Type = 'New Account', + StageName = 'Open', + CloseDate = System.today() + ); + insert opp; + opp.Type = 'Existing Account'; + + // When the opportunity type is updated + Test.startTest(); + Boolean denied = false; + try + { + update opp; + } + catch (DmlException e) + { + denied = true; + // Then the update fails with the type-change validation message + System.assert( + e.getMessage().contains('You cannot change the Opportunity type once it has been created.'), + 'DML error should include type-change validation message' + ); + } + Test.stopTest(); + System.assert(denied, 'Expected DmlException when changing Type'); + } } - + @IsTest - private static void testInsertValidationSuccessWithoutDML() + private static void insertValidOpportunityPasses() { - Opportunity opp = new Opportunity ( Name = 'Test' ); - fflib_SObjectDomain.Test.Database.onInsert(new Opportunity[] { opp } ); - fflib_SObjectDomain.triggerHandler(OpportunitiesTriggerHandler.class); - System.assertEquals(fflib_SObjectDomain.Errors.getAll().size(), 0); - } + System.runAs(TestDataFactory.getRunAsUser()) + { + // Given a valid new opportunity + Opportunity opp = new Opportunity( + Name = 'Test', + StageName = 'Open', + CloseDate = System.today() + ); -} \ No newline at end of file + // When the opportunity is inserted + Test.startTest(); + insert opp; + Test.stopTest(); + + // Then insert succeeds + System.assertNotEquals(null, opp.Id, 'Valid opportunity should insert'); + } + } +} diff --git a/sfdx-source/apex-common-samplecode/test/classes/triggerHandlers/OpportunitiesTriggerHandlerTest.cls-meta.xml b/sfdx-source/apex-common-samplecode/test/classes/triggerHandlers/OpportunitiesTriggerHandlerTest.cls-meta.xml index 5f399c3..b009296 100644 --- a/sfdx-source/apex-common-samplecode/test/classes/triggerHandlers/OpportunitiesTriggerHandlerTest.cls-meta.xml +++ b/sfdx-source/apex-common-samplecode/test/classes/triggerHandlers/OpportunitiesTriggerHandlerTest.cls-meta.xml @@ -1,5 +1,5 @@ - 63.0 + 67.0 Active From 330022d8090e65a398ba3303e46501a37539cbd4 Mon Sep 17 00:00:00 2001 From: Andrew Fawcett Date: Tue, 25 Aug 2026 11:36:41 +0100 Subject: [PATCH 02/12] Link Application class blog post in README architecture notes. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index af54577..c4440a6 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,7 @@ This repository contains a sample application illustrating the Apex Enterprise P Architecture Notes ------------------ -This sample uses **concrete** Domain, Selector, and Service classes with static `newInstance()` factories and `@TestVisible` mock hooks for unit tests. There is no `Application.cls` dependency-injection factory. +This sample uses **concrete** Domain, Selector, and Service classes with static `newInstance()` factories and `@TestVisible` mock hooks for unit tests. There is no `Application.cls` dependency-injection factory — see [Apex Enterprise Patterns: Recent Updates and Thoughts on the Application Class](https://andyinthecloud.com/2026/04/13/apex-enterprise-patterns-recent-updates-and-thoughts-on-the-application-class/) for background on this approach. | Component | Role | |-----------|------| From f6b47ebc05089e7f3a975e8f33bada682cb6bb09 Mon Sep 17 00:00:00 2001 From: Andrew Fawcett Date: Tue, 25 Aug 2026 19:29:34 +0100 Subject: [PATCH 03/12] Exclude LWC Jest tests from deploy via .forceignore. CI deploy fails when __tests__ bundles reference @lwc/engine-dom, which is not valid org metadata. --- .forceignore | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 .forceignore diff --git a/.forceignore b/.forceignore new file mode 100644 index 0000000..ab98ecf --- /dev/null +++ b/.forceignore @@ -0,0 +1,3 @@ +**/jsconfig.json +**/.eslintrc.json +**/__tests__/** \ No newline at end of file From 91d9fc7b9b899a45ca3490db53d6e3fa7946eea8 Mon Sep 17 00:00:00 2001 From: Andrew Fawcett Date: Sun, 6 Sep 2026 10:57:03 +0100 Subject: [PATCH 04/12] Return concrete fflib_SObjectUnitOfWork from UnitOfWork.newInstance(). Aligns the sample factory with the concrete-class approach discussed in PR review. --- .../apex-common-samplecode/main/classes/UnitOfWork.cls | 4 ++-- .../test/classes/service/AccountsServiceTest.cls | 6 +++--- .../test/classes/service/OpportunitiesServiceTest.cls | 6 +++--- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/sfdx-source/apex-common-samplecode/main/classes/UnitOfWork.cls b/sfdx-source/apex-common-samplecode/main/classes/UnitOfWork.cls index 5a92f79..299c91e 100644 --- a/sfdx-source/apex-common-samplecode/main/classes/UnitOfWork.cls +++ b/sfdx-source/apex-common-samplecode/main/classes/UnitOfWork.cls @@ -29,14 +29,14 @@ */ public inherited sharing class UnitOfWork { @TestVisible - private static fflib_ISObjectUnitOfWork mock; + private static fflib_SObjectUnitOfWork mock; /** * Returns a Unit of Work registered for this app's types, or {@link #mock} when tests have set one. * DML runs in user mode. * @return Unit of Work instance. */ - public static fflib_ISObjectUnitOfWork newInstance() { + public static fflib_SObjectUnitOfWork newInstance() { if (mock != null) { return mock; } diff --git a/sfdx-source/apex-common-samplecode/test/classes/service/AccountsServiceTest.cls b/sfdx-source/apex-common-samplecode/test/classes/service/AccountsServiceTest.cls index 88a78a4..b4bd2ea 100644 --- a/sfdx-source/apex-common-samplecode/test/classes/service/AccountsServiceTest.cls +++ b/sfdx-source/apex-common-samplecode/test/classes/service/AccountsServiceTest.cls @@ -50,7 +50,7 @@ private class AccountsServiceTest Set accountIds = new Map(accounts).keySet(); fflib_ApexMocks mocks = new fflib_ApexMocks(); - fflib_ISObjectUnitOfWork unitOfWorkMock = (fflib_ISObjectUnitOfWork) mocks.mock(fflib_ISObjectUnitOfWork.class); + fflib_SObjectUnitOfWork unitOfWorkMock = (fflib_SObjectUnitOfWork) mocks.mock(fflib_SObjectUnitOfWork.class); AccountsSelector selectorMock = (AccountsSelector) mocks.mock(AccountsSelector.class); mocks.startStubbing(); @@ -65,8 +65,8 @@ private class AccountsServiceTest Test.stopTest(); ((AccountsSelector) mocks.verify(selectorMock)).selectById(accountIds); - ((fflib_ISObjectUnitOfWork) mocks.verify(unitOfWorkMock)).registerDirty(accounts); - ((fflib_ISObjectUnitOfWork) mocks.verify(unitOfWorkMock)).commitWork(); + ((fflib_SObjectUnitOfWork) mocks.verify(unitOfWorkMock)).registerDirty(accounts); + ((fflib_SObjectUnitOfWork) mocks.verify(unitOfWorkMock)).commitWork(); } } } diff --git a/sfdx-source/apex-common-samplecode/test/classes/service/OpportunitiesServiceTest.cls b/sfdx-source/apex-common-samplecode/test/classes/service/OpportunitiesServiceTest.cls index 82d66d9..4384192 100644 --- a/sfdx-source/apex-common-samplecode/test/classes/service/OpportunitiesServiceTest.cls +++ b/sfdx-source/apex-common-samplecode/test/classes/service/OpportunitiesServiceTest.cls @@ -35,8 +35,8 @@ private class OpportunitiesServiceTest { // Given an opportunity with amount and no lines, returned by a mocked selector fflib_ApexMocks mocks = new fflib_ApexMocks(); - fflib_ISObjectUnitOfWork uowMock = (fflib_ISObjectUnitOfWork) mocks.mock( - fflib_ISObjectUnitOfWork.class + fflib_SObjectUnitOfWork uowMock = (fflib_SObjectUnitOfWork) mocks.mock( + fflib_SObjectUnitOfWork.class ); Opportunities domainMock = (Opportunities) mocks.mock(Opportunities.class); OpportunitiesSelector selectorMock = (OpportunitiesSelector) mocks.mock( @@ -65,6 +65,6 @@ private class OpportunitiesServiceTest // Then the selector, domain, and unit of work are used ((OpportunitiesSelector) mocks.verify(selectorMock)).selectByIdWithProducts(testOppsSet); ((Opportunities) mocks.verify(domainMock)).applyDiscount(10, uowMock); - ((fflib_ISObjectUnitOfWork) mocks.verify(uowMock, 1)).commitWork(); + ((fflib_SObjectUnitOfWork) mocks.verify(uowMock, 1)).commitWork(); } } From 4e867d5166d44291fc68754a2482fee6e754ee39 Mon Sep 17 00:00:00 2001 From: Andrew Fawcett Date: Sun, 6 Sep 2026 12:05:05 +0100 Subject: [PATCH 05/12] Bulk applyDiscounts via Map and address PR review feedback. --- .../main/classes/UnitOfWork.cls | 8 +++++ .../main/classes/actions/ApplyDiscount.cls | 17 +++++++--- .../OpportunityApplyDiscountController.cls | 4 ++- .../OpportunityApplyDiscountsController.cls | 11 ++++--- .../OpportunityCreateInvoicesController.cls | 4 +-- .../main/classes/domains/Opportunities.cls | 29 ++++++++++++---- .../classes/domains/OpportunityLineItems.cls | 15 ++++++--- .../restapis/OpportunitiesResource.cls | 4 ++- .../classes/service/OpportunitiesService.cls | 33 +++++++++---------- .../classes/actions/ApplyDiscountTest.cls | 3 +- ...OpportunityApplyDiscountControllerTest.cls | 3 +- .../classes/domains/OpportunitiesTest.cls | 4 +-- .../OpportunitiesServiceIntegrationTest.cls | 4 ++- .../service/OpportunitiesServiceTest.cls | 9 +++-- 14 files changed, 98 insertions(+), 50 deletions(-) diff --git a/sfdx-source/apex-common-samplecode/main/classes/UnitOfWork.cls b/sfdx-source/apex-common-samplecode/main/classes/UnitOfWork.cls index 299c91e..f9be4af 100644 --- a/sfdx-source/apex-common-samplecode/main/classes/UnitOfWork.cls +++ b/sfdx-source/apex-common-samplecode/main/classes/UnitOfWork.cls @@ -26,6 +26,14 @@ /** * Thin Unit of Work factory for the sample app. No Application class. + * + * This sample uses one app-level registry of {@code SObjectType} values so services can share + * the same Unit of Work instance within a transaction (for example discounting and invoicing + * in a single commit). + * + * An alternative is per-service factories with {@code newInstance(List types)} + * for a narrower scope per call site. That can reduce the central type list but makes it + * harder to pass one Unit of Work between services. */ public inherited sharing class UnitOfWork { @TestVisible diff --git a/sfdx-source/apex-common-samplecode/main/classes/actions/ApplyDiscount.cls b/sfdx-source/apex-common-samplecode/main/classes/actions/ApplyDiscount.cls index 11739a9..b344dac 100644 --- a/sfdx-source/apex-common-samplecode/main/classes/actions/ApplyDiscount.cls +++ b/sfdx-source/apex-common-samplecode/main/classes/actions/ApplyDiscount.cls @@ -30,7 +30,7 @@ */ public inherited sharing class ApplyDiscount { /** - * Applies a discount for each request via {@link OpportunitiesService#applyDiscounts}. + * Applies discounts for all requests via {@link OpportunitiesService#applyDiscounts}. * @param requests Opportunity Id and discount percent per request. Cannot be null. * @return Opportunity Ids in the same order as {@code requests}. */ @@ -40,12 +40,19 @@ public inherited sharing class ApplyDiscount { category='Opportunities' ) public static List execute(List requests) { + + // Build the map of Opportunity Ids and discount percentages. + Map discountByOpportunityId = new Map(); + for (Request request : requests) { + discountByOpportunityId.put(request.opportunityId, request.discountPercentage); + } + + // Apply discounts to the Opportunities. + OpportunitiesService.newInstance().applyDiscounts(discountByOpportunityId); + + // Build the results. List results = new List(); for (Request request : requests) { - OpportunitiesService.newInstance().applyDiscounts( - new Set{ request.opportunityId }, - request.discountPercentage - ); Result result = new Result(); result.opportunityId = request.opportunityId; result.discountPercentage = request.discountPercentage; diff --git a/sfdx-source/apex-common-samplecode/main/classes/controllers/OpportunityApplyDiscountController.cls b/sfdx-source/apex-common-samplecode/main/classes/controllers/OpportunityApplyDiscountController.cls index dff01fe..bd91196 100644 --- a/sfdx-source/apex-common-samplecode/main/classes/controllers/OpportunityApplyDiscountController.cls +++ b/sfdx-source/apex-common-samplecode/main/classes/controllers/OpportunityApplyDiscountController.cls @@ -40,7 +40,9 @@ public inherited sharing class OpportunityApplyDiscountController { try { - OpportunitiesService.newInstance().applyDiscounts(new Set{ opportunityId }, discountPercentage); + OpportunitiesService.newInstance().applyDiscounts( + new Map{ opportunityId => discountPercentage } + ); } catch (Exception e) { diff --git a/sfdx-source/apex-common-samplecode/main/classes/controllers/OpportunityApplyDiscountsController.cls b/sfdx-source/apex-common-samplecode/main/classes/controllers/OpportunityApplyDiscountsController.cls index aa37aa6..41e4af1 100644 --- a/sfdx-source/apex-common-samplecode/main/classes/controllers/OpportunityApplyDiscountsController.cls +++ b/sfdx-source/apex-common-samplecode/main/classes/controllers/OpportunityApplyDiscountsController.cls @@ -32,7 +32,7 @@ public inherited sharing class OpportunityApplyDiscountsController { /** * Discount percent entered on the list page (for example 10 for 10%). */ - public Decimal DiscountPercentage { get; set; } + public Decimal discountPercentage { get; set; } private ApexPages.StandardSetController standardSetController; @@ -45,15 +45,18 @@ public inherited sharing class OpportunityApplyDiscountsController { } /** - * Applies {@link #DiscountPercentage} to all selected Opportunities on the list view. + * Applies {@link #discountPercentage} to all selected Opportunities on the list view. * @return Null so the list page remains; adds an info message on success. */ public PageReference applyDiscounts() { try { Set selectedOpportunityIds = new Map(standardSetController.getSelected()).keySet(); - OpportunitiesService.newInstance() - .applyDiscounts(selectedOpportunityIds, DiscountPercentage); + Map discountByOpportunityId = new Map(); + for (Id opportunityId : selectedOpportunityIds) { + discountByOpportunityId.put(opportunityId, discountPercentage); + } + OpportunitiesService.newInstance().applyDiscounts(discountByOpportunityId); ApexPages.addMessage(new ApexPages.Message(ApexPages.Severity.INFO, 'Opportunities updated.')); } catch (Exception e) { ApexPages.addMessages(e); diff --git a/sfdx-source/apex-common-samplecode/main/classes/controllers/OpportunityCreateInvoicesController.cls b/sfdx-source/apex-common-samplecode/main/classes/controllers/OpportunityCreateInvoicesController.cls index bec59ef..541a120 100644 --- a/sfdx-source/apex-common-samplecode/main/classes/controllers/OpportunityCreateInvoicesController.cls +++ b/sfdx-source/apex-common-samplecode/main/classes/controllers/OpportunityCreateInvoicesController.cls @@ -33,7 +33,7 @@ public inherited sharing class OpportunityCreateInvoicesController { * Optional discount percent applied before invoice creation * (for example 10 for 10%). */ - public Decimal DiscountPercentage { get; set; } + public Decimal discountPercentage { get; set; } private ApexPages.StandardSetController standardSetController; @@ -54,7 +54,7 @@ public inherited sharing class OpportunityCreateInvoicesController { Set selectedOpportunityIds = new Map(standardSetController.getSelected()).keySet(); OpportunitiesService.newInstance() - .createInvoices(selectedOpportunityIds, DiscountPercentage); + .createInvoices(selectedOpportunityIds, discountPercentage); ApexPages.addMessage(new ApexPages.Message(ApexPages.Severity.INFO, 'Invoices created.')); } catch (Exception e) { ApexPages.addMessages(e); diff --git a/sfdx-source/apex-common-samplecode/main/classes/domains/Opportunities.cls b/sfdx-source/apex-common-samplecode/main/classes/domains/Opportunities.cls index 00c5a24..f66a1bf 100644 --- a/sfdx-source/apex-common-samplecode/main/classes/domains/Opportunities.cls +++ b/sfdx-source/apex-common-samplecode/main/classes/domains/Opportunities.cls @@ -84,30 +84,47 @@ public virtual inherited sharing class Opportunities extends fflib_SObjects } /** - * Applies a percentage discount to Opportunities without lines, or to their line items. + * Applies discounts for each Opportunity Id and percentage pair on this domain instance. * Registers dirty records on the Unit of Work. Does not commit. - * @param discountPercentage Discount percent to apply (for example 10 for 10%). Null is treated as zero. + * @param discountByOpportunityId Map of Opportunity Id to discount percent. * @param uow Unit of Work that receives dirty Opportunity / line item records. Cannot be null. */ - public virtual void applyDiscount(Decimal discountPercentage, fflib_ISObjectUnitOfWork uow) + public virtual void applyDiscounts(Map discountByOpportunityId, fflib_ISObjectUnitOfWork uow) { - Decimal factor = calculateDiscountFactor(discountPercentage); + if(discountByOpportunityId==null || discountByOpportunityId.isEmpty()) + { + return; + } List linesToApplyDiscount = new List(); + Map discountByOpportunityLineItemId = new Map(); + // Apply discounts to Opportunities without lines, or to their line items. for(Opportunity opportunity : (List) getRecords()) { + Decimal discountPercentage = discountByOpportunityId.get(opportunity.Id); if(opportunity.OpportunityLineItems==null || opportunity.OpportunityLineItems.isEmpty()) { + // Apply discount to the Opportunity (no line items). + Decimal factor = calculateDiscountFactor(discountPercentage); opportunity.Amount = opportunity.Amount * factor; uow.registerDirty(opportunity); } else { - linesToApplyDiscount.addAll(opportunity.OpportunityLineItems); + for(OpportunityLineItem lineItem : opportunity.OpportunityLineItems) + { + linesToApplyDiscount.add(lineItem); + discountByOpportunityLineItemId.put(lineItem.Id, discountPercentage); + } } } - OpportunityLineItems.newInstance(linesToApplyDiscount).applyDiscount(this, discountPercentage, uow); + // Apply discounts to line items. + if(!linesToApplyDiscount.isEmpty()) + { + OpportunityLineItems.newInstance(linesToApplyDiscount) + .applyDiscounts(this, discountByOpportunityLineItemId, uow); + } } /** diff --git a/sfdx-source/apex-common-samplecode/main/classes/domains/OpportunityLineItems.cls b/sfdx-source/apex-common-samplecode/main/classes/domains/OpportunityLineItems.cls index 78a0bb9..c394fbc 100644 --- a/sfdx-source/apex-common-samplecode/main/classes/domains/OpportunityLineItems.cls +++ b/sfdx-source/apex-common-samplecode/main/classes/domains/OpportunityLineItems.cls @@ -52,21 +52,25 @@ public virtual inherited sharing class OpportunityLineItems extends fflib_SObjec } /** - * Applies a percentage discount to UnitPrice on the wrapped line items. + * Applies discounts for each Opportunity Line Item Id and percentage pair. * When the parent Opportunity DiscountType__c is Approved Products, only lines whose * product has DiscountingApproved__c true are changed. Registers a minimal dirty * record (Id + UnitPrice) on the Unit of Work so user-mode DML does not touch unread fields. * @param opps Parent opportunities providing DiscountType__c context. Cannot be null. - * @param discountPercentage Discount percent to apply. Null is treated as zero by {@link Opportunities#calculateDiscountFactor}. + * @param discountByOpportunityLineItemId Map of Opportunity Line Item Id to discount percent. * @param uow Unit of Work that receives dirty line items. Cannot be null. */ - public virtual void applyDiscount(Opportunities opps, Decimal discountPercentage, fflib_ISObjectUnitOfWork uow) + public virtual void applyDiscounts( + Opportunities opps, + Map discountByOpportunityLineItemId, + fflib_ISObjectUnitOfWork uow + ) { Map opportunitiesById = new Map((List) opps.getRecords()); - Decimal factor = Opportunities.calculateDiscountFactor(discountPercentage); - for(OpportunityLineItem line : (List) getRecords()) { + // Apply discount to the line item only if the parent product has Approved Products discounting. + Decimal discountPercentage = discountByOpportunityLineItemId.get(line.Id); Opportunity opportunity = opportunitiesById.get(line.OpportunityId); if(opportunity.DiscountType__c=='Approved Products') { @@ -75,6 +79,7 @@ public virtual inherited sharing class OpportunityLineItems extends fflib_SObjec continue; } } + Decimal factor = Opportunities.calculateDiscountFactor(discountPercentage); line.UnitPrice = line.UnitPrice * factor; uow.registerDirty(new OpportunityLineItem(Id = line.Id, UnitPrice = line.UnitPrice)); } diff --git a/sfdx-source/apex-common-samplecode/main/classes/restapis/OpportunitiesResource.cls b/sfdx-source/apex-common-samplecode/main/classes/restapis/OpportunitiesResource.cls index 0d0da79..8466eef 100644 --- a/sfdx-source/apex-common-samplecode/main/classes/restapis/OpportunitiesResource.cls +++ b/sfdx-source/apex-common-samplecode/main/classes/restapis/OpportunitiesResource.cls @@ -43,6 +43,8 @@ global inherited sharing class OpportunitiesResource Id opportunityId = uriParts[2]; String discountParam = req.params.get('discount'); Decimal discountPercent = discountParam!=null ? Decimal.valueOf(discountParam) : 0; - OpportunitiesService.newInstance().applyDiscounts(new Set{ opportunityId }, discountPercent); + OpportunitiesService.newInstance().applyDiscounts( + new Map{ opportunityId => discountPercent } + ); } } diff --git a/sfdx-source/apex-common-samplecode/main/classes/service/OpportunitiesService.cls b/sfdx-source/apex-common-samplecode/main/classes/service/OpportunitiesService.cls index 0c0d988..cbcd061 100644 --- a/sfdx-source/apex-common-samplecode/main/classes/service/OpportunitiesService.cls +++ b/sfdx-source/apex-common-samplecode/main/classes/service/OpportunitiesService.cls @@ -52,28 +52,22 @@ public inherited sharing class OpportunitiesService } /** - * Loads Opportunities with products and applies a percentage discount and commits the updates. - * @param opportunityIds Opportunity Ids to discount. Empty set performs no DML. - * @param discountPercentage Discount percent to apply (for example 10 for 10%). + * Applies discounts in one query and one commit. + * @param discountByOpportunityId Map of Opportunity Id to discount percent. Empty map performs no DML. * @throws OpportunitiesServiceException When commit fails or another unexpected error occurs. */ - public void applyDiscounts(Set opportunityIds, Decimal discountPercentage) - { - applyDiscounts(selector.selectByIdWithProducts(opportunityIds), discountPercentage); - } - - /** - * Applies a percentage discount to the given Opportunities (with products) and commits the updates. - * @param opportunitiesWithProducts Opportunities including line items and product discount flags as needed. - * @param discountPercentage Discount percent to apply (for example 10 for 10%). - * @throws OpportunitiesServiceException When commit fails or another unexpected error occurs. - */ - public void applyDiscounts(List opportunitiesWithProducts, Decimal discountPercentage) + public void applyDiscounts(Map discountByOpportunityId) { + if(discountByOpportunityId==null || discountByOpportunityId.isEmpty()) + { + return; + } try { fflib_ISObjectUnitOfWork uow = UnitOfWork.newInstance(); - Opportunities.newInstance(opportunitiesWithProducts).applyDiscount(discountPercentage, uow); + List opportunitiesWithProducts = selector.selectByIdWithProducts(discountByOpportunityId.keySet()); + Opportunities opportunities = Opportunities.newInstance(opportunitiesWithProducts); + opportunities.applyDiscounts(discountByOpportunityId, uow); uow.commitWork(); } catch (Exception e) @@ -98,7 +92,12 @@ public inherited sharing class OpportunitiesService Opportunities opps = Opportunities.newInstance(selector.selectByIdWithProducts(opportunityIds)); if(discountPercentage!=null && discountPercentage > 0) { - opps.applyDiscount(discountPercentage, uow); + Map discountByOpportunityId = new Map(); + for(Opportunity opportunityRecord : (List) opps.getRecords()) + { + discountByOpportunityId.put(opportunityRecord.Id, discountPercentage); + } + opps.applyDiscounts(discountByOpportunityId, uow); } List invoices = new List(); diff --git a/sfdx-source/apex-common-samplecode/test/classes/actions/ApplyDiscountTest.cls b/sfdx-source/apex-common-samplecode/test/classes/actions/ApplyDiscountTest.cls index 9f4a47e..3f24106 100644 --- a/sfdx-source/apex-common-samplecode/test/classes/actions/ApplyDiscountTest.cls +++ b/sfdx-source/apex-common-samplecode/test/classes/actions/ApplyDiscountTest.cls @@ -47,8 +47,7 @@ private class ApplyDiscountTest { // Then the service is called and the opportunity id is returned ((OpportunitiesService) mocks.verify(mockService)).applyDiscounts( - new Set{ opportunityId }, - 10 + new Map{ opportunityId => 10 } ); System.assertEquals(1, results.size(), 'One result should be returned'); System.assertEquals(opportunityId, results[0].opportunityId, 'Opportunity Id should be returned'); diff --git a/sfdx-source/apex-common-samplecode/test/classes/controllers/OpportunityApplyDiscountControllerTest.cls b/sfdx-source/apex-common-samplecode/test/classes/controllers/OpportunityApplyDiscountControllerTest.cls index 350a200..d0fd811 100644 --- a/sfdx-source/apex-common-samplecode/test/classes/controllers/OpportunityApplyDiscountControllerTest.cls +++ b/sfdx-source/apex-common-samplecode/test/classes/controllers/OpportunityApplyDiscountControllerTest.cls @@ -44,8 +44,7 @@ private class OpportunityApplyDiscountControllerTest // Then the service is called with the opportunity id and discount percentage ((OpportunitiesService) mocks.verify(mockService)).applyDiscounts( - new Set{ opportunityId }, - 10 + new Map{ opportunityId => 10 } ); } } diff --git a/sfdx-source/apex-common-samplecode/test/classes/domains/OpportunitiesTest.cls b/sfdx-source/apex-common-samplecode/test/classes/domains/OpportunitiesTest.cls index 4d8aeb5..ddda8c5 100644 --- a/sfdx-source/apex-common-samplecode/test/classes/domains/OpportunitiesTest.cls +++ b/sfdx-source/apex-common-samplecode/test/classes/domains/OpportunitiesTest.cls @@ -48,7 +48,7 @@ private class OpportunitiesTest ); // When a discount is applied in the domain - opps.applyDiscount(10, uowMock); + opps.applyDiscounts(new Map{ opp.Id => 10 }, uowMock); // Then the amount is reduced on the wrapped records and registered dirty System.assertEquals(1, opps.getRecords().size(), 'Domain should still wrap one opportunity'); @@ -73,7 +73,7 @@ private class OpportunitiesTest // When discounts are applied and updates are committed by the caller Opportunities opps = Opportunities.newInstance(new List{ opp }); fflib_ISObjectUnitOfWork uow = UnitOfWork.newInstance(); - opps.applyDiscount(10, uow); + opps.applyDiscounts(new Map{ opp.Id => 10 }, uow); uow.commitWork(); // Then the stored amount reflects the discount diff --git a/sfdx-source/apex-common-samplecode/test/classes/service/OpportunitiesServiceIntegrationTest.cls b/sfdx-source/apex-common-samplecode/test/classes/service/OpportunitiesServiceIntegrationTest.cls index fd8501a..2635947 100644 --- a/sfdx-source/apex-common-samplecode/test/classes/service/OpportunitiesServiceIntegrationTest.cls +++ b/sfdx-source/apex-common-samplecode/test/classes/service/OpportunitiesServiceIntegrationTest.cls @@ -89,7 +89,9 @@ private class OpportunitiesServiceIntegrationTest { // When discounts are applied for the opportunity Test.startTest(); - OpportunitiesService.newInstance().applyDiscounts(new Set{ opp.Id }, 10); + OpportunitiesService.newInstance().applyDiscounts( + new Map{ opp.Id => 10 } + ); Test.stopTest(); } diff --git a/sfdx-source/apex-common-samplecode/test/classes/service/OpportunitiesServiceTest.cls b/sfdx-source/apex-common-samplecode/test/classes/service/OpportunitiesServiceTest.cls index 4384192..d7f0119 100644 --- a/sfdx-source/apex-common-samplecode/test/classes/service/OpportunitiesServiceTest.cls +++ b/sfdx-source/apex-common-samplecode/test/classes/service/OpportunitiesServiceTest.cls @@ -60,11 +60,16 @@ private class OpportunitiesServiceTest Opportunities.mock = domainMock; // When discounts are applied through the service - OpportunitiesService.newInstance().applyDiscounts(testOppsSet, 10); + Map discountByOpportunityId = new Map(); + for(Id opportunityId : testOppsSet) + { + discountByOpportunityId.put(opportunityId, 10); + } + OpportunitiesService.newInstance().applyDiscounts(discountByOpportunityId); // Then the selector, domain, and unit of work are used ((OpportunitiesSelector) mocks.verify(selectorMock)).selectByIdWithProducts(testOppsSet); - ((Opportunities) mocks.verify(domainMock)).applyDiscount(10, uowMock); + ((Opportunities) mocks.verify(domainMock)).applyDiscounts(discountByOpportunityId, uowMock); ((fflib_SObjectUnitOfWork) mocks.verify(uowMock, 1)).commitWork(); } } From 2b64b5dbf2cb0fb83a29ad431c879b3c21f0199c Mon Sep 17 00:00:00 2001 From: Andrew Fawcett Date: Sun, 6 Sep 2026 12:33:38 +0100 Subject: [PATCH 06/12] Bulk CreateInvoice, rename applyDiscountsAndCreateInvoices, and use fflib_SObjectUnitOfWork consistently. --- .../main/classes/actions/CreateInvoice.cls | 39 +++++---- .../classes/batchjobs/CreateInvoicesJob.cls | 6 +- .../OpportunityCreateInvoiceController.cls | 6 +- .../OpportunityCreateInvoicesController.cls | 7 +- .../main/classes/domains/Opportunities.cls | 2 +- .../classes/domains/OpportunityLineItems.cls | 2 +- .../classes/selectors/InvoicesSelector.cls | 82 +++++++++++++++++++ .../selectors/InvoicesSelector.cls-meta.xml | 5 ++ .../main/classes/service/AccountsService.cls | 2 +- .../main/classes/service/InvoicingService.cls | 6 +- .../classes/service/OpportunitiesService.cls | 42 +++++----- .../classes/actions/CreateInvoiceTest.cls | 20 +++-- ...OpportunityCreateInvoiceControllerTest.cls | 10 +-- .../classes/domains/OpportunitiesTest.cls | 8 +- 14 files changed, 175 insertions(+), 62 deletions(-) create mode 100644 sfdx-source/apex-common-samplecode/main/classes/selectors/InvoicesSelector.cls create mode 100644 sfdx-source/apex-common-samplecode/main/classes/selectors/InvoicesSelector.cls-meta.xml diff --git a/sfdx-source/apex-common-samplecode/main/classes/actions/CreateInvoice.cls b/sfdx-source/apex-common-samplecode/main/classes/actions/CreateInvoice.cls index d6a4e61..8681692 100644 --- a/sfdx-source/apex-common-samplecode/main/classes/actions/CreateInvoice.cls +++ b/sfdx-source/apex-common-samplecode/main/classes/actions/CreateInvoice.cls @@ -30,7 +30,7 @@ */ public inherited sharing class CreateInvoice { /** - * Creates an Invoice for each request via {@link OpportunitiesService#createInvoices}. + * Creates invoices for all requests via {@link OpportunitiesService#applyDiscountsAndCreateInvoices}. * @param requests Opportunity Id and optional discount percent per request. Cannot be null. * @return Invoice Id and Name in the same order as {@code requests}. */ @@ -40,29 +40,40 @@ public inherited sharing class CreateInvoice { category='Opportunities' ) public static List execute(List requests) { + + // Build the map of Opportunity Ids and discount percentages. + Map discountByOpportunityId = new Map(); + for (Request request : requests) { + discountByOpportunityId.put(request.opportunityId, request.discountPercentage); + } + + // Apply discounts and create invoices. + Map invoiceIdByOpportunityId = + OpportunitiesService.newInstance().applyDiscountsAndCreateInvoices(discountByOpportunityId); + Map invoiceNameById = invoiceNamesById(invoiceIdByOpportunityId.values()); + + // Build the results. List results = new List(); for (Request request : requests) { - Set invoiceIds = OpportunitiesService.newInstance().createInvoices( - new Set{ request.opportunityId }, - request.discountPercentage - ); - Id invoiceId = new List(invoiceIds)[0]; + Id invoiceId = invoiceIdByOpportunityId.get(request.opportunityId); Result result = new Result(); result.invoiceId = invoiceId; - result.invoiceName = invoiceName(invoiceId); + result.invoiceName = invoiceNameById.get(invoiceId); results.add(result); } return results; } - @TestVisible - private static String testInvoiceName; - - private static String invoiceName(Id invoiceId) { - if (testInvoiceName != null) { - return testInvoiceName; + private static Map invoiceNamesById(Iterable invoiceIds) { + Set invoiceIdSet = new Set(); + for (Id invoiceId : invoiceIds) { + invoiceIdSet.add(invoiceId); + } + Map invoiceNameById = new Map(); + for (Invoice__c invoice : InvoicesSelector.newInstance().selectById(invoiceIdSet)) { + invoiceNameById.put(invoice.Id, invoice.Name); } - return [SELECT Name FROM Invoice__c WHERE Id = :invoiceId].Name; + return invoiceNameById; } /** diff --git a/sfdx-source/apex-common-samplecode/main/classes/batchjobs/CreateInvoicesJob.cls b/sfdx-source/apex-common-samplecode/main/classes/batchjobs/CreateInvoicesJob.cls index 748da5f..dd63791 100644 --- a/sfdx-source/apex-common-samplecode/main/classes/batchjobs/CreateInvoicesJob.cls +++ b/sfdx-source/apex-common-samplecode/main/classes/batchjobs/CreateInvoicesJob.cls @@ -63,7 +63,11 @@ public inherited sharing class CreateInvoicesJob try { Set opportunitiesToInvoice = new Map(opps).keySet(); - OpportunitiesService.newInstance().createInvoices(opportunitiesToInvoice, 0); + Map discountByOpportunityId = new Map(); + for (Id opportunityId : opportunitiesToInvoice) { + discountByOpportunityId.put(opportunityId, 0); + } + OpportunitiesService.newInstance().applyDiscountsAndCreateInvoices(discountByOpportunityId); } catch (Exception e) { diff --git a/sfdx-source/apex-common-samplecode/main/classes/controllers/OpportunityCreateInvoiceController.cls b/sfdx-source/apex-common-samplecode/main/classes/controllers/OpportunityCreateInvoiceController.cls index 6bcd2b5..359b62f 100644 --- a/sfdx-source/apex-common-samplecode/main/classes/controllers/OpportunityCreateInvoiceController.cls +++ b/sfdx-source/apex-common-samplecode/main/classes/controllers/OpportunityCreateInvoiceController.cls @@ -41,8 +41,10 @@ public inherited sharing class OpportunityCreateInvoiceController { try { - Set invoiceIds = OpportunitiesService.newInstance().createInvoices(new Set{ opportunityId }, discountPercentage); - return new List(invoiceIds)[0]; + Map invoiceIdByOpportunityId = OpportunitiesService.newInstance().applyDiscountsAndCreateInvoices( + new Map{ opportunityId => discountPercentage } + ); + return invoiceIdByOpportunityId.get(opportunityId); } catch (Exception e) { diff --git a/sfdx-source/apex-common-samplecode/main/classes/controllers/OpportunityCreateInvoicesController.cls b/sfdx-source/apex-common-samplecode/main/classes/controllers/OpportunityCreateInvoicesController.cls index 541a120..0be4941 100644 --- a/sfdx-source/apex-common-samplecode/main/classes/controllers/OpportunityCreateInvoicesController.cls +++ b/sfdx-source/apex-common-samplecode/main/classes/controllers/OpportunityCreateInvoicesController.cls @@ -53,8 +53,11 @@ public inherited sharing class OpportunityCreateInvoicesController { try { Set selectedOpportunityIds = new Map(standardSetController.getSelected()).keySet(); - OpportunitiesService.newInstance() - .createInvoices(selectedOpportunityIds, discountPercentage); + Map discountByOpportunityId = new Map(); + for (Id opportunityId : selectedOpportunityIds) { + discountByOpportunityId.put(opportunityId, discountPercentage); + } + OpportunitiesService.newInstance().applyDiscountsAndCreateInvoices(discountByOpportunityId); ApexPages.addMessage(new ApexPages.Message(ApexPages.Severity.INFO, 'Invoices created.')); } catch (Exception e) { ApexPages.addMessages(e); diff --git a/sfdx-source/apex-common-samplecode/main/classes/domains/Opportunities.cls b/sfdx-source/apex-common-samplecode/main/classes/domains/Opportunities.cls index f66a1bf..ca1560a 100644 --- a/sfdx-source/apex-common-samplecode/main/classes/domains/Opportunities.cls +++ b/sfdx-source/apex-common-samplecode/main/classes/domains/Opportunities.cls @@ -89,7 +89,7 @@ public virtual inherited sharing class Opportunities extends fflib_SObjects * @param discountByOpportunityId Map of Opportunity Id to discount percent. * @param uow Unit of Work that receives dirty Opportunity / line item records. Cannot be null. */ - public virtual void applyDiscounts(Map discountByOpportunityId, fflib_ISObjectUnitOfWork uow) + public virtual void applyDiscounts(Map discountByOpportunityId, fflib_SObjectUnitOfWork uow) { if(discountByOpportunityId==null || discountByOpportunityId.isEmpty()) { diff --git a/sfdx-source/apex-common-samplecode/main/classes/domains/OpportunityLineItems.cls b/sfdx-source/apex-common-samplecode/main/classes/domains/OpportunityLineItems.cls index c394fbc..79a41f3 100644 --- a/sfdx-source/apex-common-samplecode/main/classes/domains/OpportunityLineItems.cls +++ b/sfdx-source/apex-common-samplecode/main/classes/domains/OpportunityLineItems.cls @@ -63,7 +63,7 @@ public virtual inherited sharing class OpportunityLineItems extends fflib_SObjec public virtual void applyDiscounts( Opportunities opps, Map discountByOpportunityLineItemId, - fflib_ISObjectUnitOfWork uow + fflib_SObjectUnitOfWork uow ) { Map opportunitiesById = new Map((List) opps.getRecords()); diff --git a/sfdx-source/apex-common-samplecode/main/classes/selectors/InvoicesSelector.cls b/sfdx-source/apex-common-samplecode/main/classes/selectors/InvoicesSelector.cls new file mode 100644 index 0000000..76d5cbc --- /dev/null +++ b/sfdx-source/apex-common-samplecode/main/classes/selectors/InvoicesSelector.cls @@ -0,0 +1,82 @@ +/** + * Copyright (c) 2013-2016, FinancialForce.com, inc + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * - Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * - Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * - Neither the name of the FinancialForce.com, inc nor the names of its contributors + * may be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL + * THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY + * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +**/ + +/** + * Selector for Invoice__c queries used by invocable and UI entry points. + */ +public virtual inherited sharing class InvoicesSelector extends fflib_SObjectSelector +{ + @TestVisible + private static InvoicesSelector mock; + + /** + * Factory that returns this selector, or {@link #mock} when tests have set one. + * @return {@link InvoicesSelector} instance. + */ + public static InvoicesSelector newInstance() + { + return mock!=null ? mock : new InvoicesSelector(); + } + + /** + * Creates a selector that queries in user mode. + */ + public InvoicesSelector() + { + super(false, fflib_SObjectSelector.DataAccess.USER_MODE); + } + + /** + * Fields included in default queries from this selector. + * @return Invoice__c Id and Name. + */ + public List getSObjectFieldList() + { + return new List{ + Invoice__c.Id, + Invoice__c.Name + }; + } + + /** + * SObject type this selector queries. + * @return {@code Invoice__c.SObjectType}. + */ + public Schema.SObjectType getSObjectType() + { + return Invoice__c.SObjectType; + } + + /** + * Selects Invoice__c records by Id with Id and Name. + * @param idSet Invoice Ids to query. Empty set returns an empty list. + * @return Matching invoices; empty list when none match. + */ + public List selectById(Set idSet) + { + return (List) selectSObjectsById(idSet); + } +} diff --git a/sfdx-source/apex-common-samplecode/main/classes/selectors/InvoicesSelector.cls-meta.xml b/sfdx-source/apex-common-samplecode/main/classes/selectors/InvoicesSelector.cls-meta.xml new file mode 100644 index 0000000..b009296 --- /dev/null +++ b/sfdx-source/apex-common-samplecode/main/classes/selectors/InvoicesSelector.cls-meta.xml @@ -0,0 +1,5 @@ + + + 67.0 + Active + diff --git a/sfdx-source/apex-common-samplecode/main/classes/service/AccountsService.cls b/sfdx-source/apex-common-samplecode/main/classes/service/AccountsService.cls index 8ec3658..76badb9 100644 --- a/sfdx-source/apex-common-samplecode/main/classes/service/AccountsService.cls +++ b/sfdx-source/apex-common-samplecode/main/classes/service/AccountsService.cls @@ -52,7 +52,7 @@ public inherited sharing class AccountsService return; } - fflib_ISObjectUnitOfWork uow = UnitOfWork.newInstance(); + fflib_SObjectUnitOfWork uow = UnitOfWork.newInstance(); List accountRecords = AccountsSelector.newInstance().selectById(accountIds); Accounts accountsDomain = Accounts.newInstance(accountRecords); accountsDomain.updateOpportunityActivity(); diff --git a/sfdx-source/apex-common-samplecode/main/classes/service/InvoicingService.cls b/sfdx-source/apex-common-samplecode/main/classes/service/InvoicingService.cls index 32d7311..0c64a38 100644 --- a/sfdx-source/apex-common-samplecode/main/classes/service/InvoicingService.cls +++ b/sfdx-source/apex-common-samplecode/main/classes/service/InvoicingService.cls @@ -59,7 +59,7 @@ public inherited sharing class InvoicingService */ public class InvoiceFactory { - private fflib_ISObjectUnitOfWork unitOfWork; + private fflib_SObjectUnitOfWork unitOfWork; /** * Invoice__c headers accumulated so far; Ids are set after commit. */ @@ -69,7 +69,7 @@ public inherited sharing class InvoicingService * Creates a factory that registers invoices on the given Unit of Work. * @param unitOfWork Unit of Work that will insert headers and lines. Cannot be null. */ - public InvoiceFactory(fflib_ISObjectUnitOfWork unitOfWork) + public InvoiceFactory(fflib_SObjectUnitOfWork unitOfWork) { this.unitOfWork = unitOfWork; this.Invoices = new List(); @@ -137,7 +137,7 @@ public inherited sharing class InvoicingService { try { - fflib_ISObjectUnitOfWork uow = UnitOfWork.newInstance(); + fflib_SObjectUnitOfWork uow = UnitOfWork.newInstance(); InvoiceFactory invoiceFactory = new InvoiceFactory(uow); List records = InvoicingTargetsRegistry.selectById(new Set(sourceRecordIds)); Object domain = InvoicingTargetsRegistry.newDomain(records); diff --git a/sfdx-source/apex-common-samplecode/main/classes/service/OpportunitiesService.cls b/sfdx-source/apex-common-samplecode/main/classes/service/OpportunitiesService.cls index cbcd061..2602591 100644 --- a/sfdx-source/apex-common-samplecode/main/classes/service/OpportunitiesService.cls +++ b/sfdx-source/apex-common-samplecode/main/classes/service/OpportunitiesService.cls @@ -64,7 +64,7 @@ public inherited sharing class OpportunitiesService } try { - fflib_ISObjectUnitOfWork uow = UnitOfWork.newInstance(); + fflib_SObjectUnitOfWork uow = UnitOfWork.newInstance(); List opportunitiesWithProducts = selector.selectByIdWithProducts(discountByOpportunityId.keySet()); Opportunities opportunities = Opportunities.newInstance(opportunitiesWithProducts); opportunities.applyDiscounts(discountByOpportunityId, uow); @@ -77,29 +77,28 @@ public inherited sharing class OpportunitiesService } /** - * Optionally discounts Opportunities, then creates Invoice__c and InvoiceLine__c records. + * Applies discounts, then creates Invoice__c and InvoiceLine__c records in one query and one commit. * Invoice date is CloseDate (the simple Opportunity path; {@link InvoicingService} uses CloseDate plus 14 days). - * @param opportunityIds Opportunity Ids to invoice. Empty set returns an empty set. - * @param discountPercentage Optional discount applied before invoicing when greater than zero; null or zero skips discounting. - * @return Ids of inserted Invoice__c records; empty set when no invoices were created. + * @param discountByOpportunityId Map of Opportunity Id to discount percent. Empty map performs no DML. + * @return Map of Opportunity Id to created Invoice__c Id. * @throws OpportunitiesServiceException When commit fails or another unexpected error occurs. */ - public Set createInvoices(Set opportunityIds, Decimal discountPercentage) + public Map applyDiscountsAndCreateInvoices(Map discountByOpportunityId) { + if(discountByOpportunityId==null || discountByOpportunityId.isEmpty()) + { + return new Map(); + } try { - fflib_ISObjectUnitOfWork uow = UnitOfWork.newInstance(); - Opportunities opps = Opportunities.newInstance(selector.selectByIdWithProducts(opportunityIds)); - if(discountPercentage!=null && discountPercentage > 0) - { - Map discountByOpportunityId = new Map(); - for(Opportunity opportunityRecord : (List) opps.getRecords()) - { - discountByOpportunityId.put(opportunityRecord.Id, discountPercentage); - } - opps.applyDiscounts(discountByOpportunityId, uow); - } + // Unit of Work to apply discounts and create invoices. + fflib_SObjectUnitOfWork uow = UnitOfWork.newInstance(); + + // Select the Opportunities and apply discounts. + Opportunities opps = Opportunities.newInstance(selector.selectByIdWithProducts(discountByOpportunityId.keySet())); + opps.applyDiscounts(discountByOpportunityId, uow); + // Create the invoices. List invoices = new List(); for(Opportunity opportunityRecord : (List) opps.getRecords()) { @@ -124,14 +123,17 @@ public inherited sharing class OpportunitiesService uow.registerNew(invoiceLineItem, InvoiceLine__c.Invoice__c, invoice); } } + + // Commit the Unit of Work. uow.commitWork(); - Set invoiceIds = new Set(); + // Build the result map. + Map invoiceIdByOpportunityId = new Map(); for(Invoice__c invoice : invoices) { - invoiceIds.add(invoice.Id); + invoiceIdByOpportunityId.put(invoice.Opportunity__c, invoice.Id); } - return invoiceIds; + return invoiceIdByOpportunityId; } catch (Exception e) { diff --git a/sfdx-source/apex-common-samplecode/test/classes/actions/CreateInvoiceTest.cls b/sfdx-source/apex-common-samplecode/test/classes/actions/CreateInvoiceTest.cls index 5a123aa..53f5441 100644 --- a/sfdx-source/apex-common-samplecode/test/classes/actions/CreateInvoiceTest.cls +++ b/sfdx-source/apex-common-samplecode/test/classes/actions/CreateInvoiceTest.cls @@ -31,17 +31,21 @@ private class CreateInvoiceTest { @IsTest private static void executeCallsServiceAndReturnsInvoiceId() { - // Given a mocked opportunities service that returns an invoice id + // Given mocked service and selector fflib_ApexMocks mocks = new fflib_ApexMocks(); OpportunitiesService mockService = (OpportunitiesService) mocks.mock(OpportunitiesService.class); + InvoicesSelector mockSelector = (InvoicesSelector) mocks.mock(InvoicesSelector.class); Id opportunityId = TestIdGenerator.generate(Opportunity.SObjectType); Id invoiceId = TestIdGenerator.generate(Invoice__c.SObjectType); - Set invoiceIds = new Set{ invoiceId }; + Map discountByOpportunityId = new Map{ opportunityId => 10 }; + Map invoiceIdByOpportunityId = new Map{ opportunityId => invoiceId }; + Invoice__c invoice = (Invoice__c) JSON.deserialize('{"Id":"' + invoiceId + '","Name":"INV-00000001"}', Invoice__c.class); mocks.startStubbing(); - mocks.when(mockService.createInvoices(new Set{ opportunityId }, 10)).thenReturn(invoiceIds); + mocks.when(mockService.applyDiscountsAndCreateInvoices(discountByOpportunityId)).thenReturn(invoiceIdByOpportunityId); + mocks.when(mockSelector.selectById(new Set{ invoiceId })).thenReturn(new List{ invoice }); mocks.stopStubbing(); OpportunitiesService.mock = mockService; - CreateInvoice.testInvoiceName = 'INV-00000001'; + InvoicesSelector.mock = mockSelector; CreateInvoice.Request request = new CreateInvoice.Request(); request.opportunityId = opportunityId; request.discountPercentage = 10; @@ -51,11 +55,11 @@ private class CreateInvoiceTest { new List{ request } ); - // Then the service is called and the invoice id is returned - ((OpportunitiesService) mocks.verify(mockService)).createInvoices( - new Set{ opportunityId }, - 10 + // Then the service and selector are called and the invoice id is returned + ((OpportunitiesService) mocks.verify(mockService)).applyDiscountsAndCreateInvoices( + new Map{ opportunityId => 10 } ); + ((InvoicesSelector) mocks.verify(mockSelector)).selectById(new Set{ invoiceId }); System.assertEquals(1, results.size(), 'One result should be returned'); System.assertEquals(invoiceId, results[0].invoiceId, 'Created invoice Id should be returned'); System.assertEquals('INV-00000001', results[0].invoiceName, 'Created invoice Name should be returned'); diff --git a/sfdx-source/apex-common-samplecode/test/classes/controllers/OpportunityCreateInvoiceControllerTest.cls b/sfdx-source/apex-common-samplecode/test/classes/controllers/OpportunityCreateInvoiceControllerTest.cls index abbb3e7..e3ce4a0 100644 --- a/sfdx-source/apex-common-samplecode/test/classes/controllers/OpportunityCreateInvoiceControllerTest.cls +++ b/sfdx-source/apex-common-samplecode/test/classes/controllers/OpportunityCreateInvoiceControllerTest.cls @@ -36,9 +36,10 @@ private class OpportunityCreateInvoiceControllerTest { OpportunitiesService mockService = (OpportunitiesService) mocks.mock(OpportunitiesService.class); Id opportunityId = TestIdGenerator.generate(Opportunity.SObjectType); Id invoiceId = TestIdGenerator.generate(Invoice__c.SObjectType); - Set invoiceIds = new Set{ invoiceId }; + Map discountByOpportunityId = new Map{ opportunityId => 10 }; + Map invoiceIdByOpportunityId = new Map{ opportunityId => invoiceId }; mocks.startStubbing(); - mocks.when(mockService.createInvoices(new Set{ opportunityId }, 10)).thenReturn(invoiceIds); + mocks.when(mockService.applyDiscountsAndCreateInvoices(discountByOpportunityId)).thenReturn(invoiceIdByOpportunityId); mocks.stopStubbing(); OpportunitiesService.mock = mockService; @@ -46,9 +47,8 @@ private class OpportunityCreateInvoiceControllerTest { Id resultId = OpportunityCreateInvoiceController.createInvoice(opportunityId, 10); // Then the service is called and the invoice id is returned - ((OpportunitiesService) mocks.verify(mockService)).createInvoices( - new Set{ opportunityId }, - 10 + ((OpportunitiesService) mocks.verify(mockService)).applyDiscountsAndCreateInvoices( + new Map{ opportunityId => 10 } ); System.assertEquals(invoiceId, resultId, 'Created invoice Id should be returned'); } diff --git a/sfdx-source/apex-common-samplecode/test/classes/domains/OpportunitiesTest.cls b/sfdx-source/apex-common-samplecode/test/classes/domains/OpportunitiesTest.cls index ddda8c5..faae864 100644 --- a/sfdx-source/apex-common-samplecode/test/classes/domains/OpportunitiesTest.cls +++ b/sfdx-source/apex-common-samplecode/test/classes/domains/OpportunitiesTest.cls @@ -43,8 +43,8 @@ private class OpportunitiesTest ); Opportunities opps = Opportunities.newInstance(new List{ opp }); fflib_ApexMocks mocks = new fflib_ApexMocks(); - fflib_ISObjectUnitOfWork uowMock = (fflib_ISObjectUnitOfWork) mocks.mock( - fflib_ISObjectUnitOfWork.class + fflib_SObjectUnitOfWork uowMock = (fflib_SObjectUnitOfWork) mocks.mock( + fflib_SObjectUnitOfWork.class ); // When a discount is applied in the domain @@ -54,7 +54,7 @@ private class OpportunitiesTest System.assertEquals(1, opps.getRecords().size(), 'Domain should still wrap one opportunity'); System.assertEquals(900, opp.Amount, 'Amount should be discounted to 900'); System.assertEquals(900, ((Opportunity) opps.getRecords()[0]).Amount, 'getRecords amount should be 900'); - ((fflib_ISObjectUnitOfWork) mocks.verify(uowMock)).registerDirty(opp); + ((fflib_SObjectUnitOfWork) mocks.verify(uowMock)).registerDirty(opp); } @IsTest @@ -72,7 +72,7 @@ private class OpportunitiesTest // When discounts are applied and updates are committed by the caller Opportunities opps = Opportunities.newInstance(new List{ opp }); - fflib_ISObjectUnitOfWork uow = UnitOfWork.newInstance(); + fflib_SObjectUnitOfWork uow = UnitOfWork.newInstance(); opps.applyDiscounts(new Map{ opp.Id => 10 }, uow); uow.commitWork(); From 5eab3ddacb354601c0746e009b9a1bb1edde5b42 Mon Sep 17 00:00:00 2001 From: Andrew Fawcett Date: Tue, 8 Sep 2026 11:05:30 +0100 Subject: [PATCH 07/12] Adapted to constructor injection See README.md Architecture Notes --- README.md | 8 ++- .../main/classes/UnitOfWork.cls | 14 ++--- .../main/classes/actions/ApplyDiscount.cls | 31 +++++++++- .../main/classes/actions/CreateInvoice.cls | 38 ++++++++++++- .../OpportunityApplyDiscountController.cls | 34 ++++++++++- .../OpportunityApplyDiscountsController.cls | 23 +++++++- .../OpportunityCreateInvoiceController.cls | 35 +++++++++++- .../OpportunityCreateInvoicesController.cls | 23 +++++++- .../OpportunityInvoicingController.cls | 33 ++++++++++- .../main/classes/domains/Opportunities.cls | 2 + .../classes/domains/OpportunityLineItems.cls | 1 + .../classes/selectors/AccountsSelector.cls | 7 +-- .../selectors/DeveloperWorkItemsSelector.cls | 7 +-- .../classes/selectors/InvoicesSelector.cls | 7 +-- .../selectors/OpportunitiesSelector.cls | 7 +-- .../OpportunityLineItemsSelector.cls | 7 +-- .../selectors/PricebookEntriesSelector.cls | 7 +-- .../classes/selectors/PricebooksSelector.cls | 7 +-- .../classes/selectors/ProductsSelector.cls | 9 +-- .../selectors/TrainingWorkItemsSelector.cls | 7 +-- .../main/classes/selectors/UsersSelector.cls | 7 +-- .../main/classes/service/AccountsService.cls | 28 +++++++-- .../main/classes/service/InvoicingService.cls | 20 +++++-- .../classes/service/OpportunitiesService.cls | 30 +++++----- .../main/lwc/jsconfig.json | 2 +- .../classes/actions/ApplyDiscountTest.cls | 3 +- .../classes/actions/CreateInvoiceTest.cls | 4 +- ...OpportunityApplyDiscountControllerTest.cls | 4 +- ...pportunityApplyDiscountsControllerTest.cls | 57 +++++++++++++++++++ ...yApplyDiscountsControllerTest.cls-meta.xml | 5 ++ ...OpportunityCreateInvoiceControllerTest.cls | 4 +- ...pportunityCreateInvoicesControllerTest.cls | 57 +++++++++++++++++++ ...yCreateInvoicesControllerTest.cls-meta.xml | 5 ++ .../OpportunityInvoicingControllerTest.cls | 4 +- .../classes/service/AccountsServiceTest.cls | 5 +- .../service/OpportunitiesServiceTest.cls | 4 +- 36 files changed, 423 insertions(+), 123 deletions(-) create mode 100644 sfdx-source/apex-common-samplecode/test/classes/controllers/OpportunityApplyDiscountsControllerTest.cls create mode 100644 sfdx-source/apex-common-samplecode/test/classes/controllers/OpportunityApplyDiscountsControllerTest.cls-meta.xml create mode 100644 sfdx-source/apex-common-samplecode/test/classes/controllers/OpportunityCreateInvoicesControllerTest.cls create mode 100644 sfdx-source/apex-common-samplecode/test/classes/controllers/OpportunityCreateInvoicesControllerTest.cls-meta.xml diff --git a/README.md b/README.md index c4440a6..e99421e 100644 --- a/README.md +++ b/README.md @@ -30,13 +30,15 @@ This repository contains a sample application illustrating the Apex Enterprise P Architecture Notes ------------------ -This sample uses **concrete** Domain, Selector, and Service classes with static `newInstance()` factories and `@TestVisible` mock hooks for unit tests. There is no `Application.cls` dependency-injection factory — see [Apex Enterprise Patterns: Recent Updates and Thoughts on the Application Class](https://andyinthecloud.com/2026/04/13/apex-enterprise-patterns-recent-updates-and-thoughts-on-the-application-class/) for background on this approach. +This sample uses **concrete** Domain, Selector, and Service classes. Constructors take collaborators; `newInstance()` is the default composition. Prefer `X.newInstance()` at entry points over `new X()`. Use the constructor to inject mocks (during Apex Tests) or to compose deliberately (for example one Unit of Work passed to two services). `X.newInstance()` is also the single place a later metadata-driven factory would resolve which service, selector, or domain implementation to construct. + +Domains wrap records, so they are constructed when those records are in hand — including mid-method, as when `Opportunities.applyDiscounts` builds `OpportunityLineItems`. Domain `newInstance(records)` keeps a `@TestVisible` mock for that case. This sample no longer includes an `Application` factory; that is reserved for more advanced DI, such as via [AT4DX](https://github.com/apex-enterprise-patterns/at4dx). | Component | Role | |-----------|------| | **Services** | `OpportunitiesService`, `InvoicingService`, `AccountsService` — orchestrate selectors, domains, and Unit of Work | -| **Domains** | Business logic on records (e.g. discounting, invoice DTO generation) | -| **Trigger handlers** | `fflib_SObjectDomain` subclasses wired from triggers | +| **Domains** | `Opportunities`, `OpportunityLineItems`, `Accounts` — record behaviour (discounting, invoice DTOs) on `fflib_SObjects`. Constructed via `newInstance(records)` when the records are in hand; not trigger lifecycle | +| **Trigger handlers** | `OpportunitiesTriggerHandler` — `fflib_SObjectDomain` trigger lifecycle (defaults, validation, related updates) | | **UnitOfWork** | `UnitOfWork.cls` — thin factory returning `fflib_SObjectUnitOfWork` with `UserModeDML()` | | **InvoicingTargetsRegistry** | Resolves invoice targets from `InvoiceTargets__mdt` at runtime | diff --git a/sfdx-source/apex-common-samplecode/main/classes/UnitOfWork.cls b/sfdx-source/apex-common-samplecode/main/classes/UnitOfWork.cls index f9be4af..553a6b5 100644 --- a/sfdx-source/apex-common-samplecode/main/classes/UnitOfWork.cls +++ b/sfdx-source/apex-common-samplecode/main/classes/UnitOfWork.cls @@ -27,27 +27,21 @@ /** * Thin Unit of Work factory for the sample app. No Application class. * - * This sample uses one app-level registry of {@code SObjectType} values so services can share - * the same Unit of Work instance within a transaction (for example discounting and invoicing - * in a single commit). + * This sample uses one app-level registry of {@code SObjectType} values so a caller can + * construct one Unit of Work and pass it into multiple service constructors in the same + * transaction. Service methods still commit when they own the complete use case. * * An alternative is per-service factories with {@code newInstance(List types)} * for a narrower scope per call site. That can reduce the central type list but makes it * harder to pass one Unit of Work between services. */ public inherited sharing class UnitOfWork { - @TestVisible - private static fflib_SObjectUnitOfWork mock; - /** - * Returns a Unit of Work registered for this app's types, or {@link #mock} when tests have set one. + * Returns a Unit of Work registered for this app's types. * DML runs in user mode. * @return Unit of Work instance. */ public static fflib_SObjectUnitOfWork newInstance() { - if (mock != null) { - return mock; - } return new fflib_SObjectUnitOfWork( new List{ Account.SObjectType, diff --git a/sfdx-source/apex-common-samplecode/main/classes/actions/ApplyDiscount.cls b/sfdx-source/apex-common-samplecode/main/classes/actions/ApplyDiscount.cls index b344dac..564ece7 100644 --- a/sfdx-source/apex-common-samplecode/main/classes/actions/ApplyDiscount.cls +++ b/sfdx-source/apex-common-samplecode/main/classes/actions/ApplyDiscount.cls @@ -27,8 +27,28 @@ /** * Invocable action that applies a percentage discount to an Opportunity. * Delegates to {@link OpportunitiesService}. + * + * Prefer the no-arg constructor / {@link #execute} for the default service. + * Use the service constructor when composing deliberately (including tests). */ public inherited sharing class ApplyDiscount { + private final OpportunitiesService service; + + /** + * Default composition: {@link OpportunitiesService#newInstance}. + */ + public ApplyDiscount() { + this(OpportunitiesService.newInstance()); + } + + /** + * Creates an action with the given service. + * @param service Opportunities service used to apply discounts. Cannot be null. + */ + public ApplyDiscount(OpportunitiesService service) { + this.service = service; + } + /** * Applies discounts for all requests via {@link OpportunitiesService#applyDiscounts}. * @param requests Opportunity Id and discount percent per request. Cannot be null. @@ -40,6 +60,15 @@ public inherited sharing class ApplyDiscount { category='Opportunities' ) public static List execute(List requests) { + return new ApplyDiscount().run(requests); + } + + /** + * Instance entry used by {@link #execute} and by callers that inject a service. + * @param requests Opportunity Id and discount percent per request. Cannot be null. + * @return Opportunity Ids in the same order as {@code requests}. + */ + public List run(List requests) { // Build the map of Opportunity Ids and discount percentages. Map discountByOpportunityId = new Map(); @@ -48,7 +77,7 @@ public inherited sharing class ApplyDiscount { } // Apply discounts to the Opportunities. - OpportunitiesService.newInstance().applyDiscounts(discountByOpportunityId); + service.applyDiscounts(discountByOpportunityId); // Build the results. List results = new List(); diff --git a/sfdx-source/apex-common-samplecode/main/classes/actions/CreateInvoice.cls b/sfdx-source/apex-common-samplecode/main/classes/actions/CreateInvoice.cls index 8681692..35744d8 100644 --- a/sfdx-source/apex-common-samplecode/main/classes/actions/CreateInvoice.cls +++ b/sfdx-source/apex-common-samplecode/main/classes/actions/CreateInvoice.cls @@ -27,8 +27,31 @@ /** * Invocable action that creates an Invoice from an Opportunity. * Delegates to {@link OpportunitiesService}. + * + * Prefer the no-arg constructor / {@link #execute} for the default service and selector. + * Use the constructor when composing deliberately (including tests). */ public inherited sharing class CreateInvoice { + private final OpportunitiesService service; + private final InvoicesSelector selector; + + /** + * Default composition: {@link OpportunitiesService#newInstance} and {@link InvoicesSelector#newInstance}. + */ + public CreateInvoice() { + this(OpportunitiesService.newInstance(), InvoicesSelector.newInstance()); + } + + /** + * Creates an action with the given service and selector. + * @param service Opportunities service used to create invoices. Cannot be null. + * @param selector Selector used to load created invoice names. Cannot be null. + */ + public CreateInvoice(OpportunitiesService service, InvoicesSelector selector) { + this.service = service; + this.selector = selector; + } + /** * Creates invoices for all requests via {@link OpportunitiesService#applyDiscountsAndCreateInvoices}. * @param requests Opportunity Id and optional discount percent per request. Cannot be null. @@ -40,6 +63,15 @@ public inherited sharing class CreateInvoice { category='Opportunities' ) public static List execute(List requests) { + return new CreateInvoice().run(requests); + } + + /** + * Instance entry used by {@link #execute} and by callers that inject collaborators. + * @param requests Opportunity Id and optional discount percent per request. Cannot be null. + * @return Invoice Id and Name in the same order as {@code requests}. + */ + public List run(List requests) { // Build the map of Opportunity Ids and discount percentages. Map discountByOpportunityId = new Map(); @@ -49,7 +81,7 @@ public inherited sharing class CreateInvoice { // Apply discounts and create invoices. Map invoiceIdByOpportunityId = - OpportunitiesService.newInstance().applyDiscountsAndCreateInvoices(discountByOpportunityId); + service.applyDiscountsAndCreateInvoices(discountByOpportunityId); Map invoiceNameById = invoiceNamesById(invoiceIdByOpportunityId.values()); // Build the results. @@ -64,13 +96,13 @@ public inherited sharing class CreateInvoice { return results; } - private static Map invoiceNamesById(Iterable invoiceIds) { + private Map invoiceNamesById(Iterable invoiceIds) { Set invoiceIdSet = new Set(); for (Id invoiceId : invoiceIds) { invoiceIdSet.add(invoiceId); } Map invoiceNameById = new Map(); - for (Invoice__c invoice : InvoicesSelector.newInstance().selectById(invoiceIdSet)) { + for (Invoice__c invoice : selector.selectById(invoiceIdSet)) { invoiceNameById.put(invoice.Id, invoice.Name); } return invoiceNameById; diff --git a/sfdx-source/apex-common-samplecode/main/classes/controllers/OpportunityApplyDiscountController.cls b/sfdx-source/apex-common-samplecode/main/classes/controllers/OpportunityApplyDiscountController.cls index bd91196..01f4249 100644 --- a/sfdx-source/apex-common-samplecode/main/classes/controllers/OpportunityApplyDiscountController.cls +++ b/sfdx-source/apex-common-samplecode/main/classes/controllers/OpportunityApplyDiscountController.cls @@ -27,9 +27,31 @@ /** * Controller for the Apply Discount Lightning action. * Delegates discounting to {@link OpportunitiesService}. + * + * Prefer the no-arg constructor / {@link #applyDiscount} for the default service. + * Use the service constructor when composing deliberately (including tests). */ public inherited sharing class OpportunityApplyDiscountController { + private final OpportunitiesService service; + + /** + * Default composition: {@link OpportunitiesService#newInstance}. + */ + public OpportunityApplyDiscountController() + { + this(OpportunitiesService.newInstance()); + } + + /** + * Creates a controller with the given service. + * @param service Opportunities service used to apply discounts. Cannot be null. + */ + public OpportunityApplyDiscountController(OpportunitiesService service) + { + this.service = service; + } + /** * Applies a discount to a single Opportunity from the Lightning action. * @param opportunityId Opportunity Id to discount. Cannot be null. @@ -37,10 +59,20 @@ public inherited sharing class OpportunityApplyDiscountController */ @AuraEnabled(cacheable=false) public static void applyDiscount(Id opportunityId, Decimal discountPercentage) + { + new OpportunityApplyDiscountController().apply(opportunityId, discountPercentage); + } + + /** + * Instance entry used by {@link #applyDiscount} and by callers that inject a service. + * @param opportunityId Opportunity Id to discount. Cannot be null. + * @param discountPercentage Discount percent to apply (for example 10 for 10%). + */ + public void apply(Id opportunityId, Decimal discountPercentage) { try { - OpportunitiesService.newInstance().applyDiscounts( + service.applyDiscounts( new Map{ opportunityId => discountPercentage } ); } diff --git a/sfdx-source/apex-common-samplecode/main/classes/controllers/OpportunityApplyDiscountsController.cls b/sfdx-source/apex-common-samplecode/main/classes/controllers/OpportunityApplyDiscountsController.cls index 41e4af1..bb714c2 100644 --- a/sfdx-source/apex-common-samplecode/main/classes/controllers/OpportunityApplyDiscountsController.cls +++ b/sfdx-source/apex-common-samplecode/main/classes/controllers/OpportunityApplyDiscountsController.cls @@ -27,6 +27,9 @@ /** * Visualforce controller for the Apply Discounts list-view button. * Delegates discounting to {@link OpportunitiesService}. + * + * Prefer the StandardSetController constructor for the default service. + * Use the service overload when composing deliberately (including tests). */ public inherited sharing class OpportunityApplyDiscountsController { /** @@ -35,13 +38,27 @@ public inherited sharing class OpportunityApplyDiscountsController { public Decimal discountPercentage { get; set; } private ApexPages.StandardSetController standardSetController; + private final OpportunitiesService service; /** - * Creates a controller for a list StandardSetController (bulk apply). + * Default composition: {@link OpportunitiesService#newInstance}. * @param controller StandardSetController with selected Opportunities. Cannot be null. */ public OpportunityApplyDiscountsController(ApexPages.StandardSetController controller) { - standardSetController = controller; + this(controller, OpportunitiesService.newInstance()); + } + + /** + * Creates a controller with the given StandardSetController and service. + * @param controller StandardSetController with selected Opportunities. Cannot be null. + * @param service Opportunities service used to apply discounts. Cannot be null. + */ + public OpportunityApplyDiscountsController( + ApexPages.StandardSetController controller, + OpportunitiesService service + ) { + this.standardSetController = controller; + this.service = service; } /** @@ -56,7 +73,7 @@ public inherited sharing class OpportunityApplyDiscountsController { for (Id opportunityId : selectedOpportunityIds) { discountByOpportunityId.put(opportunityId, discountPercentage); } - OpportunitiesService.newInstance().applyDiscounts(discountByOpportunityId); + service.applyDiscounts(discountByOpportunityId); ApexPages.addMessage(new ApexPages.Message(ApexPages.Severity.INFO, 'Opportunities updated.')); } catch (Exception e) { ApexPages.addMessages(e); diff --git a/sfdx-source/apex-common-samplecode/main/classes/controllers/OpportunityCreateInvoiceController.cls b/sfdx-source/apex-common-samplecode/main/classes/controllers/OpportunityCreateInvoiceController.cls index 359b62f..455de45 100644 --- a/sfdx-source/apex-common-samplecode/main/classes/controllers/OpportunityCreateInvoiceController.cls +++ b/sfdx-source/apex-common-samplecode/main/classes/controllers/OpportunityCreateInvoiceController.cls @@ -27,9 +27,31 @@ /** * Controller for the Create Invoice Lightning action. * Delegates invoice creation to {@link OpportunitiesService}. + * + * Prefer the no-arg constructor / {@link #createInvoice} for the default service. + * Use the service constructor when composing deliberately (including tests). */ public inherited sharing class OpportunityCreateInvoiceController { + private final OpportunitiesService service; + + /** + * Default composition: {@link OpportunitiesService#newInstance}. + */ + public OpportunityCreateInvoiceController() + { + this(OpportunitiesService.newInstance()); + } + + /** + * Creates a controller with the given service. + * @param service Opportunities service used to create invoices. Cannot be null. + */ + public OpportunityCreateInvoiceController(OpportunitiesService service) + { + this.service = service; + } + /** * Creates an invoice for a single Opportunity from the Lightning action. * @param opportunityId Opportunity Id to invoice. Cannot be null. @@ -38,10 +60,21 @@ public inherited sharing class OpportunityCreateInvoiceController */ @AuraEnabled(cacheable=false) public static Id createInvoice(Id opportunityId, Decimal discountPercentage) + { + return new OpportunityCreateInvoiceController().create(opportunityId, discountPercentage); + } + + /** + * Instance entry used by {@link #createInvoice} and by callers that inject a service. + * @param opportunityId Opportunity Id to invoice. Cannot be null. + * @param discountPercentage Optional discount applied before invoicing when greater than zero. + * @return Id of the first created Invoice__c. + */ + public Id create(Id opportunityId, Decimal discountPercentage) { try { - Map invoiceIdByOpportunityId = OpportunitiesService.newInstance().applyDiscountsAndCreateInvoices( + Map invoiceIdByOpportunityId = service.applyDiscountsAndCreateInvoices( new Map{ opportunityId => discountPercentage } ); return invoiceIdByOpportunityId.get(opportunityId); diff --git a/sfdx-source/apex-common-samplecode/main/classes/controllers/OpportunityCreateInvoicesController.cls b/sfdx-source/apex-common-samplecode/main/classes/controllers/OpportunityCreateInvoicesController.cls index 0be4941..7355b70 100644 --- a/sfdx-source/apex-common-samplecode/main/classes/controllers/OpportunityCreateInvoicesController.cls +++ b/sfdx-source/apex-common-samplecode/main/classes/controllers/OpportunityCreateInvoicesController.cls @@ -27,6 +27,9 @@ /** * Visualforce controller for the Create Invoices list-view button. * Delegates invoice creation to {@link OpportunitiesService}. + * + * Prefer the StandardSetController constructor for the default service. + * Use the service overload when composing deliberately (including tests). */ public inherited sharing class OpportunityCreateInvoicesController { /** @@ -36,13 +39,27 @@ public inherited sharing class OpportunityCreateInvoicesController { public Decimal discountPercentage { get; set; } private ApexPages.StandardSetController standardSetController; + private final OpportunitiesService service; /** - * Creates a controller for a list StandardSetController (bulk create). + * Default composition: {@link OpportunitiesService#newInstance}. * @param controller StandardSetController with selected Opportunities. Cannot be null. */ public OpportunityCreateInvoicesController(ApexPages.StandardSetController controller) { - standardSetController = controller; + this(controller, OpportunitiesService.newInstance()); + } + + /** + * Creates a controller with the given StandardSetController and service. + * @param controller StandardSetController with selected Opportunities. Cannot be null. + * @param service Opportunities service used to create invoices. Cannot be null. + */ + public OpportunityCreateInvoicesController( + ApexPages.StandardSetController controller, + OpportunitiesService service + ) { + this.standardSetController = controller; + this.service = service; } /** @@ -57,7 +74,7 @@ public inherited sharing class OpportunityCreateInvoicesController { for (Id opportunityId : selectedOpportunityIds) { discountByOpportunityId.put(opportunityId, discountPercentage); } - OpportunitiesService.newInstance().applyDiscountsAndCreateInvoices(discountByOpportunityId); + service.applyDiscountsAndCreateInvoices(discountByOpportunityId); ApexPages.addMessage(new ApexPages.Message(ApexPages.Severity.INFO, 'Invoices created.')); } catch (Exception e) { ApexPages.addMessages(e); diff --git a/sfdx-source/apex-common-samplecode/main/classes/controllers/OpportunityInvoicingController.cls b/sfdx-source/apex-common-samplecode/main/classes/controllers/OpportunityInvoicingController.cls index b605d13..760bfc7 100644 --- a/sfdx-source/apex-common-samplecode/main/classes/controllers/OpportunityInvoicingController.cls +++ b/sfdx-source/apex-common-samplecode/main/classes/controllers/OpportunityInvoicingController.cls @@ -26,19 +26,50 @@ /** * Controller that submits the Opportunity invoicing Batch Apex job from a headless Lightning action. + * + * Prefer the no-arg constructor / {@link #startJob} for the default service. + * Use the service constructor when composing deliberately (including tests). */ public inherited sharing class OpportunityInvoicingController { + private final OpportunitiesService service; + + /** + * Default composition: {@link OpportunitiesService#newInstance}. + */ + public OpportunityInvoicingController() + { + this(OpportunitiesService.newInstance()); + } + + /** + * Creates a controller with the given service. + * @param service Opportunities service used to submit the job. Cannot be null. + */ + public OpportunityInvoicingController(OpportunitiesService service) + { + this.service = service; + } + /** * Submits {@link CreateInvoicesJob} through {@link OpportunitiesService#submitInvoicingJob}. * @return Async Apex job Id returned after a successful submit. */ @AuraEnabled(cacheable=false) public static Id startJob() + { + return new OpportunityInvoicingController().start(); + } + + /** + * Instance entry used by {@link #startJob} and by callers that inject a service. + * @return Async Apex job Id returned after a successful submit. + */ + public Id start() { try { - return OpportunitiesService.newInstance().submitInvoicingJob(); + return service.submitInvoicingJob(); } catch (Exception e) { diff --git a/sfdx-source/apex-common-samplecode/main/classes/domains/Opportunities.cls b/sfdx-source/apex-common-samplecode/main/classes/domains/Opportunities.cls index ca1560a..c15e7bb 100644 --- a/sfdx-source/apex-common-samplecode/main/classes/domains/Opportunities.cls +++ b/sfdx-source/apex-common-samplecode/main/classes/domains/Opportunities.cls @@ -36,6 +36,8 @@ public virtual inherited sharing class Opportunities extends fflib_SObjects /** * Factory that wraps the given Opportunity records, or {@link #mock} when tests have set one. + * Domain records are not known until the caller has them, so this factory (not a constructor + * collaborator) is the seam for substituting a domain created mid-method. * @param recordList Opportunities to manage. Null is treated as an empty list. * @return {@link Opportunities} instance. */ diff --git a/sfdx-source/apex-common-samplecode/main/classes/domains/OpportunityLineItems.cls b/sfdx-source/apex-common-samplecode/main/classes/domains/OpportunityLineItems.cls index 79a41f3..741b61e 100644 --- a/sfdx-source/apex-common-samplecode/main/classes/domains/OpportunityLineItems.cls +++ b/sfdx-source/apex-common-samplecode/main/classes/domains/OpportunityLineItems.cls @@ -34,6 +34,7 @@ public virtual inherited sharing class OpportunityLineItems extends fflib_SObjec /** * Factory that wraps the given line items, or {@link #mock} when tests have set one. + * Used when another domain constructs line items mid-method (records are not known earlier). * @param recordList Opportunity line items to manage. Null is treated as an empty list. * @return {@link OpportunityLineItems} instance. */ diff --git a/sfdx-source/apex-common-samplecode/main/classes/selectors/AccountsSelector.cls b/sfdx-source/apex-common-samplecode/main/classes/selectors/AccountsSelector.cls index 79911e0..b52a112 100644 --- a/sfdx-source/apex-common-samplecode/main/classes/selectors/AccountsSelector.cls +++ b/sfdx-source/apex-common-samplecode/main/classes/selectors/AccountsSelector.cls @@ -29,16 +29,13 @@ */ public virtual inherited sharing class AccountsSelector extends fflib_SObjectSelector { - @TestVisible - private static AccountsSelector mock; - /** - * Factory that returns this selector, or {@link #mock} when tests have set one. + * Factory that returns this selector. * @return {@link AccountsSelector} instance. */ public static AccountsSelector newInstance() { - return mock!=null ? mock : new AccountsSelector(); + return new AccountsSelector(); } /** diff --git a/sfdx-source/apex-common-samplecode/main/classes/selectors/DeveloperWorkItemsSelector.cls b/sfdx-source/apex-common-samplecode/main/classes/selectors/DeveloperWorkItemsSelector.cls index d5417a3..d92e631 100644 --- a/sfdx-source/apex-common-samplecode/main/classes/selectors/DeveloperWorkItemsSelector.cls +++ b/sfdx-source/apex-common-samplecode/main/classes/selectors/DeveloperWorkItemsSelector.cls @@ -28,15 +28,12 @@ * Selector for DeveloperWorkItem__c including related Work Order account for invoicing. */ public virtual inherited sharing class DeveloperWorkItemsSelector extends fflib_SObjectSelector { - @TestVisible - private static DeveloperWorkItemsSelector mock; - /** - * Factory that returns this selector, or {@link #mock} when tests have set one. + * Factory that returns this selector. * @return {@link DeveloperWorkItemsSelector} instance. */ public static DeveloperWorkItemsSelector newInstance() { - return mock != null ? mock : new DeveloperWorkItemsSelector(); + return new DeveloperWorkItemsSelector(); } /** diff --git a/sfdx-source/apex-common-samplecode/main/classes/selectors/InvoicesSelector.cls b/sfdx-source/apex-common-samplecode/main/classes/selectors/InvoicesSelector.cls index 76d5cbc..75d7fae 100644 --- a/sfdx-source/apex-common-samplecode/main/classes/selectors/InvoicesSelector.cls +++ b/sfdx-source/apex-common-samplecode/main/classes/selectors/InvoicesSelector.cls @@ -29,16 +29,13 @@ */ public virtual inherited sharing class InvoicesSelector extends fflib_SObjectSelector { - @TestVisible - private static InvoicesSelector mock; - /** - * Factory that returns this selector, or {@link #mock} when tests have set one. + * Factory that returns this selector. * @return {@link InvoicesSelector} instance. */ public static InvoicesSelector newInstance() { - return mock!=null ? mock : new InvoicesSelector(); + return new InvoicesSelector(); } /** diff --git a/sfdx-source/apex-common-samplecode/main/classes/selectors/OpportunitiesSelector.cls b/sfdx-source/apex-common-samplecode/main/classes/selectors/OpportunitiesSelector.cls index 96fecf1..a311d80 100644 --- a/sfdx-source/apex-common-samplecode/main/classes/selectors/OpportunitiesSelector.cls +++ b/sfdx-source/apex-common-samplecode/main/classes/selectors/OpportunitiesSelector.cls @@ -29,16 +29,13 @@ */ public virtual inherited sharing class OpportunitiesSelector extends fflib_SObjectSelector { - @TestVisible - private static OpportunitiesSelector mock; - /** - * Factory that returns this selector, or {@link #mock} when tests have set one. + * Factory that returns this selector. * @return {@link OpportunitiesSelector} instance. */ public static OpportunitiesSelector newInstance() { - return mock!=null ? mock : new OpportunitiesSelector(); + return new OpportunitiesSelector(); } /** diff --git a/sfdx-source/apex-common-samplecode/main/classes/selectors/OpportunityLineItemsSelector.cls b/sfdx-source/apex-common-samplecode/main/classes/selectors/OpportunityLineItemsSelector.cls index 1eee6cf..e1cfebb 100644 --- a/sfdx-source/apex-common-samplecode/main/classes/selectors/OpportunityLineItemsSelector.cls +++ b/sfdx-source/apex-common-samplecode/main/classes/selectors/OpportunityLineItemsSelector.cls @@ -29,16 +29,13 @@ */ public virtual inherited sharing class OpportunityLineItemsSelector extends fflib_SObjectSelector { - @TestVisible - private static OpportunityLineItemsSelector mock; - /** - * Factory that returns this selector, or {@link #mock} when tests have set one. + * Factory that returns this selector. * @return {@link OpportunityLineItemsSelector} instance. */ public static OpportunityLineItemsSelector newInstance() { - return mock!=null ? mock : new OpportunityLineItemsSelector(); + return new OpportunityLineItemsSelector(); } /** diff --git a/sfdx-source/apex-common-samplecode/main/classes/selectors/PricebookEntriesSelector.cls b/sfdx-source/apex-common-samplecode/main/classes/selectors/PricebookEntriesSelector.cls index fe405c0..cfe74ba 100644 --- a/sfdx-source/apex-common-samplecode/main/classes/selectors/PricebookEntriesSelector.cls +++ b/sfdx-source/apex-common-samplecode/main/classes/selectors/PricebookEntriesSelector.cls @@ -29,16 +29,13 @@ */ public virtual inherited sharing class PricebookEntriesSelector extends fflib_SObjectSelector { - @TestVisible - private static PricebookEntriesSelector mock; - /** - * Factory that returns this selector, or {@link #mock} when tests have set one. + * Factory that returns this selector. * @return {@link PricebookEntriesSelector} instance. */ public static PricebookEntriesSelector newInstance() { - return mock!=null ? mock : new PricebookEntriesSelector(); + return new PricebookEntriesSelector(); } /** diff --git a/sfdx-source/apex-common-samplecode/main/classes/selectors/PricebooksSelector.cls b/sfdx-source/apex-common-samplecode/main/classes/selectors/PricebooksSelector.cls index d5de888..df71f9a 100644 --- a/sfdx-source/apex-common-samplecode/main/classes/selectors/PricebooksSelector.cls +++ b/sfdx-source/apex-common-samplecode/main/classes/selectors/PricebooksSelector.cls @@ -29,16 +29,13 @@ */ public virtual inherited sharing class PricebooksSelector extends fflib_SObjectSelector { - @TestVisible - private static PricebooksSelector mock; - /** - * Factory that returns this selector, or {@link #mock} when tests have set one. + * Factory that returns this selector. * @return {@link PricebooksSelector} instance. */ public static PricebooksSelector newInstance() { - return mock!=null ? mock : new PricebooksSelector(); + return new PricebooksSelector(); } /** diff --git a/sfdx-source/apex-common-samplecode/main/classes/selectors/ProductsSelector.cls b/sfdx-source/apex-common-samplecode/main/classes/selectors/ProductsSelector.cls index 7b16c28..ac0ad77 100644 --- a/sfdx-source/apex-common-samplecode/main/classes/selectors/ProductsSelector.cls +++ b/sfdx-source/apex-common-samplecode/main/classes/selectors/ProductsSelector.cls @@ -29,11 +29,8 @@ */ public virtual inherited sharing class ProductsSelector extends fflib_SObjectSelector { - @TestVisible - private static ProductsSelector mock; - /** - * Factory that returns a selector without field set fields, or {@link #mock} when tests have set one. + * Factory that returns a selector without field set fields. * @return {@link ProductsSelector} instance. */ public static ProductsSelector newInstance() @@ -42,13 +39,13 @@ public virtual inherited sharing class ProductsSelector extends fflib_SObjectSel } /** - * Factory that returns a selector with optional field set fields, or {@link #mock} when tests have set one. + * Factory that returns a selector with optional field set fields. * @param includeFieldSetFields When true, OpportunityDiscount field set fields are included. * @return {@link ProductsSelector} instance. */ public static ProductsSelector newInstance(Boolean includeFieldSetFields) { - return mock!=null ? mock : new ProductsSelector(includeFieldSetFields); + return new ProductsSelector(includeFieldSetFields); } /** diff --git a/sfdx-source/apex-common-samplecode/main/classes/selectors/TrainingWorkItemsSelector.cls b/sfdx-source/apex-common-samplecode/main/classes/selectors/TrainingWorkItemsSelector.cls index 8aad0aa..2f50eec 100644 --- a/sfdx-source/apex-common-samplecode/main/classes/selectors/TrainingWorkItemsSelector.cls +++ b/sfdx-source/apex-common-samplecode/main/classes/selectors/TrainingWorkItemsSelector.cls @@ -28,15 +28,12 @@ * Selector for TrainingWorkItem__c including related Work Order account for invoicing. */ public virtual inherited sharing class TrainingWorkItemsSelector extends fflib_SObjectSelector { - @TestVisible - private static TrainingWorkItemsSelector mock; - /** - * Factory that returns this selector, or {@link #mock} when tests have set one. + * Factory that returns this selector. * @return {@link TrainingWorkItemsSelector} instance. */ public static TrainingWorkItemsSelector newInstance() { - return mock != null ? mock : new TrainingWorkItemsSelector(); + return new TrainingWorkItemsSelector(); } /** diff --git a/sfdx-source/apex-common-samplecode/main/classes/selectors/UsersSelector.cls b/sfdx-source/apex-common-samplecode/main/classes/selectors/UsersSelector.cls index 938268e..177ff14 100644 --- a/sfdx-source/apex-common-samplecode/main/classes/selectors/UsersSelector.cls +++ b/sfdx-source/apex-common-samplecode/main/classes/selectors/UsersSelector.cls @@ -29,16 +29,13 @@ */ public virtual inherited sharing class UsersSelector extends fflib_SObjectSelector { - @TestVisible - private static UsersSelector mock; - /** - * Factory that returns this selector, or {@link #mock} when tests have set one. + * Factory that returns this selector. * @return {@link UsersSelector} instance. */ public static UsersSelector newInstance() { - return mock!=null ? mock : new UsersSelector(); + return new UsersSelector(); } /** diff --git a/sfdx-source/apex-common-samplecode/main/classes/service/AccountsService.cls b/sfdx-source/apex-common-samplecode/main/classes/service/AccountsService.cls index 76badb9..4bc4be6 100644 --- a/sfdx-source/apex-common-samplecode/main/classes/service/AccountsService.cls +++ b/sfdx-source/apex-common-samplecode/main/classes/service/AccountsService.cls @@ -26,19 +26,36 @@ /** * Service for Account-related operations invoked from the service layer. + * + * Prefer {@link #newInstance} for the default selector and Unit of Work. + * Use the constructor when tests inject mocks or a caller composes deliberately. */ public inherited sharing class AccountsService { - @TestVisible - private static AccountsService mock; + private final AccountsSelector selector; + private final fflib_SObjectUnitOfWork uow; /** - * Factory that returns this service, or {@link #mock} when tests have set one. + * Default composition: {@link AccountsSelector#newInstance} and {@link UnitOfWork#newInstance}. * @return {@link AccountsService} instance. */ public static AccountsService newInstance() { - return mock!=null ? mock : new AccountsService(); + return new AccountsService( + AccountsSelector.newInstance(), + UnitOfWork.newInstance() + ); + } + + /** + * Creates a service with the given selector and Unit of Work. + * @param selector Account selector used to load records. Cannot be null. + * @param uow Unit of Work used to register and commit changes. Cannot be null. + */ + public AccountsService(AccountsSelector selector, fflib_SObjectUnitOfWork uow) + { + this.selector = selector; + this.uow = uow; } /** @@ -52,8 +69,7 @@ public inherited sharing class AccountsService return; } - fflib_SObjectUnitOfWork uow = UnitOfWork.newInstance(); - List accountRecords = AccountsSelector.newInstance().selectById(accountIds); + List accountRecords = selector.selectById(accountIds); Accounts accountsDomain = Accounts.newInstance(accountRecords); accountsDomain.updateOpportunityActivity(); uow.registerDirty(accountsDomain.getRecords()); diff --git a/sfdx-source/apex-common-samplecode/main/classes/service/InvoicingService.cls b/sfdx-source/apex-common-samplecode/main/classes/service/InvoicingService.cls index 0c64a38..a5b5311 100644 --- a/sfdx-source/apex-common-samplecode/main/classes/service/InvoicingService.cls +++ b/sfdx-source/apex-common-samplecode/main/classes/service/InvoicingService.cls @@ -27,19 +27,30 @@ /** * Polymorphic invoicing service that generates invoices from registered source SObject types * via {@link InvoicingTargetsRegistry} and domains implementing {@link ISupportInvoicing}. + * + * Prefer {@link #newInstance} for the default Unit of Work. + * Use the constructor when tests inject mocks or a caller composes deliberately. */ public inherited sharing class InvoicingService { - @TestVisible - private static InvoicingService mock; + private final fflib_SObjectUnitOfWork uow; /** - * Factory that returns this service, or {@link #mock} when tests have set one. + * Default composition: {@link UnitOfWork#newInstance}. * @return {@link InvoicingService} instance. */ public static InvoicingService newInstance() { - return mock!=null ? mock : new InvoicingService(); + return new InvoicingService(UnitOfWork.newInstance()); + } + + /** + * Creates a service with the given Unit of Work. + * @param uow Unit of Work used to register and commit invoices. Cannot be null. + */ + public InvoicingService(fflib_SObjectUnitOfWork uow) + { + this.uow = uow; } /** @@ -137,7 +148,6 @@ public inherited sharing class InvoicingService { try { - fflib_SObjectUnitOfWork uow = UnitOfWork.newInstance(); InvoiceFactory invoiceFactory = new InvoiceFactory(uow); List records = InvoicingTargetsRegistry.selectById(new Set(sourceRecordIds)); Object domain = InvoicingTargetsRegistry.newDomain(records); diff --git a/sfdx-source/apex-common-samplecode/main/classes/service/OpportunitiesService.cls b/sfdx-source/apex-common-samplecode/main/classes/service/OpportunitiesService.cls index 2602591..71a6a28 100644 --- a/sfdx-source/apex-common-samplecode/main/classes/service/OpportunitiesService.cls +++ b/sfdx-source/apex-common-samplecode/main/classes/service/OpportunitiesService.cls @@ -26,29 +26,37 @@ /** * Service for Opportunity discounting, invoice creation, and invoicing job submission. + * + * Prefer {@link #newInstance} for the default selector and Unit of Work. + * Use the constructor when tests inject mocks or a caller composes deliberately + * (for example one Unit of Work shared with another service). */ public inherited sharing class OpportunitiesService { - @TestVisible - private static OpportunitiesService mock; + private final OpportunitiesSelector selector; + private final fflib_SObjectUnitOfWork uow; /** - * Factory that returns this service, or {@link #mock} when tests have set one. + * Default composition: {@link OpportunitiesSelector#newInstance} and {@link UnitOfWork#newInstance}. * @return {@link OpportunitiesService} instance. */ public static OpportunitiesService newInstance() { - return mock!=null ? mock : new OpportunitiesService(); + return new OpportunitiesService( + OpportunitiesSelector.newInstance(), + UnitOfWork.newInstance() + ); } - private OpportunitiesSelector selector; - /** - * Creates a service using {@link OpportunitiesSelector#newInstance}. + * Creates a service with the given selector and Unit of Work. + * @param selector Opportunity selector used to load records. Cannot be null. + * @param uow Unit of Work used to register and commit changes. Cannot be null. */ - public OpportunitiesService() + public OpportunitiesService(OpportunitiesSelector selector, fflib_SObjectUnitOfWork uow) { - this.selector = OpportunitiesSelector.newInstance(); + this.selector = selector; + this.uow = uow; } /** @@ -64,7 +72,6 @@ public inherited sharing class OpportunitiesService } try { - fflib_SObjectUnitOfWork uow = UnitOfWork.newInstance(); List opportunitiesWithProducts = selector.selectByIdWithProducts(discountByOpportunityId.keySet()); Opportunities opportunities = Opportunities.newInstance(opportunitiesWithProducts); opportunities.applyDiscounts(discountByOpportunityId, uow); @@ -91,9 +98,6 @@ public inherited sharing class OpportunitiesService } try { - // Unit of Work to apply discounts and create invoices. - fflib_SObjectUnitOfWork uow = UnitOfWork.newInstance(); - // Select the Opportunities and apply discounts. Opportunities opps = Opportunities.newInstance(selector.selectByIdWithProducts(discountByOpportunityId.keySet())); opps.applyDiscounts(discountByOpportunityId, uow); diff --git a/sfdx-source/apex-common-samplecode/main/lwc/jsconfig.json b/sfdx-source/apex-common-samplecode/main/lwc/jsconfig.json index bf7b731..dec1502 100644 --- a/sfdx-source/apex-common-samplecode/main/lwc/jsconfig.json +++ b/sfdx-source/apex-common-samplecode/main/lwc/jsconfig.json @@ -10,7 +10,7 @@ }, "include": [ "**/*", - "../../../.sfdx/typings/lwc/**/*.d.ts" + "../../../../.sfdx/typings/lwc/**/*.d.ts" ], "typeAcquisition": { "include": [ diff --git a/sfdx-source/apex-common-samplecode/test/classes/actions/ApplyDiscountTest.cls b/sfdx-source/apex-common-samplecode/test/classes/actions/ApplyDiscountTest.cls index 3f24106..3ad0d72 100644 --- a/sfdx-source/apex-common-samplecode/test/classes/actions/ApplyDiscountTest.cls +++ b/sfdx-source/apex-common-samplecode/test/classes/actions/ApplyDiscountTest.cls @@ -35,13 +35,12 @@ private class ApplyDiscountTest { fflib_ApexMocks mocks = new fflib_ApexMocks(); OpportunitiesService mockService = (OpportunitiesService) mocks.mock(OpportunitiesService.class); Id opportunityId = TestIdGenerator.generate(Opportunity.SObjectType); - OpportunitiesService.mock = mockService; ApplyDiscount.Request request = new ApplyDiscount.Request(); request.opportunityId = opportunityId; request.discountPercentage = 10; // When the invocable action is executed - List results = ApplyDiscount.execute( + List results = new ApplyDiscount(mockService).run( new List{ request } ); diff --git a/sfdx-source/apex-common-samplecode/test/classes/actions/CreateInvoiceTest.cls b/sfdx-source/apex-common-samplecode/test/classes/actions/CreateInvoiceTest.cls index 53f5441..bebd8df 100644 --- a/sfdx-source/apex-common-samplecode/test/classes/actions/CreateInvoiceTest.cls +++ b/sfdx-source/apex-common-samplecode/test/classes/actions/CreateInvoiceTest.cls @@ -44,14 +44,12 @@ private class CreateInvoiceTest { mocks.when(mockService.applyDiscountsAndCreateInvoices(discountByOpportunityId)).thenReturn(invoiceIdByOpportunityId); mocks.when(mockSelector.selectById(new Set{ invoiceId })).thenReturn(new List{ invoice }); mocks.stopStubbing(); - OpportunitiesService.mock = mockService; - InvoicesSelector.mock = mockSelector; CreateInvoice.Request request = new CreateInvoice.Request(); request.opportunityId = opportunityId; request.discountPercentage = 10; // When the invocable action is executed - List results = CreateInvoice.execute( + List results = new CreateInvoice(mockService, mockSelector).run( new List{ request } ); diff --git a/sfdx-source/apex-common-samplecode/test/classes/controllers/OpportunityApplyDiscountControllerTest.cls b/sfdx-source/apex-common-samplecode/test/classes/controllers/OpportunityApplyDiscountControllerTest.cls index d0fd811..a47ebd8 100644 --- a/sfdx-source/apex-common-samplecode/test/classes/controllers/OpportunityApplyDiscountControllerTest.cls +++ b/sfdx-source/apex-common-samplecode/test/classes/controllers/OpportunityApplyDiscountControllerTest.cls @@ -37,10 +37,8 @@ private class OpportunityApplyDiscountControllerTest fflib_ApexMocks mocks = new fflib_ApexMocks(); OpportunitiesService mockService = (OpportunitiesService) mocks.mock(OpportunitiesService.class); Id opportunityId = TestIdGenerator.generate(Opportunity.SObjectType); - OpportunitiesService.mock = mockService; - // When apply discount is invoked from the Lightning action - OpportunityApplyDiscountController.applyDiscount(opportunityId, 10); + new OpportunityApplyDiscountController(mockService).apply(opportunityId, 10); // Then the service is called with the opportunity id and discount percentage ((OpportunitiesService) mocks.verify(mockService)).applyDiscounts( diff --git a/sfdx-source/apex-common-samplecode/test/classes/controllers/OpportunityApplyDiscountsControllerTest.cls b/sfdx-source/apex-common-samplecode/test/classes/controllers/OpportunityApplyDiscountsControllerTest.cls new file mode 100644 index 0000000..737e15f --- /dev/null +++ b/sfdx-source/apex-common-samplecode/test/classes/controllers/OpportunityApplyDiscountsControllerTest.cls @@ -0,0 +1,57 @@ +/** + * Copyright (c) 2012-2014, FinancialForce.com, inc + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * - Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * - Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * - Neither the name of the FinancialForce.com, inc nor the names of its contributors + * may be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL + * THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY + * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +**/ + +/** + * Unit tests for OpportunityApplyDiscountsController using fflib-apex-mocks. + */ +@IsTest +private class OpportunityApplyDiscountsControllerTest { + @IsTest + private static void applyDiscountsCallsService() { + // Given selected opportunities and a mocked opportunities service + fflib_ApexMocks mocks = new fflib_ApexMocks(); + OpportunitiesService mockService = (OpportunitiesService) mocks.mock(OpportunitiesService.class); + Id opportunityId = TestIdGenerator.generate(Opportunity.SObjectType); + List selected = new List{ + new Opportunity(Id = opportunityId) + }; + ApexPages.StandardSetController standardSetController = new ApexPages.StandardSetController(selected); + standardSetController.setSelected(selected); + OpportunityApplyDiscountsController controller = new OpportunityApplyDiscountsController( + standardSetController, + mockService + ); + controller.discountPercentage = 10; + + // When apply discounts is invoked from the list view + controller.applyDiscounts(); + + // Then the service is called with the selected opportunity ids and discount percentage + ((OpportunitiesService) mocks.verify(mockService)).applyDiscounts( + new Map{ opportunityId => 10 } + ); + } +} diff --git a/sfdx-source/apex-common-samplecode/test/classes/controllers/OpportunityApplyDiscountsControllerTest.cls-meta.xml b/sfdx-source/apex-common-samplecode/test/classes/controllers/OpportunityApplyDiscountsControllerTest.cls-meta.xml new file mode 100644 index 0000000..b009296 --- /dev/null +++ b/sfdx-source/apex-common-samplecode/test/classes/controllers/OpportunityApplyDiscountsControllerTest.cls-meta.xml @@ -0,0 +1,5 @@ + + + 67.0 + Active + diff --git a/sfdx-source/apex-common-samplecode/test/classes/controllers/OpportunityCreateInvoiceControllerTest.cls b/sfdx-source/apex-common-samplecode/test/classes/controllers/OpportunityCreateInvoiceControllerTest.cls index e3ce4a0..f9d9f59 100644 --- a/sfdx-source/apex-common-samplecode/test/classes/controllers/OpportunityCreateInvoiceControllerTest.cls +++ b/sfdx-source/apex-common-samplecode/test/classes/controllers/OpportunityCreateInvoiceControllerTest.cls @@ -41,10 +41,8 @@ private class OpportunityCreateInvoiceControllerTest { mocks.startStubbing(); mocks.when(mockService.applyDiscountsAndCreateInvoices(discountByOpportunityId)).thenReturn(invoiceIdByOpportunityId); mocks.stopStubbing(); - OpportunitiesService.mock = mockService; - // When create invoice is invoked from the Lightning action - Id resultId = OpportunityCreateInvoiceController.createInvoice(opportunityId, 10); + Id resultId = new OpportunityCreateInvoiceController(mockService).create(opportunityId, 10); // Then the service is called and the invoice id is returned ((OpportunitiesService) mocks.verify(mockService)).applyDiscountsAndCreateInvoices( diff --git a/sfdx-source/apex-common-samplecode/test/classes/controllers/OpportunityCreateInvoicesControllerTest.cls b/sfdx-source/apex-common-samplecode/test/classes/controllers/OpportunityCreateInvoicesControllerTest.cls new file mode 100644 index 0000000..9618ada --- /dev/null +++ b/sfdx-source/apex-common-samplecode/test/classes/controllers/OpportunityCreateInvoicesControllerTest.cls @@ -0,0 +1,57 @@ +/** + * Copyright (c) 2012-2014, FinancialForce.com, inc + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * - Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * - Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * - Neither the name of the FinancialForce.com, inc nor the names of its contributors + * may be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL + * THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY + * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +**/ + +/** + * Unit tests for OpportunityCreateInvoicesController using fflib-apex-mocks. + */ +@IsTest +private class OpportunityCreateInvoicesControllerTest { + @IsTest + private static void createInvoicesCallsService() { + // Given selected opportunities and a mocked opportunities service + fflib_ApexMocks mocks = new fflib_ApexMocks(); + OpportunitiesService mockService = (OpportunitiesService) mocks.mock(OpportunitiesService.class); + Id opportunityId = TestIdGenerator.generate(Opportunity.SObjectType); + List selected = new List{ + new Opportunity(Id = opportunityId) + }; + ApexPages.StandardSetController standardSetController = new ApexPages.StandardSetController(selected); + standardSetController.setSelected(selected); + OpportunityCreateInvoicesController controller = new OpportunityCreateInvoicesController( + standardSetController, + mockService + ); + controller.discountPercentage = 10; + + // When create invoices is invoked from the list view + controller.createInvoices(); + + // Then the service is called with the selected opportunity ids and discount percentage + ((OpportunitiesService) mocks.verify(mockService)).applyDiscountsAndCreateInvoices( + new Map{ opportunityId => 10 } + ); + } +} diff --git a/sfdx-source/apex-common-samplecode/test/classes/controllers/OpportunityCreateInvoicesControllerTest.cls-meta.xml b/sfdx-source/apex-common-samplecode/test/classes/controllers/OpportunityCreateInvoicesControllerTest.cls-meta.xml new file mode 100644 index 0000000..b009296 --- /dev/null +++ b/sfdx-source/apex-common-samplecode/test/classes/controllers/OpportunityCreateInvoicesControllerTest.cls-meta.xml @@ -0,0 +1,5 @@ + + + 67.0 + Active + diff --git a/sfdx-source/apex-common-samplecode/test/classes/controllers/OpportunityInvoicingControllerTest.cls b/sfdx-source/apex-common-samplecode/test/classes/controllers/OpportunityInvoicingControllerTest.cls index 96d8086..52bf737 100644 --- a/sfdx-source/apex-common-samplecode/test/classes/controllers/OpportunityInvoicingControllerTest.cls +++ b/sfdx-source/apex-common-samplecode/test/classes/controllers/OpportunityInvoicingControllerTest.cls @@ -38,10 +38,8 @@ private class OpportunityInvoicingControllerTest { mocks.startStubbing(); mocks.when(mockService.submitInvoicingJob()).thenReturn(jobId); mocks.stopStubbing(); - OpportunitiesService.mock = mockService; - // When start job is invoked from the Lightning action - Id resultId = OpportunityInvoicingController.startJob(); + Id resultId = new OpportunityInvoicingController(mockService).start(); // Then the service is called and the job id is returned ((OpportunitiesService) mocks.verify(mockService)).submitInvoicingJob(); diff --git a/sfdx-source/apex-common-samplecode/test/classes/service/AccountsServiceTest.cls b/sfdx-source/apex-common-samplecode/test/classes/service/AccountsServiceTest.cls index b4bd2ea..49783d5 100644 --- a/sfdx-source/apex-common-samplecode/test/classes/service/AccountsServiceTest.cls +++ b/sfdx-source/apex-common-samplecode/test/classes/service/AccountsServiceTest.cls @@ -57,11 +57,8 @@ private class AccountsServiceTest mocks.when(selectorMock.selectById(accountIds)).thenReturn(accounts); mocks.stopStubbing(); - UnitOfWork.mock = unitOfWorkMock; - AccountsSelector.mock = selectorMock; - Test.startTest(); - AccountsService.newInstance().updateOpportunityActivity(accountIds); + new AccountsService(selectorMock, unitOfWorkMock).updateOpportunityActivity(accountIds); Test.stopTest(); ((AccountsSelector) mocks.verify(selectorMock)).selectById(accountIds); diff --git a/sfdx-source/apex-common-samplecode/test/classes/service/OpportunitiesServiceTest.cls b/sfdx-source/apex-common-samplecode/test/classes/service/OpportunitiesServiceTest.cls index d7f0119..f276fc0 100644 --- a/sfdx-source/apex-common-samplecode/test/classes/service/OpportunitiesServiceTest.cls +++ b/sfdx-source/apex-common-samplecode/test/classes/service/OpportunitiesServiceTest.cls @@ -55,8 +55,6 @@ private class OpportunitiesServiceTest mocks.startStubbing(); mocks.when(selectorMock.selectByIdWithProducts(testOppsSet)).thenReturn(testOppsList); mocks.stopStubbing(); - UnitOfWork.mock = uowMock; - OpportunitiesSelector.mock = selectorMock; Opportunities.mock = domainMock; // When discounts are applied through the service @@ -65,7 +63,7 @@ private class OpportunitiesServiceTest { discountByOpportunityId.put(opportunityId, 10); } - OpportunitiesService.newInstance().applyDiscounts(discountByOpportunityId); + new OpportunitiesService(selectorMock, uowMock).applyDiscounts(discountByOpportunityId); // Then the selector, domain, and unit of work are used ((OpportunitiesSelector) mocks.verify(selectorMock)).selectByIdWithProducts(testOppsSet); From 31ac091b9172f56f4f237b659e845ef9cc4543b5 Mon Sep 17 00:00:00 2001 From: Andrew Fawcett Date: Tue, 8 Sep 2026 12:06:33 +0100 Subject: [PATCH 08/12] Tweak to method names when static's delegate Make it clearer the connection between the two --- .../main/classes/actions/ApplyDiscount.cls | 6 +++--- .../main/classes/actions/CreateInvoice.cls | 6 +++--- .../controllers/OpportunityApplyDiscountController.cls | 6 +++--- .../controllers/OpportunityCreateInvoiceController.cls | 6 +++--- .../classes/controllers/OpportunityInvoicingController.cls | 6 +++--- .../test/classes/actions/ApplyDiscountTest.cls | 2 +- .../test/classes/actions/CreateInvoiceTest.cls | 2 +- .../controllers/OpportunityApplyDiscountControllerTest.cls | 2 +- .../controllers/OpportunityCreateInvoiceControllerTest.cls | 2 +- .../controllers/OpportunityInvoicingControllerTest.cls | 2 +- 10 files changed, 20 insertions(+), 20 deletions(-) diff --git a/sfdx-source/apex-common-samplecode/main/classes/actions/ApplyDiscount.cls b/sfdx-source/apex-common-samplecode/main/classes/actions/ApplyDiscount.cls index 564ece7..5040a5b 100644 --- a/sfdx-source/apex-common-samplecode/main/classes/actions/ApplyDiscount.cls +++ b/sfdx-source/apex-common-samplecode/main/classes/actions/ApplyDiscount.cls @@ -60,15 +60,15 @@ public inherited sharing class ApplyDiscount { category='Opportunities' ) public static List execute(List requests) { - return new ApplyDiscount().run(requests); + return new ApplyDiscount().handleExecute(requests); } /** - * Instance entry used by {@link #execute} and by callers that inject a service. + * Instance entry for {@link #execute}, used by callers that inject a service. * @param requests Opportunity Id and discount percent per request. Cannot be null. * @return Opportunity Ids in the same order as {@code requests}. */ - public List run(List requests) { + public List handleExecute(List requests) { // Build the map of Opportunity Ids and discount percentages. Map discountByOpportunityId = new Map(); diff --git a/sfdx-source/apex-common-samplecode/main/classes/actions/CreateInvoice.cls b/sfdx-source/apex-common-samplecode/main/classes/actions/CreateInvoice.cls index 35744d8..95184de 100644 --- a/sfdx-source/apex-common-samplecode/main/classes/actions/CreateInvoice.cls +++ b/sfdx-source/apex-common-samplecode/main/classes/actions/CreateInvoice.cls @@ -63,15 +63,15 @@ public inherited sharing class CreateInvoice { category='Opportunities' ) public static List execute(List requests) { - return new CreateInvoice().run(requests); + return new CreateInvoice().handleExecute(requests); } /** - * Instance entry used by {@link #execute} and by callers that inject collaborators. + * Instance entry for {@link #execute}, used by callers that inject collaborators. * @param requests Opportunity Id and optional discount percent per request. Cannot be null. * @return Invoice Id and Name in the same order as {@code requests}. */ - public List run(List requests) { + public List handleExecute(List requests) { // Build the map of Opportunity Ids and discount percentages. Map discountByOpportunityId = new Map(); diff --git a/sfdx-source/apex-common-samplecode/main/classes/controllers/OpportunityApplyDiscountController.cls b/sfdx-source/apex-common-samplecode/main/classes/controllers/OpportunityApplyDiscountController.cls index 01f4249..be411e1 100644 --- a/sfdx-source/apex-common-samplecode/main/classes/controllers/OpportunityApplyDiscountController.cls +++ b/sfdx-source/apex-common-samplecode/main/classes/controllers/OpportunityApplyDiscountController.cls @@ -60,15 +60,15 @@ public inherited sharing class OpportunityApplyDiscountController @AuraEnabled(cacheable=false) public static void applyDiscount(Id opportunityId, Decimal discountPercentage) { - new OpportunityApplyDiscountController().apply(opportunityId, discountPercentage); + new OpportunityApplyDiscountController().handleApplyDiscount(opportunityId, discountPercentage); } /** - * Instance entry used by {@link #applyDiscount} and by callers that inject a service. + * Instance entry for {@link #applyDiscount}, used by callers that inject a service. * @param opportunityId Opportunity Id to discount. Cannot be null. * @param discountPercentage Discount percent to apply (for example 10 for 10%). */ - public void apply(Id opportunityId, Decimal discountPercentage) + public void handleApplyDiscount(Id opportunityId, Decimal discountPercentage) { try { diff --git a/sfdx-source/apex-common-samplecode/main/classes/controllers/OpportunityCreateInvoiceController.cls b/sfdx-source/apex-common-samplecode/main/classes/controllers/OpportunityCreateInvoiceController.cls index 455de45..8003175 100644 --- a/sfdx-source/apex-common-samplecode/main/classes/controllers/OpportunityCreateInvoiceController.cls +++ b/sfdx-source/apex-common-samplecode/main/classes/controllers/OpportunityCreateInvoiceController.cls @@ -61,16 +61,16 @@ public inherited sharing class OpportunityCreateInvoiceController @AuraEnabled(cacheable=false) public static Id createInvoice(Id opportunityId, Decimal discountPercentage) { - return new OpportunityCreateInvoiceController().create(opportunityId, discountPercentage); + return new OpportunityCreateInvoiceController().handleCreateInvoice(opportunityId, discountPercentage); } /** - * Instance entry used by {@link #createInvoice} and by callers that inject a service. + * Instance entry for {@link #createInvoice}, used by callers that inject a service. * @param opportunityId Opportunity Id to invoice. Cannot be null. * @param discountPercentage Optional discount applied before invoicing when greater than zero. * @return Id of the first created Invoice__c. */ - public Id create(Id opportunityId, Decimal discountPercentage) + public Id handleCreateInvoice(Id opportunityId, Decimal discountPercentage) { try { diff --git a/sfdx-source/apex-common-samplecode/main/classes/controllers/OpportunityInvoicingController.cls b/sfdx-source/apex-common-samplecode/main/classes/controllers/OpportunityInvoicingController.cls index 760bfc7..b68afd8 100644 --- a/sfdx-source/apex-common-samplecode/main/classes/controllers/OpportunityInvoicingController.cls +++ b/sfdx-source/apex-common-samplecode/main/classes/controllers/OpportunityInvoicingController.cls @@ -58,14 +58,14 @@ public inherited sharing class OpportunityInvoicingController @AuraEnabled(cacheable=false) public static Id startJob() { - return new OpportunityInvoicingController().start(); + return new OpportunityInvoicingController().handleStartJob(); } /** - * Instance entry used by {@link #startJob} and by callers that inject a service. + * Instance entry for {@link #startJob}, used by callers that inject a service. * @return Async Apex job Id returned after a successful submit. */ - public Id start() + public Id handleStartJob() { try { diff --git a/sfdx-source/apex-common-samplecode/test/classes/actions/ApplyDiscountTest.cls b/sfdx-source/apex-common-samplecode/test/classes/actions/ApplyDiscountTest.cls index 3ad0d72..417cb7b 100644 --- a/sfdx-source/apex-common-samplecode/test/classes/actions/ApplyDiscountTest.cls +++ b/sfdx-source/apex-common-samplecode/test/classes/actions/ApplyDiscountTest.cls @@ -40,7 +40,7 @@ private class ApplyDiscountTest { request.discountPercentage = 10; // When the invocable action is executed - List results = new ApplyDiscount(mockService).run( + List results = new ApplyDiscount(mockService).handleExecute( new List{ request } ); diff --git a/sfdx-source/apex-common-samplecode/test/classes/actions/CreateInvoiceTest.cls b/sfdx-source/apex-common-samplecode/test/classes/actions/CreateInvoiceTest.cls index bebd8df..fbd2b59 100644 --- a/sfdx-source/apex-common-samplecode/test/classes/actions/CreateInvoiceTest.cls +++ b/sfdx-source/apex-common-samplecode/test/classes/actions/CreateInvoiceTest.cls @@ -49,7 +49,7 @@ private class CreateInvoiceTest { request.discountPercentage = 10; // When the invocable action is executed - List results = new CreateInvoice(mockService, mockSelector).run( + List results = new CreateInvoice(mockService, mockSelector).handleExecute( new List{ request } ); diff --git a/sfdx-source/apex-common-samplecode/test/classes/controllers/OpportunityApplyDiscountControllerTest.cls b/sfdx-source/apex-common-samplecode/test/classes/controllers/OpportunityApplyDiscountControllerTest.cls index a47ebd8..b95ed26 100644 --- a/sfdx-source/apex-common-samplecode/test/classes/controllers/OpportunityApplyDiscountControllerTest.cls +++ b/sfdx-source/apex-common-samplecode/test/classes/controllers/OpportunityApplyDiscountControllerTest.cls @@ -38,7 +38,7 @@ private class OpportunityApplyDiscountControllerTest OpportunitiesService mockService = (OpportunitiesService) mocks.mock(OpportunitiesService.class); Id opportunityId = TestIdGenerator.generate(Opportunity.SObjectType); // When apply discount is invoked from the Lightning action - new OpportunityApplyDiscountController(mockService).apply(opportunityId, 10); + new OpportunityApplyDiscountController(mockService).handleApplyDiscount(opportunityId, 10); // Then the service is called with the opportunity id and discount percentage ((OpportunitiesService) mocks.verify(mockService)).applyDiscounts( diff --git a/sfdx-source/apex-common-samplecode/test/classes/controllers/OpportunityCreateInvoiceControllerTest.cls b/sfdx-source/apex-common-samplecode/test/classes/controllers/OpportunityCreateInvoiceControllerTest.cls index f9d9f59..7667953 100644 --- a/sfdx-source/apex-common-samplecode/test/classes/controllers/OpportunityCreateInvoiceControllerTest.cls +++ b/sfdx-source/apex-common-samplecode/test/classes/controllers/OpportunityCreateInvoiceControllerTest.cls @@ -42,7 +42,7 @@ private class OpportunityCreateInvoiceControllerTest { mocks.when(mockService.applyDiscountsAndCreateInvoices(discountByOpportunityId)).thenReturn(invoiceIdByOpportunityId); mocks.stopStubbing(); // When create invoice is invoked from the Lightning action - Id resultId = new OpportunityCreateInvoiceController(mockService).create(opportunityId, 10); + Id resultId = new OpportunityCreateInvoiceController(mockService).handleCreateInvoice(opportunityId, 10); // Then the service is called and the invoice id is returned ((OpportunitiesService) mocks.verify(mockService)).applyDiscountsAndCreateInvoices( diff --git a/sfdx-source/apex-common-samplecode/test/classes/controllers/OpportunityInvoicingControllerTest.cls b/sfdx-source/apex-common-samplecode/test/classes/controllers/OpportunityInvoicingControllerTest.cls index 52bf737..e6d928f 100644 --- a/sfdx-source/apex-common-samplecode/test/classes/controllers/OpportunityInvoicingControllerTest.cls +++ b/sfdx-source/apex-common-samplecode/test/classes/controllers/OpportunityInvoicingControllerTest.cls @@ -39,7 +39,7 @@ private class OpportunityInvoicingControllerTest { mocks.when(mockService.submitInvoicingJob()).thenReturn(jobId); mocks.stopStubbing(); // When start job is invoked from the Lightning action - Id resultId = new OpportunityInvoicingController(mockService).start(); + Id resultId = new OpportunityInvoicingController(mockService).handleStartJob(); // Then the service is called and the job id is returned ((OpportunitiesService) mocks.verify(mockService)).submitInvoicingJob(); From 01289b308ed045e6450b10ff7eba49e0e0b12aa0 Mon Sep 17 00:00:00 2001 From: Andrew Fawcett Date: Tue, 8 Sep 2026 12:23:48 +0100 Subject: [PATCH 09/12] Tweaked naming of service instance variables 'service' to 'opportunitiesSvc' improves readability of the code, e.g. opportunitiesSvc.applyDiscountsAndCreateInvoices --- .../main/classes/actions/ApplyDiscount.cls | 10 +++++----- .../main/classes/actions/CreateInvoice.cls | 10 +++++----- .../controllers/OpportunityApplyDiscountController.cls | 10 +++++----- .../OpportunityApplyDiscountsController.cls | 10 +++++----- .../controllers/OpportunityCreateInvoiceController.cls | 10 +++++----- .../OpportunityCreateInvoicesController.cls | 10 +++++----- .../controllers/OpportunityInvoicingController.cls | 10 +++++----- 7 files changed, 35 insertions(+), 35 deletions(-) diff --git a/sfdx-source/apex-common-samplecode/main/classes/actions/ApplyDiscount.cls b/sfdx-source/apex-common-samplecode/main/classes/actions/ApplyDiscount.cls index 5040a5b..0b7f96a 100644 --- a/sfdx-source/apex-common-samplecode/main/classes/actions/ApplyDiscount.cls +++ b/sfdx-source/apex-common-samplecode/main/classes/actions/ApplyDiscount.cls @@ -32,7 +32,7 @@ * Use the service constructor when composing deliberately (including tests). */ public inherited sharing class ApplyDiscount { - private final OpportunitiesService service; + private final OpportunitiesService opportunitiesSvc; /** * Default composition: {@link OpportunitiesService#newInstance}. @@ -43,10 +43,10 @@ public inherited sharing class ApplyDiscount { /** * Creates an action with the given service. - * @param service Opportunities service used to apply discounts. Cannot be null. + * @param opportunitiesSvc Opportunities service used to apply discounts. Cannot be null. */ - public ApplyDiscount(OpportunitiesService service) { - this.service = service; + public ApplyDiscount(OpportunitiesService opportunitiesSvc) { + this.opportunitiesSvc = opportunitiesSvc; } /** @@ -77,7 +77,7 @@ public inherited sharing class ApplyDiscount { } // Apply discounts to the Opportunities. - service.applyDiscounts(discountByOpportunityId); + opportunitiesSvc.applyDiscounts(discountByOpportunityId); // Build the results. List results = new List(); diff --git a/sfdx-source/apex-common-samplecode/main/classes/actions/CreateInvoice.cls b/sfdx-source/apex-common-samplecode/main/classes/actions/CreateInvoice.cls index 95184de..dd4ba3d 100644 --- a/sfdx-source/apex-common-samplecode/main/classes/actions/CreateInvoice.cls +++ b/sfdx-source/apex-common-samplecode/main/classes/actions/CreateInvoice.cls @@ -32,7 +32,7 @@ * Use the constructor when composing deliberately (including tests). */ public inherited sharing class CreateInvoice { - private final OpportunitiesService service; + private final OpportunitiesService opportunitiesSvc; private final InvoicesSelector selector; /** @@ -44,11 +44,11 @@ public inherited sharing class CreateInvoice { /** * Creates an action with the given service and selector. - * @param service Opportunities service used to create invoices. Cannot be null. + * @param opportunitiesSvc Opportunities service used to create invoices. Cannot be null. * @param selector Selector used to load created invoice names. Cannot be null. */ - public CreateInvoice(OpportunitiesService service, InvoicesSelector selector) { - this.service = service; + public CreateInvoice(OpportunitiesService opportunitiesSvc, InvoicesSelector selector) { + this.opportunitiesSvc = opportunitiesSvc; this.selector = selector; } @@ -81,7 +81,7 @@ public inherited sharing class CreateInvoice { // Apply discounts and create invoices. Map invoiceIdByOpportunityId = - service.applyDiscountsAndCreateInvoices(discountByOpportunityId); + opportunitiesSvc.applyDiscountsAndCreateInvoices(discountByOpportunityId); Map invoiceNameById = invoiceNamesById(invoiceIdByOpportunityId.values()); // Build the results. diff --git a/sfdx-source/apex-common-samplecode/main/classes/controllers/OpportunityApplyDiscountController.cls b/sfdx-source/apex-common-samplecode/main/classes/controllers/OpportunityApplyDiscountController.cls index be411e1..d4db899 100644 --- a/sfdx-source/apex-common-samplecode/main/classes/controllers/OpportunityApplyDiscountController.cls +++ b/sfdx-source/apex-common-samplecode/main/classes/controllers/OpportunityApplyDiscountController.cls @@ -33,7 +33,7 @@ */ public inherited sharing class OpportunityApplyDiscountController { - private final OpportunitiesService service; + private final OpportunitiesService opportunitiesSvc; /** * Default composition: {@link OpportunitiesService#newInstance}. @@ -45,11 +45,11 @@ public inherited sharing class OpportunityApplyDiscountController /** * Creates a controller with the given service. - * @param service Opportunities service used to apply discounts. Cannot be null. + * @param opportunitiesSvc Opportunities service used to apply discounts. Cannot be null. */ - public OpportunityApplyDiscountController(OpportunitiesService service) + public OpportunityApplyDiscountController(OpportunitiesService opportunitiesSvc) { - this.service = service; + this.opportunitiesSvc = opportunitiesSvc; } /** @@ -72,7 +72,7 @@ public inherited sharing class OpportunityApplyDiscountController { try { - service.applyDiscounts( + opportunitiesSvc.applyDiscounts( new Map{ opportunityId => discountPercentage } ); } diff --git a/sfdx-source/apex-common-samplecode/main/classes/controllers/OpportunityApplyDiscountsController.cls b/sfdx-source/apex-common-samplecode/main/classes/controllers/OpportunityApplyDiscountsController.cls index bb714c2..4374ba7 100644 --- a/sfdx-source/apex-common-samplecode/main/classes/controllers/OpportunityApplyDiscountsController.cls +++ b/sfdx-source/apex-common-samplecode/main/classes/controllers/OpportunityApplyDiscountsController.cls @@ -38,7 +38,7 @@ public inherited sharing class OpportunityApplyDiscountsController { public Decimal discountPercentage { get; set; } private ApexPages.StandardSetController standardSetController; - private final OpportunitiesService service; + private final OpportunitiesService opportunitiesSvc; /** * Default composition: {@link OpportunitiesService#newInstance}. @@ -51,14 +51,14 @@ public inherited sharing class OpportunityApplyDiscountsController { /** * Creates a controller with the given StandardSetController and service. * @param controller StandardSetController with selected Opportunities. Cannot be null. - * @param service Opportunities service used to apply discounts. Cannot be null. + * @param opportunitiesSvc Opportunities service used to apply discounts. Cannot be null. */ public OpportunityApplyDiscountsController( ApexPages.StandardSetController controller, - OpportunitiesService service + OpportunitiesService opportunitiesSvc ) { this.standardSetController = controller; - this.service = service; + this.opportunitiesSvc = opportunitiesSvc; } /** @@ -73,7 +73,7 @@ public inherited sharing class OpportunityApplyDiscountsController { for (Id opportunityId : selectedOpportunityIds) { discountByOpportunityId.put(opportunityId, discountPercentage); } - service.applyDiscounts(discountByOpportunityId); + opportunitiesSvc.applyDiscounts(discountByOpportunityId); ApexPages.addMessage(new ApexPages.Message(ApexPages.Severity.INFO, 'Opportunities updated.')); } catch (Exception e) { ApexPages.addMessages(e); diff --git a/sfdx-source/apex-common-samplecode/main/classes/controllers/OpportunityCreateInvoiceController.cls b/sfdx-source/apex-common-samplecode/main/classes/controllers/OpportunityCreateInvoiceController.cls index 8003175..fb146f1 100644 --- a/sfdx-source/apex-common-samplecode/main/classes/controllers/OpportunityCreateInvoiceController.cls +++ b/sfdx-source/apex-common-samplecode/main/classes/controllers/OpportunityCreateInvoiceController.cls @@ -33,7 +33,7 @@ */ public inherited sharing class OpportunityCreateInvoiceController { - private final OpportunitiesService service; + private final OpportunitiesService opportunitiesSvc; /** * Default composition: {@link OpportunitiesService#newInstance}. @@ -45,11 +45,11 @@ public inherited sharing class OpportunityCreateInvoiceController /** * Creates a controller with the given service. - * @param service Opportunities service used to create invoices. Cannot be null. + * @param opportunitiesSvc Opportunities service used to create invoices. Cannot be null. */ - public OpportunityCreateInvoiceController(OpportunitiesService service) + public OpportunityCreateInvoiceController(OpportunitiesService opportunitiesSvc) { - this.service = service; + this.opportunitiesSvc = opportunitiesSvc; } /** @@ -74,7 +74,7 @@ public inherited sharing class OpportunityCreateInvoiceController { try { - Map invoiceIdByOpportunityId = service.applyDiscountsAndCreateInvoices( + Map invoiceIdByOpportunityId = opportunitiesSvc.applyDiscountsAndCreateInvoices( new Map{ opportunityId => discountPercentage } ); return invoiceIdByOpportunityId.get(opportunityId); diff --git a/sfdx-source/apex-common-samplecode/main/classes/controllers/OpportunityCreateInvoicesController.cls b/sfdx-source/apex-common-samplecode/main/classes/controllers/OpportunityCreateInvoicesController.cls index 7355b70..7b8b05d 100644 --- a/sfdx-source/apex-common-samplecode/main/classes/controllers/OpportunityCreateInvoicesController.cls +++ b/sfdx-source/apex-common-samplecode/main/classes/controllers/OpportunityCreateInvoicesController.cls @@ -39,7 +39,7 @@ public inherited sharing class OpportunityCreateInvoicesController { public Decimal discountPercentage { get; set; } private ApexPages.StandardSetController standardSetController; - private final OpportunitiesService service; + private final OpportunitiesService opportunitiesSvc; /** * Default composition: {@link OpportunitiesService#newInstance}. @@ -52,14 +52,14 @@ public inherited sharing class OpportunityCreateInvoicesController { /** * Creates a controller with the given StandardSetController and service. * @param controller StandardSetController with selected Opportunities. Cannot be null. - * @param service Opportunities service used to create invoices. Cannot be null. + * @param opportunitiesSvc Opportunities service used to create invoices. Cannot be null. */ public OpportunityCreateInvoicesController( ApexPages.StandardSetController controller, - OpportunitiesService service + OpportunitiesService opportunitiesSvc ) { this.standardSetController = controller; - this.service = service; + this.opportunitiesSvc = opportunitiesSvc; } /** @@ -74,7 +74,7 @@ public inherited sharing class OpportunityCreateInvoicesController { for (Id opportunityId : selectedOpportunityIds) { discountByOpportunityId.put(opportunityId, discountPercentage); } - service.applyDiscountsAndCreateInvoices(discountByOpportunityId); + opportunitiesSvc.applyDiscountsAndCreateInvoices(discountByOpportunityId); ApexPages.addMessage(new ApexPages.Message(ApexPages.Severity.INFO, 'Invoices created.')); } catch (Exception e) { ApexPages.addMessages(e); diff --git a/sfdx-source/apex-common-samplecode/main/classes/controllers/OpportunityInvoicingController.cls b/sfdx-source/apex-common-samplecode/main/classes/controllers/OpportunityInvoicingController.cls index b68afd8..e8e7301 100644 --- a/sfdx-source/apex-common-samplecode/main/classes/controllers/OpportunityInvoicingController.cls +++ b/sfdx-source/apex-common-samplecode/main/classes/controllers/OpportunityInvoicingController.cls @@ -32,7 +32,7 @@ */ public inherited sharing class OpportunityInvoicingController { - private final OpportunitiesService service; + private final OpportunitiesService opportunitiesSvc; /** * Default composition: {@link OpportunitiesService#newInstance}. @@ -44,11 +44,11 @@ public inherited sharing class OpportunityInvoicingController /** * Creates a controller with the given service. - * @param service Opportunities service used to submit the job. Cannot be null. + * @param opportunitiesSvc Opportunities service used to submit the job. Cannot be null. */ - public OpportunityInvoicingController(OpportunitiesService service) + public OpportunityInvoicingController(OpportunitiesService opportunitiesSvc) { - this.service = service; + this.opportunitiesSvc = opportunitiesSvc; } /** @@ -69,7 +69,7 @@ public inherited sharing class OpportunityInvoicingController { try { - return service.submitInvoicingJob(); + return opportunitiesSvc.submitInvoicingJob(); } catch (Exception e) { From 038df7e9b1050e44e9d57ba602ffe7b59a6bd14a Mon Sep 17 00:00:00 2001 From: Andrew Fawcett Date: Tue, 8 Sep 2026 14:00:48 +0100 Subject: [PATCH 10/12] Returned to service method scoped unit of work --- README.md | 4 ++-- .../main/classes/UnitOfWork.cls | 17 ++++++++------ .../main/classes/service/AccountsService.cls | 20 +++++++--------- .../main/classes/service/InvoicingService.cls | 20 ++++------------ .../classes/service/OpportunitiesService.cls | 23 ++++++++----------- .../classes/service/AccountsServiceTest.cls | 4 +++- .../service/OpportunitiesServiceTest.cls | 3 ++- 7 files changed, 40 insertions(+), 51 deletions(-) diff --git a/README.md b/README.md index e99421e..2722ddd 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,7 @@ This repository contains a sample application illustrating the Apex Enterprise P Architecture Notes ------------------ -This sample uses **concrete** Domain, Selector, and Service classes. Constructors take collaborators; `newInstance()` is the default composition. Prefer `X.newInstance()` at entry points over `new X()`. Use the constructor to inject mocks (during Apex Tests) or to compose deliberately (for example one Unit of Work passed to two services). `X.newInstance()` is also the single place a later metadata-driven factory would resolve which service, selector, or domain implementation to construct. +This sample uses **concrete** Domain, Selector, and Service classes. Constructors take collaborators; `newInstance()` is the default composition. Prefer `X.newInstance()` at entry points over `new X()`. Use the constructor to inject mocks (during Apex Tests) or to compose deliberately. Service methods that persist call `UnitOfWork.newInstance()` so each method gets a fresh Unit of Work; tests set `UnitOfWork.mock`. `X.newInstance()` is also the single place a later metadata-driven factory would resolve which service, selector, or domain implementation to construct. Domains wrap records, so they are constructed when those records are in hand — including mid-method, as when `Opportunities.applyDiscounts` builds `OpportunityLineItems`. Domain `newInstance(records)` keeps a `@TestVisible` mock for that case. This sample no longer includes an `Application` factory; that is reserved for more advanced DI, such as via [AT4DX](https://github.com/apex-enterprise-patterns/at4dx). @@ -39,7 +39,7 @@ Domains wrap records, so they are constructed when those records are in hand — | **Services** | `OpportunitiesService`, `InvoicingService`, `AccountsService` — orchestrate selectors, domains, and Unit of Work | | **Domains** | `Opportunities`, `OpportunityLineItems`, `Accounts` — record behaviour (discounting, invoice DTOs) on `fflib_SObjects`. Constructed via `newInstance(records)` when the records are in hand; not trigger lifecycle | | **Trigger handlers** | `OpportunitiesTriggerHandler` — `fflib_SObjectDomain` trigger lifecycle (defaults, validation, related updates) | -| **UnitOfWork** | `UnitOfWork.cls` — thin factory returning `fflib_SObjectUnitOfWork` with `UserModeDML()` | +| **UnitOfWork** | `UnitOfWork.cls` — thin factory; each service method calls `newInstance()` for a fresh UoW; tests set `UnitOfWork.mock` | | **InvoicingTargetsRegistry** | Resolves invoice targets from `InvoiceTargets__mdt` at runtime | User Mode and CRUD/FLS diff --git a/sfdx-source/apex-common-samplecode/main/classes/UnitOfWork.cls b/sfdx-source/apex-common-samplecode/main/classes/UnitOfWork.cls index 553a6b5..5da7b71 100644 --- a/sfdx-source/apex-common-samplecode/main/classes/UnitOfWork.cls +++ b/sfdx-source/apex-common-samplecode/main/classes/UnitOfWork.cls @@ -27,21 +27,24 @@ /** * Thin Unit of Work factory for the sample app. No Application class. * - * This sample uses one app-level registry of {@code SObjectType} values so a caller can - * construct one Unit of Work and pass it into multiple service constructors in the same - * transaction. Service methods still commit when they own the complete use case. + * Service methods call {@link #newInstance} so each persist operation gets a fresh + * Unit of Work. The type list is the app-level registry of SObject types. * - * An alternative is per-service factories with {@code newInstance(List types)} - * for a narrower scope per call site. That can reduce the central type list but makes it - * harder to pass one Unit of Work between services. + * Tests set {@link #mock} to substitute the instance that {@link #newInstance} returns. */ public inherited sharing class UnitOfWork { + @TestVisible + private static fflib_SObjectUnitOfWork mock; + /** - * Returns a Unit of Work registered for this app's types. + * Returns a Unit of Work registered for this app's types, or {@link #mock} when tests have set one. * DML runs in user mode. * @return Unit of Work instance. */ public static fflib_SObjectUnitOfWork newInstance() { + if (mock != null) { + return mock; + } return new fflib_SObjectUnitOfWork( new List{ Account.SObjectType, diff --git a/sfdx-source/apex-common-samplecode/main/classes/service/AccountsService.cls b/sfdx-source/apex-common-samplecode/main/classes/service/AccountsService.cls index 4bc4be6..56cd0f4 100644 --- a/sfdx-source/apex-common-samplecode/main/classes/service/AccountsService.cls +++ b/sfdx-source/apex-common-samplecode/main/classes/service/AccountsService.cls @@ -27,35 +27,30 @@ /** * Service for Account-related operations invoked from the service layer. * - * Prefer {@link #newInstance} for the default selector and Unit of Work. - * Use the constructor when tests inject mocks or a caller composes deliberately. + * Prefer {@link #newInstance} for the default selector. + * Use the constructor when tests inject a selector mock. + * Each method that persists creates a fresh {@link UnitOfWork#newInstance}. */ public inherited sharing class AccountsService { private final AccountsSelector selector; - private final fflib_SObjectUnitOfWork uow; /** - * Default composition: {@link AccountsSelector#newInstance} and {@link UnitOfWork#newInstance}. + * Default composition: {@link AccountsSelector#newInstance}. * @return {@link AccountsService} instance. */ public static AccountsService newInstance() { - return new AccountsService( - AccountsSelector.newInstance(), - UnitOfWork.newInstance() - ); + return new AccountsService(AccountsSelector.newInstance()); } /** - * Creates a service with the given selector and Unit of Work. + * Creates a service with the given selector. * @param selector Account selector used to load records. Cannot be null. - * @param uow Unit of Work used to register and commit changes. Cannot be null. */ - public AccountsService(AccountsSelector selector, fflib_SObjectUnitOfWork uow) + public AccountsService(AccountsSelector selector) { this.selector = selector; - this.uow = uow; } /** @@ -69,6 +64,7 @@ public inherited sharing class AccountsService return; } + fflib_SObjectUnitOfWork uow = UnitOfWork.newInstance(); List accountRecords = selector.selectById(accountIds); Accounts accountsDomain = Accounts.newInstance(accountRecords); accountsDomain.updateOpportunityActivity(); diff --git a/sfdx-source/apex-common-samplecode/main/classes/service/InvoicingService.cls b/sfdx-source/apex-common-samplecode/main/classes/service/InvoicingService.cls index a5b5311..23fbb45 100644 --- a/sfdx-source/apex-common-samplecode/main/classes/service/InvoicingService.cls +++ b/sfdx-source/apex-common-samplecode/main/classes/service/InvoicingService.cls @@ -28,29 +28,18 @@ * Polymorphic invoicing service that generates invoices from registered source SObject types * via {@link InvoicingTargetsRegistry} and domains implementing {@link ISupportInvoicing}. * - * Prefer {@link #newInstance} for the default Unit of Work. - * Use the constructor when tests inject mocks or a caller composes deliberately. + * Prefer {@link #newInstance} for the default composition. + * Each method that persists creates a fresh {@link UnitOfWork#newInstance}. */ public inherited sharing class InvoicingService { - private final fflib_SObjectUnitOfWork uow; - /** - * Default composition: {@link UnitOfWork#newInstance}. + * Default composition. * @return {@link InvoicingService} instance. */ public static InvoicingService newInstance() { - return new InvoicingService(UnitOfWork.newInstance()); - } - - /** - * Creates a service with the given Unit of Work. - * @param uow Unit of Work used to register and commit invoices. Cannot be null. - */ - public InvoicingService(fflib_SObjectUnitOfWork uow) - { - this.uow = uow; + return new InvoicingService(); } /** @@ -148,6 +137,7 @@ public inherited sharing class InvoicingService { try { + fflib_SObjectUnitOfWork uow = UnitOfWork.newInstance(); InvoiceFactory invoiceFactory = new InvoiceFactory(uow); List records = InvoicingTargetsRegistry.selectById(new Set(sourceRecordIds)); Object domain = InvoicingTargetsRegistry.newDomain(records); diff --git a/sfdx-source/apex-common-samplecode/main/classes/service/OpportunitiesService.cls b/sfdx-source/apex-common-samplecode/main/classes/service/OpportunitiesService.cls index 71a6a28..df4e3ef 100644 --- a/sfdx-source/apex-common-samplecode/main/classes/service/OpportunitiesService.cls +++ b/sfdx-source/apex-common-samplecode/main/classes/service/OpportunitiesService.cls @@ -27,36 +27,30 @@ /** * Service for Opportunity discounting, invoice creation, and invoicing job submission. * - * Prefer {@link #newInstance} for the default selector and Unit of Work. - * Use the constructor when tests inject mocks or a caller composes deliberately - * (for example one Unit of Work shared with another service). + * Prefer {@link #newInstance} for the default selector. + * Use the constructor when tests inject a selector mock. + * Each method that persists creates a fresh {@link UnitOfWork#newInstance}. */ public inherited sharing class OpportunitiesService { private final OpportunitiesSelector selector; - private final fflib_SObjectUnitOfWork uow; /** - * Default composition: {@link OpportunitiesSelector#newInstance} and {@link UnitOfWork#newInstance}. + * Default composition: {@link OpportunitiesSelector#newInstance}. * @return {@link OpportunitiesService} instance. */ public static OpportunitiesService newInstance() { - return new OpportunitiesService( - OpportunitiesSelector.newInstance(), - UnitOfWork.newInstance() - ); + return new OpportunitiesService(OpportunitiesSelector.newInstance()); } /** - * Creates a service with the given selector and Unit of Work. + * Creates a service with the given selector. * @param selector Opportunity selector used to load records. Cannot be null. - * @param uow Unit of Work used to register and commit changes. Cannot be null. */ - public OpportunitiesService(OpportunitiesSelector selector, fflib_SObjectUnitOfWork uow) + public OpportunitiesService(OpportunitiesSelector selector) { this.selector = selector; - this.uow = uow; } /** @@ -72,6 +66,7 @@ public inherited sharing class OpportunitiesService } try { + fflib_SObjectUnitOfWork uow = UnitOfWork.newInstance(); List opportunitiesWithProducts = selector.selectByIdWithProducts(discountByOpportunityId.keySet()); Opportunities opportunities = Opportunities.newInstance(opportunitiesWithProducts); opportunities.applyDiscounts(discountByOpportunityId, uow); @@ -98,6 +93,8 @@ public inherited sharing class OpportunitiesService } try { + fflib_SObjectUnitOfWork uow = UnitOfWork.newInstance(); + // Select the Opportunities and apply discounts. Opportunities opps = Opportunities.newInstance(selector.selectByIdWithProducts(discountByOpportunityId.keySet())); opps.applyDiscounts(discountByOpportunityId, uow); diff --git a/sfdx-source/apex-common-samplecode/test/classes/service/AccountsServiceTest.cls b/sfdx-source/apex-common-samplecode/test/classes/service/AccountsServiceTest.cls index 49783d5..cccfe2b 100644 --- a/sfdx-source/apex-common-samplecode/test/classes/service/AccountsServiceTest.cls +++ b/sfdx-source/apex-common-samplecode/test/classes/service/AccountsServiceTest.cls @@ -57,8 +57,10 @@ private class AccountsServiceTest mocks.when(selectorMock.selectById(accountIds)).thenReturn(accounts); mocks.stopStubbing(); + UnitOfWork.mock = unitOfWorkMock; + Test.startTest(); - new AccountsService(selectorMock, unitOfWorkMock).updateOpportunityActivity(accountIds); + new AccountsService(selectorMock).updateOpportunityActivity(accountIds); Test.stopTest(); ((AccountsSelector) mocks.verify(selectorMock)).selectById(accountIds); diff --git a/sfdx-source/apex-common-samplecode/test/classes/service/OpportunitiesServiceTest.cls b/sfdx-source/apex-common-samplecode/test/classes/service/OpportunitiesServiceTest.cls index f276fc0..9c9f869 100644 --- a/sfdx-source/apex-common-samplecode/test/classes/service/OpportunitiesServiceTest.cls +++ b/sfdx-source/apex-common-samplecode/test/classes/service/OpportunitiesServiceTest.cls @@ -56,6 +56,7 @@ private class OpportunitiesServiceTest mocks.when(selectorMock.selectByIdWithProducts(testOppsSet)).thenReturn(testOppsList); mocks.stopStubbing(); Opportunities.mock = domainMock; + UnitOfWork.mock = uowMock; // When discounts are applied through the service Map discountByOpportunityId = new Map(); @@ -63,7 +64,7 @@ private class OpportunitiesServiceTest { discountByOpportunityId.put(opportunityId, 10); } - new OpportunitiesService(selectorMock, uowMock).applyDiscounts(discountByOpportunityId); + new OpportunitiesService(selectorMock).applyDiscounts(discountByOpportunityId); // Then the selector, domain, and unit of work are used ((OpportunitiesSelector) mocks.verify(selectorMock)).selectByIdWithProducts(testOppsSet); From 9d21d307f08f4c1762ca64954a6f9845957e9777 Mon Sep 17 00:00:00 2001 From: Andrew Fawcett Date: Thu, 1 Oct 2026 14:53:01 +0100 Subject: [PATCH 11/12] Addressed further feedback on framing of invoice service factory and uow sample --- README.md | 21 +++++++--- .../main/classes/UnitOfWork.cls | 40 ++++++++++++++----- .../main/classes/service/InvoicingService.cls | 9 +++-- .../InvoicingTargetsRegistry.cls | 17 +++++--- .../InvoicingTargetsRegistry.cls-meta.xml | 0 5 files changed, 64 insertions(+), 23 deletions(-) rename sfdx-source/apex-common-samplecode/main/classes/{ => service}/InvoicingTargetsRegistry.cls (86%) rename sfdx-source/apex-common-samplecode/main/classes/{ => service}/InvoicingTargetsRegistry.cls-meta.xml (100%) diff --git a/README.md b/README.md index 2722ddd..fb22586 100644 --- a/README.md +++ b/README.md @@ -30,17 +30,26 @@ This repository contains a sample application illustrating the Apex Enterprise P Architecture Notes ------------------ -This sample uses **concrete** Domain, Selector, and Service classes. Constructors take collaborators; `newInstance()` is the default composition. Prefer `X.newInstance()` at entry points over `new X()`. Use the constructor to inject mocks (during Apex Tests) or to compose deliberately. Service methods that persist call `UnitOfWork.newInstance()` so each method gets a fresh Unit of Work; tests set `UnitOfWork.mock`. `X.newInstance()` is also the single place a later metadata-driven factory would resolve which service, selector, or domain implementation to construct. - -Domains wrap records, so they are constructed when those records are in hand — including mid-method, as when `Opportunities.applyDiscounts` builds `OpportunityLineItems`. Domain `newInstance(records)` keeps a `@TestVisible` mock for that case. This sample no longer includes an `Application` factory; that is reserved for more advanced DI, such as via [AT4DX](https://github.com/apex-enterprise-patterns/at4dx). +This sample uses **concrete** Domain, Selector, and Service classes. `newInstance()` is the default composition: it calls the public constructor with the usual collaborators. Prefer `X.newInstance()` over `new X()` when constructing a service, selector, or domain. Use the constructor to inject mocks in tests or to compose deliberately. Constructors stay public so the [Apex Stub API](https://developer.salesforce.com/docs/atlas.en-us.apexcode.meta/apexcode/apex_testing_stub_api.htm) can mock the class (`Test.createStub` cannot stub a type that has only private constructors) and so tests can pass collaborators in. | Component | Role | |-----------|------| | **Services** | `OpportunitiesService`, `InvoicingService`, `AccountsService` — orchestrate selectors, domains, and Unit of Work | | **Domains** | `Opportunities`, `OpportunityLineItems`, `Accounts` — record behaviour (discounting, invoice DTOs) on `fflib_SObjects`. Constructed via `newInstance(records)` when the records are in hand; not trigger lifecycle | | **Trigger handlers** | `OpportunitiesTriggerHandler` — `fflib_SObjectDomain` trigger lifecycle (defaults, validation, related updates) | -| **UnitOfWork** | `UnitOfWork.cls` — thin factory; each service method calls `newInstance()` for a fresh UoW; tests set `UnitOfWork.mock` | -| **InvoicingTargetsRegistry** | Resolves invoice targets from `InvoiceTargets__mdt` at runtime | +| **UnitOfWork** | `UnitOfWork.cls` — thin factory; no-arg `newInstance()` uses the sample type list; `newInstance(types)` for a narrower list; tests set `UnitOfWork.mock` | +| **InvoicingTargetsRegistry** | Example of reusing fflib selector/domain factories locally for `InvoicingService.generate` (`InvoiceTargets__mdt`); not an application factory | + +Exceptions and local factories +------------------------------ + +The constructor / `newInstance()` baseline above does not cover every type in that table. These notes are where composition differs, or where a local factory is used instead of an application-wide `Application` class: + +- This sample has no application-wide `Application` factory; that is reserved for more advanced DI, such as via [AT4DX](https://github.com/apex-enterprise-patterns/at4dx). +- Controllers and invocable actions have no `newInstance()` of their own: the platform calls a static `@AuraEnabled` / `@InvocableMethod`, or a Visualforce `StandardSetController` constructor. Those entry points compose `OpportunitiesService.newInstance()` internally; tests inject the service through the constructor. +- A service does not keep one Unit of Work for its lifetime. Each method that commits DML calls `UnitOfWork.newInstance()`, registers its work, and commits; a second call on the same service instance must not reuse a Unit of Work that has already committed. Tests set `UnitOfWork.mock`. `UnitOfWork.newInstance(List)` is there when a method should not take the whole sample type list. +- A domain wraps records you often do not have until you are already inside a method — for example `Opportunities.applyDiscounts` collects line items and then constructs `OpportunityLineItems`. That domain cannot be passed into the `Opportunities` constructor, so `newInstance(records)` is the factory and tests set the `@TestVisible` `mock` it returns. +- `InvoicingTargetsRegistry` is an example of reusing the fflib selector and domain factories in a more concrete way when one service (`InvoicingService.generate`) needs runtime type resolution. It reads `InvoiceTargets__mdt` and, for a set of source Ids, selects the records and constructs the domain that implements `ISupportInvoicing` (Opportunity, DeveloperWorkItem, TrainingWorkItem, or a further metadata row) so the service does not hard-code those types. User Mode and CRUD/FLS ---------------------- @@ -56,7 +65,7 @@ Tests that exercise USER_MODE code use `TestDataFactory` to create a Standard Us | Area | Approach | |------|----------| | **Selectors** | `super(false, fflib_SObjectSelector.DataAccess.USER_MODE)` in selector constructors (and `includeFieldSetFields` overload where present) | -| **UnitOfWork** | `UnitOfWork.cls` factory; uses `UserModeDML()` | +| **UnitOfWork** | `UnitOfWork.cls` factory (`newInstance()` / `newInstance(types)`); uses `UserModeDML()` | | **Tests** | `TestDataFactory`; `@TestSetup` + `System.runAs(getRunAsUser())` for USER_MODE tests | | **Permission set** | `ApexEnterprisePatternsSampleApp` grants field-level access for USER_MODE tests | diff --git a/sfdx-source/apex-common-samplecode/main/classes/UnitOfWork.cls b/sfdx-source/apex-common-samplecode/main/classes/UnitOfWork.cls index 5da7b71..9655a8f 100644 --- a/sfdx-source/apex-common-samplecode/main/classes/UnitOfWork.cls +++ b/sfdx-source/apex-common-samplecode/main/classes/UnitOfWork.cls @@ -28,24 +28,30 @@ * Thin Unit of Work factory for the sample app. No Application class. * * Service methods call {@link #newInstance} so each persist operation gets a fresh - * Unit of Work. The type list is the app-level registry of SObject types. + * Unit of Work. The no-arg overload uses this sample's SObject type list. * - * Tests set {@link #mock} to substitute the instance that {@link #newInstance} returns. + * That list is only {@code SObjectType} tokens, not Apex classes, so a compile error + * in a service, selector, or domain does not cascade through this factory the way + * a central {@code Application} class that referenced those types would. + * + * A single app-level list is fine at this sample's size. In a larger org (many + * custom objects, circular lookups) the insert order becomes hard to maintain + * and one list is a poor fit across functional domains. Prefer + * {@link #newInstance(List)} with the types that method actually registers. + * + * Tests set {@link #mock} to substitute the instance that either overload returns. */ public inherited sharing class UnitOfWork { @TestVisible private static fflib_SObjectUnitOfWork mock; /** - * Returns a Unit of Work registered for this app's types, or {@link #mock} when tests have set one. - * DML runs in user mode. + * Returns a Unit of Work registered for this sample's types, or {@link #mock} when tests have set one. + * DML runs in user mode. Type order is parent before child for related inserts. * @return Unit of Work instance. */ public static fflib_SObjectUnitOfWork newInstance() { - if (mock != null) { - return mock; - } - return new fflib_SObjectUnitOfWork( + return newInstance( new List{ Account.SObjectType, Opportunity.SObjectType, @@ -58,7 +64,23 @@ public inherited sharing class UnitOfWork { DeveloperWorkItem__c.SObjectType, TrainingWorkItem__c.SObjectType, DesignWorkItem__c.SObjectType - }, + } + ); + } + + /** + * Returns a Unit of Work registered for the given types, or {@link #mock} when tests have set one. + * Use this when a method should not take the whole sample type list. + * DML runs in user mode. List parent types before children. + * @param types SObject types this Unit of Work may register. Cannot be null. + * @return Unit of Work instance. + */ + public static fflib_SObjectUnitOfWork newInstance(List types) { + if (mock != null) { + return mock; + } + return new fflib_SObjectUnitOfWork( + types, new fflib_SObjectUnitOfWork.UserModeDML() ); } diff --git a/sfdx-source/apex-common-samplecode/main/classes/service/InvoicingService.cls b/sfdx-source/apex-common-samplecode/main/classes/service/InvoicingService.cls index 23fbb45..71f6922 100644 --- a/sfdx-source/apex-common-samplecode/main/classes/service/InvoicingService.cls +++ b/sfdx-source/apex-common-samplecode/main/classes/service/InvoicingService.cls @@ -28,6 +28,9 @@ * Polymorphic invoicing service that generates invoices from registered source SObject types * via {@link InvoicingTargetsRegistry} and domains implementing {@link ISupportInvoicing}. * + * {@link InvoicingTargetsRegistry} shows how to reuse the fflib selector and domain + * factories in a concrete, local way when generate must resolve a source type at runtime. + * * Prefer {@link #newInstance} for the default composition. * Each method that persists creates a fresh {@link UnitOfWork#newInstance}. */ @@ -58,7 +61,7 @@ public inherited sharing class InvoicingService * Registers invoice headers and lines with a Unit of Work. */ public class InvoiceFactory -{ + { private fflib_SObjectUnitOfWork unitOfWork; /** * Invoice__c headers accumulated so far; Ids are set after commit. @@ -108,7 +111,7 @@ public inherited sharing class InvoicingService * In-memory invoice header used by domains before persistence. */ public class Invoice -{ + { public List Lines { get; set; } public Id Account { get; set; } public String Description { get; set; } @@ -119,7 +122,7 @@ public inherited sharing class InvoicingService * In-memory invoice line used by domains before persistence. */ public class InvoiceLine -{ + { public String Description { get; set; } public Id Product { get; set; } public Decimal UnitPrice { get; set; } diff --git a/sfdx-source/apex-common-samplecode/main/classes/InvoicingTargetsRegistry.cls b/sfdx-source/apex-common-samplecode/main/classes/service/InvoicingTargetsRegistry.cls similarity index 86% rename from sfdx-source/apex-common-samplecode/main/classes/InvoicingTargetsRegistry.cls rename to sfdx-source/apex-common-samplecode/main/classes/service/InvoicingTargetsRegistry.cls index 8a26e68..26a4781 100644 --- a/sfdx-source/apex-common-samplecode/main/classes/InvoicingTargetsRegistry.cls +++ b/sfdx-source/apex-common-samplecode/main/classes/service/InvoicingTargetsRegistry.cls @@ -25,12 +25,19 @@ **/ /** - * Registry for InvoicingService.generate: polymorphic resolution of domains/selectors - * for SObject types whose domain implements InvoicingService.ISupportInvoicing. + * Example of reusing {@link fflib_Application.SelectorFactory} and + * {@link fflib_Application.DomainFactory} in a concrete, local way when a + * service needs runtime type resolution — here for {@link InvoicingService#generate}. * - * Configure via InvoiceTargets__mdt only for SObject types whose domain implements ISupportInvoicing. - * Not a general-purpose factory. Use direct construction (e.g. AccountsSelector.newInstance(), - * Opportunities.newInstance(records)) when the SObject type is known at compile time. + * This is not an application-wide {@code Application} class and not a coarse-grained + * service. It wires only the invoice source types from {@link InvoiceTargets__mdt} + * (Opportunity, DeveloperWorkItem, TrainingWorkItem, and further rows) so + * {@link InvoicingService} does not hard-code them. + * + * Configure via InvoiceTargets__mdt only for SObject types whose domain implements + * {@link InvoicingService.ISupportInvoicing}. Use direct construction + * (e.g. AccountsSelector.newInstance(), Opportunities.newInstance(records)) when + * the SObject type is known at compile time. */ public inherited sharing class InvoicingTargetsRegistry { private static fflib_Application.SelectorFactory selectorFactory; diff --git a/sfdx-source/apex-common-samplecode/main/classes/InvoicingTargetsRegistry.cls-meta.xml b/sfdx-source/apex-common-samplecode/main/classes/service/InvoicingTargetsRegistry.cls-meta.xml similarity index 100% rename from sfdx-source/apex-common-samplecode/main/classes/InvoicingTargetsRegistry.cls-meta.xml rename to sfdx-source/apex-common-samplecode/main/classes/service/InvoicingTargetsRegistry.cls-meta.xml From 60ba31405611ea7e426c1dbae9a33dde1cb12a0e Mon Sep 17 00:00:00 2001 From: Andrew Fawcett Date: Thu, 1 Oct 2026 15:34:49 +0100 Subject: [PATCH 12/12] Align REST and the invoice batch job with constructor injection. --- README.md | 2 +- .../classes/batchjobs/CreateInvoicesJob.cls | 28 +++++++- .../restapis/OpportunitiesResource.cls | 33 +++++++++- .../batchjobs/CreateInvoicesJobTest.cls | 66 +++++++++++++++++++ .../CreateInvoicesJobTest.cls-meta.xml | 5 ++ .../restapis/OpportunitiesResourceTest.cls | 51 ++++++++++++++ .../OpportunitiesResourceTest.cls-meta.xml | 5 ++ 7 files changed, 186 insertions(+), 4 deletions(-) create mode 100644 sfdx-source/apex-common-samplecode/test/classes/batchjobs/CreateInvoicesJobTest.cls create mode 100644 sfdx-source/apex-common-samplecode/test/classes/batchjobs/CreateInvoicesJobTest.cls-meta.xml create mode 100644 sfdx-source/apex-common-samplecode/test/classes/restapis/OpportunitiesResourceTest.cls create mode 100644 sfdx-source/apex-common-samplecode/test/classes/restapis/OpportunitiesResourceTest.cls-meta.xml diff --git a/README.md b/README.md index fb22586..a22f42a 100644 --- a/README.md +++ b/README.md @@ -46,7 +46,7 @@ Exceptions and local factories The constructor / `newInstance()` baseline above does not cover every type in that table. These notes are where composition differs, or where a local factory is used instead of an application-wide `Application` class: - This sample has no application-wide `Application` factory; that is reserved for more advanced DI, such as via [AT4DX](https://github.com/apex-enterprise-patterns/at4dx). -- Controllers and invocable actions have no `newInstance()` of their own: the platform calls a static `@AuraEnabled` / `@InvocableMethod`, or a Visualforce `StandardSetController` constructor. Those entry points compose `OpportunitiesService.newInstance()` internally; tests inject the service through the constructor. +- Controllers, invocable actions, REST resources, and the invoice batch job have no `newInstance()` of their own. The platform calls a static `@AuraEnabled` / `@InvocableMethod` / `@HttpPost`, or a Visualforce `StandardSetController` constructor; the invoice job is constructed with its no-arg constructor (`new CreateInvoicesJob()`). Those constructors compose `OpportunitiesService.newInstance()` (and a selector where needed). Tests inject those collaborators through the constructor. - A service does not keep one Unit of Work for its lifetime. Each method that commits DML calls `UnitOfWork.newInstance()`, registers its work, and commits; a second call on the same service instance must not reuse a Unit of Work that has already committed. Tests set `UnitOfWork.mock`. `UnitOfWork.newInstance(List)` is there when a method should not take the whole sample type list. - A domain wraps records you often do not have until you are already inside a method — for example `Opportunities.applyDiscounts` collects line items and then constructs `OpportunityLineItems`. That domain cannot be passed into the `Opportunities` constructor, so `newInstance(records)` is the factory and tests set the `@TestVisible` `mock` it returns. - `InvoicingTargetsRegistry` is an example of reusing the fflib selector and domain factories in a more concrete way when one service (`InvoicingService.generate`) needs runtime type resolution. It reads `InvoiceTargets__mdt` and, for a set of source Ids, selects the records and constructs the domain that implements `ISupportInvoicing` (Opportunity, DeveloperWorkItem, TrainingWorkItem, or a further metadata row) so the service does not hard-code those types. diff --git a/sfdx-source/apex-common-samplecode/main/classes/batchjobs/CreateInvoicesJob.cls b/sfdx-source/apex-common-samplecode/main/classes/batchjobs/CreateInvoicesJob.cls index dd63791..a09ba38 100644 --- a/sfdx-source/apex-common-samplecode/main/classes/batchjobs/CreateInvoicesJob.cls +++ b/sfdx-source/apex-common-samplecode/main/classes/batchjobs/CreateInvoicesJob.cls @@ -27,11 +27,35 @@ /** * Batch and schedulable job that invoices Opportunities marked Ready via {@link OpportunitiesService}. * Accumulates per-chunk failures and emails the running user on finish. + * + * Prefer the no-arg constructor for the default service and selector. + * Use the constructor when composing deliberately (including tests). */ public inherited sharing class CreateInvoicesJob implements System.Schedulable, Database.Batchable, Database.Stateful { + private final OpportunitiesService opportunitiesSvc; + private final OpportunitiesSelector selector; private List jobErrors = new List(); + + /** + * Default composition: {@link OpportunitiesService#newInstance} and {@link OpportunitiesSelector#newInstance}. + */ + public CreateInvoicesJob() + { + this(OpportunitiesService.newInstance(), OpportunitiesSelector.newInstance()); + } + + /** + * Creates a job with the given service and selector. + * @param opportunitiesSvc Opportunities service used to create invoices. Cannot be null. + * @param selector Selector used to query Opportunities ready to invoice. Cannot be null. + */ + public CreateInvoicesJob(OpportunitiesService opportunitiesSvc, OpportunitiesSelector selector) + { + this.opportunitiesSvc = opportunitiesSvc; + this.selector = selector; + } /** * Schedulable entry point that starts a new batch instance of this job. @@ -49,7 +73,7 @@ public inherited sharing class CreateInvoicesJob */ public Database.QueryLocator start(Database.BatchableContext context) { - return OpportunitiesSelector.newInstance().readyToInvoiceAsQueryLocator(); + return selector.readyToInvoiceAsQueryLocator(); } /** @@ -67,7 +91,7 @@ public inherited sharing class CreateInvoicesJob for (Id opportunityId : opportunitiesToInvoice) { discountByOpportunityId.put(opportunityId, 0); } - OpportunitiesService.newInstance().applyDiscountsAndCreateInvoices(discountByOpportunityId); + opportunitiesSvc.applyDiscountsAndCreateInvoices(discountByOpportunityId); } catch (Exception e) { diff --git a/sfdx-source/apex-common-samplecode/main/classes/restapis/OpportunitiesResource.cls b/sfdx-source/apex-common-samplecode/main/classes/restapis/OpportunitiesResource.cls index 8466eef..e569b55 100644 --- a/sfdx-source/apex-common-samplecode/main/classes/restapis/OpportunitiesResource.cls +++ b/sfdx-source/apex-common-samplecode/main/classes/restapis/OpportunitiesResource.cls @@ -27,23 +27,54 @@ /** * REST resource that applies a discount to a single Opportunity identified in the URL path. * Path pattern: {@code /opportunities/{id}/applydiscount} with optional {@code discount} query parameter. + * + * Prefer the no-arg constructor / {@link #applyDiscount} for the default service. + * Use the service constructor when composing deliberately (including tests). */ @RestResource(UrlMapping='/opportunities/*/applydiscount') global inherited sharing class OpportunitiesResource { + private final OpportunitiesService opportunitiesSvc; + + /** + * Default composition: {@link OpportunitiesService#newInstance}. + */ + public OpportunitiesResource() + { + this(OpportunitiesService.newInstance()); + } + + /** + * Creates a resource with the given service. + * @param opportunitiesSvc Opportunities service used to apply discounts. Cannot be null. + */ + public OpportunitiesResource(OpportunitiesService opportunitiesSvc) + { + this.opportunitiesSvc = opportunitiesSvc; + } + /** * Applies a discount to the Opportunity Id in the request URI via {@link OpportunitiesService}. * Reads the {@code discount} query parameter; defaults to zero when absent. */ @HttpPost global static void applyDiscount() + { + new OpportunitiesResource().handleApplyDiscount(); + } + + /** + * Instance entry for {@link #applyDiscount}, used by callers that inject a service. + * Reads the Opportunity Id from {@link RestContext} and the {@code discount} query parameter. + */ + public void handleApplyDiscount() { RestRequest req = RestContext.request; String[] uriParts = req.requestURI.split('/'); Id opportunityId = uriParts[2]; String discountParam = req.params.get('discount'); Decimal discountPercent = discountParam!=null ? Decimal.valueOf(discountParam) : 0; - OpportunitiesService.newInstance().applyDiscounts( + opportunitiesSvc.applyDiscounts( new Map{ opportunityId => discountPercent } ); } diff --git a/sfdx-source/apex-common-samplecode/test/classes/batchjobs/CreateInvoicesJobTest.cls b/sfdx-source/apex-common-samplecode/test/classes/batchjobs/CreateInvoicesJobTest.cls new file mode 100644 index 0000000..b2f8ee6 --- /dev/null +++ b/sfdx-source/apex-common-samplecode/test/classes/batchjobs/CreateInvoicesJobTest.cls @@ -0,0 +1,66 @@ +/** + * Copyright (c) 2013, FinancialForce.com, inc + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * - Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * - Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * - Neither the name of the FinancialForce.com, inc nor the names of its contributors + * may be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL + * THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY + * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +**/ + +/** + * Unit tests for {@link CreateInvoicesJob} using fflib-apex-mocks. + */ +@IsTest +private class CreateInvoicesJobTest +{ + @IsTest + private static void startCallsSelector() + { + fflib_ApexMocks mocks = new fflib_ApexMocks(); + OpportunitiesService mockService = (OpportunitiesService) mocks.mock(OpportunitiesService.class); + OpportunitiesSelector selectorMock = (OpportunitiesSelector) mocks.mock(OpportunitiesSelector.class); + Database.QueryLocator locator = Database.getQueryLocator( + [SELECT Id FROM Opportunity WHERE Id = null] + ); + mocks.startStubbing(); + mocks.when(selectorMock.readyToInvoiceAsQueryLocator()).thenReturn(locator); + mocks.stopStubbing(); + + new CreateInvoicesJob(mockService, selectorMock).start(null); + + ((OpportunitiesSelector) mocks.verify(selectorMock)).readyToInvoiceAsQueryLocator(); + } + + @IsTest + private static void executeCallsService() + { + fflib_ApexMocks mocks = new fflib_ApexMocks(); + OpportunitiesService mockService = (OpportunitiesService) mocks.mock(OpportunitiesService.class); + OpportunitiesSelector selectorMock = (OpportunitiesSelector) mocks.mock(OpportunitiesSelector.class); + Id opportunityId = TestIdGenerator.generate(Opportunity.SObjectType); + Opportunity opp = new Opportunity(Id = opportunityId, Name = 'Ready'); + + new CreateInvoicesJob(mockService, selectorMock).execute(null, new List{ opp }); + + ((OpportunitiesService) mocks.verify(mockService)).applyDiscountsAndCreateInvoices( + new Map{ opportunityId => 0 } + ); + } +} diff --git a/sfdx-source/apex-common-samplecode/test/classes/batchjobs/CreateInvoicesJobTest.cls-meta.xml b/sfdx-source/apex-common-samplecode/test/classes/batchjobs/CreateInvoicesJobTest.cls-meta.xml new file mode 100644 index 0000000..b009296 --- /dev/null +++ b/sfdx-source/apex-common-samplecode/test/classes/batchjobs/CreateInvoicesJobTest.cls-meta.xml @@ -0,0 +1,5 @@ + + + 67.0 + Active + diff --git a/sfdx-source/apex-common-samplecode/test/classes/restapis/OpportunitiesResourceTest.cls b/sfdx-source/apex-common-samplecode/test/classes/restapis/OpportunitiesResourceTest.cls new file mode 100644 index 0000000..ad6109c --- /dev/null +++ b/sfdx-source/apex-common-samplecode/test/classes/restapis/OpportunitiesResourceTest.cls @@ -0,0 +1,51 @@ +/** + * Copyright (c) 2012-2014, FinancialForce.com, inc + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * - Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * - Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * - Neither the name of the FinancialForce.com, inc nor the names of its contributors + * may be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL + * THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY + * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +**/ + +/** + * Unit tests for {@link OpportunitiesResource} using fflib-apex-mocks. + */ +@IsTest +private class OpportunitiesResourceTest +{ + @IsTest + private static void handleApplyDiscountCallsService() + { + fflib_ApexMocks mocks = new fflib_ApexMocks(); + OpportunitiesService mockService = (OpportunitiesService) mocks.mock(OpportunitiesService.class); + Id opportunityId = TestIdGenerator.generate(Opportunity.SObjectType); + RestRequest req = new RestRequest(); + req.requestURI = '/opportunities/' + opportunityId + '/applydiscount'; + req.httpMethod = 'POST'; + req.params.put('discount', '10'); + RestContext.request = req; + + new OpportunitiesResource(mockService).handleApplyDiscount(); + + ((OpportunitiesService) mocks.verify(mockService)).applyDiscounts( + new Map{ opportunityId => 10 } + ); + } +} diff --git a/sfdx-source/apex-common-samplecode/test/classes/restapis/OpportunitiesResourceTest.cls-meta.xml b/sfdx-source/apex-common-samplecode/test/classes/restapis/OpportunitiesResourceTest.cls-meta.xml new file mode 100644 index 0000000..b009296 --- /dev/null +++ b/sfdx-source/apex-common-samplecode/test/classes/restapis/OpportunitiesResourceTest.cls-meta.xml @@ -0,0 +1,5 @@ + + + 67.0 + Active +