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 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..a22f42a 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,84 @@ 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. `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; 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, 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. 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 (`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 | +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 +105,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/UnitOfWork.cls b/sfdx-source/apex-common-samplecode/main/classes/UnitOfWork.cls new file mode 100644 index 0000000..9655a8f --- /dev/null +++ b/sfdx-source/apex-common-samplecode/main/classes/UnitOfWork.cls @@ -0,0 +1,87 @@ +/** + * 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. +**/ + +/** + * 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 no-arg overload uses this sample's SObject type list. + * + * 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 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() { + return newInstance( + 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 + } + ); + } + + /** + * 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/domains/IAccounts.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/domains/IAccounts.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/domains/IAccounts.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..0b7f96a --- /dev/null +++ b/sfdx-source/apex-common-samplecode/main/classes/actions/ApplyDiscount.cls @@ -0,0 +1,124 @@ +/** + * 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}. + * + * 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 opportunitiesSvc; + + /** + * Default composition: {@link OpportunitiesService#newInstance}. + */ + public ApplyDiscount() { + this(OpportunitiesService.newInstance()); + } + + /** + * Creates an action with the given service. + * @param opportunitiesSvc Opportunities service used to apply discounts. Cannot be null. + */ + public ApplyDiscount(OpportunitiesService opportunitiesSvc) { + this.opportunitiesSvc = opportunitiesSvc; + } + + /** + * 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}. + */ + @InvocableMethod( + label='Apply Discount' + description='Applies a percentage discount to an Opportunity and its discountable products.' + category='Opportunities' + ) + public static List execute(List requests) { + return new ApplyDiscount().handleExecute(requests); + } + + /** + * 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 handleExecute(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. + opportunitiesSvc.applyDiscounts(discountByOpportunityId); + + // Build the results. + List results = new List(); + for (Request request : requests) { + 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/Application.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/Application.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/Application.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..dd4ba3d --- /dev/null +++ b/sfdx-source/apex-common-samplecode/main/classes/actions/CreateInvoice.cls @@ -0,0 +1,142 @@ +/** + * 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}. + * + * 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 opportunitiesSvc; + 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 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 opportunitiesSvc, InvoicesSelector selector) { + this.opportunitiesSvc = opportunitiesSvc; + 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. + * @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) { + return new CreateInvoice().handleExecute(requests); + } + + /** + * 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 handleExecute(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 = + opportunitiesSvc.applyDiscountsAndCreateInvoices(discountByOpportunityId); + Map invoiceNameById = invoiceNamesById(invoiceIdByOpportunityId.values()); + + // Build the results. + List results = new List(); + for (Request request : requests) { + Id invoiceId = invoiceIdByOpportunityId.get(request.opportunityId); + Result result = new Result(); + result.invoiceId = invoiceId; + result.invoiceName = invoiceNameById.get(invoiceId); + results.add(result); + } + return results; + } + + private Map invoiceNamesById(Iterable invoiceIds) { + Set invoiceIdSet = new Set(); + for (Id invoiceId : invoiceIds) { + invoiceIdSet.add(invoiceId); + } + Map invoiceNameById = new Map(); + for (Invoice__c invoice : selector.selectById(invoiceIdSet)) { + invoiceNameById.put(invoice.Id, invoice.Name); + } + return invoiceNameById; + } + + /** + * 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/domains/IChargeable.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/domains/IChargeable.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/domains/IChargeable.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..a09ba38 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,76 @@ **/ /** - * Class implements the required interfaces for Scheduling and executing in Batch + * Batch and schedulable job that invoices Opportunities marked Ready via {@link OpportunitiesService}. + * Accumulates per-chunk failures and emails the running user on finish. * - * 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 + * 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 { - // Simple job log for errors + 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. + * @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 selector.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); + Map discountByOpportunityId = new Map(); + for (Id opportunityId : opportunitiesToInvoice) { + discountByOpportunityId.put(opportunityId, 0); + } + opportunitiesSvc.applyDiscountsAndCreateInvoices(discountByOpportunityId); } 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 +102,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 +124,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 +135,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..d4db899 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,67 @@ **/ /** - * 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}. + * + * 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 { - public Decimal DiscountPercentage { get; set; } - - private ApexPages.StandardController standardController = null; - - private ApexPages.StandardSetController standardSetController = null; - - public OpportunityApplyDiscountController(ApexPages.StandardController controller) + private final OpportunitiesService opportunitiesSvc; + + /** + * Default composition: {@link OpportunitiesService#newInstance}. + */ + public OpportunityApplyDiscountController() { - standardController = controller; + this(OpportunitiesService.newInstance()); } - - public OpportunityApplyDiscountController(ApexPages.StandardSetController controller) + + /** + * Creates a controller with the given service. + * @param opportunitiesSvc Opportunities service used to apply discounts. Cannot be null. + */ + public OpportunityApplyDiscountController(OpportunitiesService opportunitiesSvc) { - standardSetController = controller; + this.opportunitiesSvc = opportunitiesSvc; } - - 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); - } - catch (Exception e) - { - ApexPages.addMessages(e); - } - - return ApexPages.hasMessages() ? null : standardController.view(); + new OpportunityApplyDiscountController().handleApplyDiscount(opportunityId, discountPercentage); } - public PageReference applyDiscounts() + /** + * 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 handleApplyDiscount(Id opportunityId, Decimal discountPercentage) { 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.')); + opportunitiesSvc.applyDiscounts( + new Map{ opportunityId => discountPercentage } + ); } catch (Exception e) { - ApexPages.addMessages(e); - } - - return null; + throw toAura(e); + } + } + + private static AuraHandledException toAura(Exception e) + { + 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/OpportunityApplyDiscountsController.cls b/sfdx-source/apex-common-samplecode/main/classes/controllers/OpportunityApplyDiscountsController.cls new file mode 100644 index 0000000..4374ba7 --- /dev/null +++ b/sfdx-source/apex-common-samplecode/main/classes/controllers/OpportunityApplyDiscountsController.cls @@ -0,0 +1,83 @@ +/** + * 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. +**/ + +/** + * 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 { + /** + * Discount percent entered on the list page (for example 10 for 10%). + */ + public Decimal discountPercentage { get; set; } + + private ApexPages.StandardSetController standardSetController; + private final OpportunitiesService opportunitiesSvc; + + /** + * Default composition: {@link OpportunitiesService#newInstance}. + * @param controller StandardSetController with selected Opportunities. Cannot be null. + */ + public OpportunityApplyDiscountsController(ApexPages.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 opportunitiesSvc Opportunities service used to apply discounts. Cannot be null. + */ + public OpportunityApplyDiscountsController( + ApexPages.StandardSetController controller, + OpportunitiesService opportunitiesSvc + ) { + this.standardSetController = controller; + this.opportunitiesSvc = opportunitiesSvc; + } + + /** + * 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(); + Map discountByOpportunityId = new Map(); + for (Id opportunityId : selectedOpportunityIds) { + discountByOpportunityId.put(opportunityId, discountPercentage); + } + opportunitiesSvc.applyDiscounts(discountByOpportunityId); + ApexPages.addMessage(new ApexPages.Message(ApexPages.Severity.INFO, 'Opportunities updated.')); + } catch (Exception e) { + ApexPages.addMessages(e); + } + return null; + } +} diff --git a/sfdx-source/apex-common-samplecode/main/classes/controllers/OpportunityConsoleController.cls-meta.xml b/sfdx-source/apex-common-samplecode/main/classes/controllers/OpportunityApplyDiscountsController.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/controllers/OpportunityApplyDiscountsController.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/controllers/OpportunityApplyDiscountsController.cls-meta.xml @@ -1,5 +1,5 @@ - 63.0 + 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..fb146f1 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,70 @@ **/ /** - * 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}. + * + * 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 { - public Decimal DiscountPercentage { get; set; } - - private ApexPages.StandardController standardController = null; - - private ApexPages.StandardSetController standardSetController = null; - - public OpportunityCreateInvoiceController(ApexPages.StandardController controller) + private final OpportunitiesService opportunitiesSvc; + + /** + * Default composition: {@link OpportunitiesService#newInstance}. + */ + public OpportunityCreateInvoiceController() { - standardController = controller; + this(OpportunitiesService.newInstance()); } - - public OpportunityCreateInvoiceController(ApexPages.StandardSetController controller) + + /** + * Creates a controller with the given service. + * @param opportunitiesSvc Opportunities service used to create invoices. Cannot be null. + */ + public OpportunityCreateInvoiceController(OpportunitiesService opportunitiesSvc) { - standardSetController = controller; + this.opportunitiesSvc = opportunitiesSvc; } - 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]); - } - catch (Exception e) - { - ApexPages.addMessages(e); - } - - return ApexPages.hasMessages() ? null : standardController.view(); + return new OpportunityCreateInvoiceController().handleCreateInvoice(opportunityId, discountPercentage); } - - public PageReference createInvoices() + + /** + * 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 handleCreateInvoice(Id opportunityId, Decimal discountPercentage) { 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.')); + Map invoiceIdByOpportunityId = opportunitiesSvc.applyDiscountsAndCreateInvoices( + new Map{ opportunityId => discountPercentage } + ); + return invoiceIdByOpportunityId.get(opportunityId); } - catch (Exception e) + catch (Exception e) { - ApexPages.addMessages(e); - } - - return null; - } -} \ No newline at end of file + throw toAura(e); + } + } + + 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/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..7b8b05d --- /dev/null +++ b/sfdx-source/apex-common-samplecode/main/classes/controllers/OpportunityCreateInvoicesController.cls @@ -0,0 +1,84 @@ +/** + * 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}. + * + * Prefer the StandardSetController constructor for the default service. + * Use the service overload when composing deliberately (including tests). + */ +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; + private final OpportunitiesService opportunitiesSvc; + + /** + * Default composition: {@link OpportunitiesService#newInstance}. + * @param controller StandardSetController with selected Opportunities. Cannot be null. + */ + public OpportunityCreateInvoicesController(ApexPages.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 opportunitiesSvc Opportunities service used to create invoices. Cannot be null. + */ + public OpportunityCreateInvoicesController( + ApexPages.StandardSetController controller, + OpportunitiesService opportunitiesSvc + ) { + this.standardSetController = controller; + this.opportunitiesSvc = opportunitiesSvc; + } + + /** + * 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(); + Map discountByOpportunityId = new Map(); + for (Id opportunityId : selectedOpportunityIds) { + discountByOpportunityId.put(opportunityId, discountPercentage); + } + opportunitiesSvc.applyDiscountsAndCreateInvoices(discountByOpportunityId); + 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..e8e7301 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,62 @@ **/ /** - * 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. + * + * 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 { - public Id JobId {get;set;} - - public OpportunityInvoicingController(ApexPages.StandardSetController standardSetControlelr) + private final OpportunitiesService opportunitiesSvc; + + /** + * Default composition: {@link OpportunitiesService#newInstance}. + */ + public OpportunityInvoicingController() { - ApexPages.addMessage( - new ApexPages.Message( - ApexPages.Severity.INFO, 'Click Start to scan Opportunities that are ready for invoicing and process them.')); + this(OpportunitiesService.newInstance()); } - - public PageReference startJob() + + /** + * Creates a controller with the given service. + * @param opportunitiesSvc Opportunities service used to submit the job. Cannot be null. + */ + public OpportunityInvoicingController(OpportunitiesService opportunitiesSvc) + { + this.opportunitiesSvc = opportunitiesSvc; + } + + /** + * 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().handleStartJob(); + } + + /** + * Instance entry for {@link #startJob}, used by callers that inject a service. + * @return Async Apex job Id returned after a successful submit. + */ + public Id handleStartJob() { 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 opportunitiesSvc.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..c15e7bb 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,44 @@ * 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. + * 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. + */ + 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 +69,87 @@ 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 discounts for each Opportunity Id and percentage pair on this domain instance. + * Registers dirty records on the Unit of Work. Does not commit. + * @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_SObjectUnitOfWork uow) { - // Calculate discount factor - Decimal factor = calculateDiscountFactor(discountPercentage); - - // Opportunity lines to apply discount to + if(discountByOpportunityId==null || discountByOpportunityId.isEmpty()) + { + return; + } List linesToApplyDiscount = new List(); - - // Apply discount + Map discountByOpportunityLineItemId = new Map(); + + // Apply discounts to Opportunities without lines, or to their line items. for(Opportunity opportunity : (List) getRecords()) { - // Apply to the Opportunity Amount? - if(opportunity.OpportunityLineItems == null - || opportunity.OpportunityLineItems.isEmpty()) + Decimal discountPercentage = discountByOpportunityId.get(opportunity.Id); + if(opportunity.OpportunityLineItems==null || opportunity.OpportunityLineItems.isEmpty()) { - // Adjust the Amount on the Opportunity if no lines + // Apply discount to the Opportunity (no line items). + Decimal factor = calculateDiscountFactor(discountPercentage); opportunity.Amount = opportunity.Amount * factor; uow.registerDirty(opportunity); } else { - // Collect lines to apply discount to - linesToApplyDiscount.addAll(opportunity.OpportunityLineItems); + for(OpportunityLineItem lineItem : opportunity.OpportunityLineItems) + { + linesToApplyDiscount.add(lineItem); + discountByOpportunityLineItemId.put(lineItem.Id, discountPercentage); + } } - } - - // Apply discount to lines - IOpportunityLineItems lineItems = (IOpportunityLineItems) Application.Domain.newInstance(linesToApplyDiscount); + } - lineItems.applyDiscount(this, discountPercentage, uow); + // Apply discounts to line items. + if(!linesToApplyDiscount.isEmpty()) + { + OpportunityLineItems.newInstance(linesToApplyDiscount) + .applyDiscounts(this, discountByOpportunityLineItemId, 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 +157,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..741b61e 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,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 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. + * 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. + */ + 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 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 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 applyDiscounts( + Opportunities opps, + Map discountByOpportunityLineItemId, + fflib_SObjectUnitOfWork uow + ) { - // Opportunties by Id - Map opportunitiesById = - new Map((List) opps.getRecords()); - - // Calculate discount factor - Decimal factor = Opportunities.calculateDiscountFactor(discountPercentage); - - // Adjust UnitPrice of each line according to Discount Type of Opportunity - for(OpportunityLineItem line : (List) this.records) + Map opportunitiesById = new Map((List) opps.getRecords()); + for(OpportunityLineItem line : (List) getRecords()) { - // Skip products that have not been approved for discounting + // 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') + 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); + Decimal factor = Opportunities.calculateDiscountFactor(discountPercentage); + 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..e569b55 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,58 @@ * 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. + * + * 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 with sharing class OpportunitiesResource +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 - // 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; + 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('/'); - - // 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 ); + opportunitiesSvc.applyDiscounts( + new Map{ 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..b52a112 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,74 @@ **/ /** - * 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() + /** + * Factory that returns this selector. + * @return {@link AccountsSelector} instance. + */ + public static AccountsSelector newInstance() { - return (IAccountsSelector) Application.Selector.newInstance(Account.SObjectType); + return 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..d92e631 --- /dev/null +++ b/sfdx-source/apex-common-samplecode/main/classes/selectors/DeveloperWorkItemsSelector.cls @@ -0,0 +1,89 @@ +/** + * 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 { + /** + * Factory that returns this selector. + * @return {@link DeveloperWorkItemsSelector} instance. + */ + public static DeveloperWorkItemsSelector newInstance() { + return 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/controllers/OpportunityConsoleController.cls b/sfdx-source/apex-common-samplecode/main/classes/selectors/InvoicesSelector.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/selectors/InvoicesSelector.cls index 0868ea3..75d7fae 100644 --- a/sfdx-source/apex-common-samplecode/main/classes/controllers/OpportunityConsoleController.cls +++ b/sfdx-source/apex-common-samplecode/main/classes/selectors/InvoicesSelector.cls @@ -1,5 +1,5 @@ /** - * Copyright (c) 2013, FinancialForce.com, inc + * Copyright (c) 2013-2016, FinancialForce.com, inc * All rights reserved. * * Redistribution and use in source and binary forms, with or without modification, @@ -24,12 +24,56 @@ * 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 +/** + * Selector for Invoice__c queries used by invocable and UI entry points. + */ +public virtual inherited sharing class InvoicesSelector extends fflib_SObjectSelector { - @RemoteAction - public static void applyDiscount(Id opportunityId, Decimal discountPercent) + /** + * Factory that returns this selector. + * @return {@link InvoicesSelector} instance. + */ + public static InvoicesSelector newInstance() + { + return 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) { - // Call service - OpportunitiesService.applyDiscounts(new Set { opportunityId }, discountPercent); + return (List) selectSObjectsById(idSet); } -} \ No newline at end of file +} 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/selectors/OpportunitiesSelector.cls b/sfdx-source/apex-common-samplecode/main/classes/selectors/OpportunitiesSelector.cls index f79980f..a311d80 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,138 @@ * 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() + /** + * Factory that returns this selector. + * @return {@link OpportunitiesSelector} instance. + */ + public static OpportunitiesSelector newInstance() { - return (IOpportunitiesSelector) Application.Selector.newInstance(Opportunity.SObjectType); + return 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..e1cfebb 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,83 @@ * 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() + /** + * Factory that returns this selector. + * @return {@link OpportunityLineItemsSelector} instance. + */ + public static OpportunityLineItemsSelector newInstance() { - return (IOpportunityLineItemsSelector) Application.Selector.newInstance(OpportunityLineItem.SObjectType); + return 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..cfe74ba 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,35 @@ * 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() + /** + * Factory that returns this selector. + * @return {@link PricebookEntriesSelector} instance. + */ + public static PricebookEntriesSelector newInstance() { - return (IPricebookEntriesSelector) Application.Selector.newInstance(PricebookEntry.SObjectType); + return 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 +64,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..df71f9a 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,35 @@ * 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() + /** + * Factory that returns this selector. + * @return {@link PricebooksSelector} instance. + */ + public static PricebooksSelector newInstance() { - return (IPricebooksSelector) Application.Selector.newInstance(Pricebook2.SObjectType); + return 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 +61,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..ac0ad77 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,54 @@ * 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() + /** + * Factory that returns a selector without field set fields. + * @return {@link ProductsSelector} instance. + */ + public static ProductsSelector newInstance() + { + return newInstance(false); + } + + /** + * 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 (IProductsSelector) Application.Selector.newInstance(Product2.SObjectType); + return 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 +81,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..2f50eec --- /dev/null +++ b/sfdx-source/apex-common-samplecode/main/classes/selectors/TrainingWorkItemsSelector.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. +**/ + +/** + * Selector for TrainingWorkItem__c including related Work Order account for invoicing. + */ +public virtual inherited sharing class TrainingWorkItemsSelector extends fflib_SObjectSelector { + /** + * Factory that returns this selector. + * @return {@link TrainingWorkItemsSelector} instance. + */ + public static TrainingWorkItemsSelector newInstance() { + return 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..177ff14 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,57 @@ **/ /** - * 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() + /** + * Factory that returns this selector. + * @return {@link UsersSelector} instance. + */ + public static UsersSelector newInstance() { - return (IUsersSelector) Application.Selector.newInstance(User.SObjectType); + return 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..56cd0f4 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,73 @@ * 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. + * + * 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 { - public static void updateOpportunityActivity(Set accountIds) + private final AccountsSelector selector; + + /** + * Default composition: {@link AccountsSelector#newInstance}. + * @return {@link AccountsService} instance. + */ + public static AccountsService newInstance() { - service().updateOpportunityActivity(accountIds); + return new AccountsService(AccountsSelector.newInstance()); } - private static IAccountsService service() + /** + * Creates a service with the given selector. + * @param selector Account selector used to load records. Cannot be null. + */ + public AccountsService(AccountsSelector selector) { - return (IAccountsService) Application.Service.newInstance(IAccountsService.class); + this.selector = selector; + } + + /** + * 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) + { + if(accountIds.isEmpty()) + { + return; + } + + fflib_SObjectUnitOfWork uow = UnitOfWork.newInstance(); + List accountRecords = selector.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..71f6922 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,64 @@ * 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}. + * + * {@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}. + */ +public inherited sharing class InvoicingService { - public static List generate(List sourceRecordIds) + /** + * Default composition. + * @return {@link InvoicingService} instance. + */ + public static InvoicingService newInstance() { - return service().generate(sourceRecordIds); + return 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; + private fflib_SObjectUnitOfWork 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;} - - public InvoiceFactory(fflib_ISObjectUnitOfWork unitOfWork) + * 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_SObjectUnitOfWork 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 +89,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;} + 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_SObjectUnitOfWork 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 + { } -} \ 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/InvoicingTargetsRegistry.cls b/sfdx-source/apex-common-samplecode/main/classes/service/InvoicingTargetsRegistry.cls new file mode 100644 index 0000000..26a4781 --- /dev/null +++ b/sfdx-source/apex-common-samplecode/main/classes/service/InvoicingTargetsRegistry.cls @@ -0,0 +1,121 @@ +/** + * 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. +**/ + +/** + * 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}. + * + * 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; + 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/service/InvoicingTargetsRegistry.cls-meta.xml b/sfdx-source/apex-common-samplecode/main/classes/service/InvoicingTargetsRegistry.cls-meta.xml new file mode 100644 index 0000000..b009296 --- /dev/null +++ b/sfdx-source/apex-common-samplecode/main/classes/service/InvoicingTargetsRegistry.cls-meta.xml @@ -0,0 +1,5 @@ + + + 67.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..df4e3ef 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,150 @@ * 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. + * + * 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; + + /** + * Default composition: {@link OpportunitiesSelector#newInstance}. + * @return {@link OpportunitiesService} instance. + */ + public static OpportunitiesService newInstance() + { + return new OpportunitiesService(OpportunitiesSelector.newInstance()); + } + + /** + * Creates a service with the given selector. + * @param selector Opportunity selector used to load records. Cannot be null. + */ + public OpportunitiesService(OpportunitiesSelector selector) + { + this.selector = selector; + } + + /** + * 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(Map discountByOpportunityId) + { + if(discountByOpportunityId==null || discountByOpportunityId.isEmpty()) + { + return; + } + try + { + fflib_SObjectUnitOfWork uow = UnitOfWork.newInstance(); + List opportunitiesWithProducts = selector.selectByIdWithProducts(discountByOpportunityId.keySet()); + Opportunities opportunities = Opportunities.newInstance(opportunitiesWithProducts); + opportunities.applyDiscounts(discountByOpportunityId, uow); + uow.commitWork(); + } + catch (Exception e) + { + throw wrap(e); + } + } + + /** + * 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 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 Map applyDiscountsAndCreateInvoices(Map discountByOpportunityId) { - service().applyDiscounts(opportunityIds, discountPercentage); + if(discountByOpportunityId==null || discountByOpportunityId.isEmpty()) + { + return new Map(); + } + try + { + 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()) + { + 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); + } + } + + // Commit the Unit of Work. + uow.commitWork(); + + // Build the result map. + Map invoiceIdByOpportunityId = new Map(); + for(Invoice__c invoice : invoices) + { + invoiceIdByOpportunityId.put(invoice.Opportunity__c, invoice.Id); + } + return invoiceIdByOpportunityId; + } + catch (Exception e) + { + throw wrap(e); + } } - - public static Set createInvoices(Set opportunityIds, Decimal discountPercentage) - { - return service().createInvoices(opportunityIds, discountPercentage); + + /** + * Submits {@link CreateInvoicesJob} as a Batch Apex job. + * @return Async Apex job Id returned by {@code Database.executeBatch}. + */ + public Id submitInvoicingJob() + { + return Database.executeBatch(new CreateInvoicesJob()); } - - public static Id submitInvoicingJob() + + private static OpportunitiesServiceException wrap(Exception e) { - return service().submitInvoicingJob(); - } + 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); + } - private static IOpportunitiesService service() + /** + * Represents an exception thrown by {@link OpportunitiesService} operations. + */ + public class OpportunitiesServiceException extends Exception { - return (IOpportunitiesService) Application.Service.newInstance(IOpportunitiesService.class); - } -} \ No newline at end of file + } +} 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..dec1502 --- /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..417cb7b --- /dev/null +++ b/sfdx-source/apex-common-samplecode/test/classes/actions/ApplyDiscountTest.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 {@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); + ApplyDiscount.Request request = new ApplyDiscount.Request(); + request.opportunityId = opportunityId; + request.discountPercentage = 10; + + // When the invocable action is executed + List results = new ApplyDiscount(mockService).handleExecute( + new List{ request } + ); + + // Then the service is called and the opportunity id is returned + ((OpportunitiesService) mocks.verify(mockService)).applyDiscounts( + 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'); + 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/test/classes/actions/CreateInvoiceTest.cls b/sfdx-source/apex-common-samplecode/test/classes/actions/CreateInvoiceTest.cls new file mode 100644 index 0000000..fbd2b59 --- /dev/null +++ b/sfdx-source/apex-common-samplecode/test/classes/actions/CreateInvoiceTest.cls @@ -0,0 +1,65 @@ +/** + * 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 CreateInvoice} using fflib-apex-mocks. + */ +@IsTest +private class CreateInvoiceTest { + @IsTest + private static void executeCallsServiceAndReturnsInvoiceId() { + // 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); + 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.applyDiscountsAndCreateInvoices(discountByOpportunityId)).thenReturn(invoiceIdByOpportunityId); + mocks.when(mockSelector.selectById(new Set{ invoiceId })).thenReturn(new List{ invoice }); + mocks.stopStubbing(); + CreateInvoice.Request request = new CreateInvoice.Request(); + request.opportunityId = opportunityId; + request.discountPercentage = 10; + + // When the invocable action is executed + List results = new CreateInvoice(mockService, mockSelector).handleExecute( + new List{ request } + ); + + // 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/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/main/classes/service/InvoicingServiceImpl.cls b/sfdx-source/apex-common-samplecode/test/classes/batchjobs/CreateInvoicesJobTest.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/batchjobs/CreateInvoicesJobTest.cls index 18138c9..b2f8ee6 100644 --- a/sfdx-source/apex-common-samplecode/main/classes/service/InvoicingServiceImpl.cls +++ b/sfdx-source/apex-common-samplecode/test/classes/batchjobs/CreateInvoicesJobTest.cls @@ -1,5 +1,5 @@ /** - * Copyright (c) 2012-2014, FinancialForce.com, inc + * Copyright (c) 2013, FinancialForce.com, inc * All rights reserved. * * Redistribution and use in source and binary forms, with or without modification, @@ -24,41 +24,43 @@ * 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 +/** + * Unit tests for {@link CreateInvoicesJob} using fflib-apex-mocks. + */ +@IsTest +private class CreateInvoicesJobTest { - /** - * Generate invoices for the given source records (so long as their domain classes implement ISupportInvoicing) - **/ - public List generate(List sourceRecordIds) + @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() { - // 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.'); + 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 } + ); } -} \ No newline at end of file +} 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/controllers/OpportunityApplyDiscountControllerTest.cls b/sfdx-source/apex-common-samplecode/test/classes/controllers/OpportunityApplyDiscountControllerTest.cls index 72d5cde..b95ed26 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,25 @@ * 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); + // When apply discount is invoked from the Lightning action + new OpportunityApplyDiscountController(mockService).handleApplyDiscount(opportunityId, 10); - // 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(); - - // 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 Map{ 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/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 new file mode 100644 index 0000000..7667953 --- /dev/null +++ b/sfdx-source/apex-common-samplecode/test/classes/controllers/OpportunityCreateInvoiceControllerTest.cls @@ -0,0 +1,53 @@ +/** + * 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); + Map discountByOpportunityId = new Map{ opportunityId => 10 }; + Map invoiceIdByOpportunityId = new Map{ opportunityId => invoiceId }; + mocks.startStubbing(); + mocks.when(mockService.applyDiscountsAndCreateInvoices(discountByOpportunityId)).thenReturn(invoiceIdByOpportunityId); + mocks.stopStubbing(); + // When create invoice is invoked from the Lightning action + Id resultId = new OpportunityCreateInvoiceController(mockService).handleCreateInvoice(opportunityId, 10); + + // Then the service is called and the invoice id is returned + ((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/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/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/main/classes/selectors/IAccountsSelector.cls b/sfdx-source/apex-common-samplecode/test/classes/controllers/OpportunityInvoicingControllerTest.cls similarity index 63% 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..e6d928f 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,25 @@ * 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(); + // When start job is invoked from the Lightning action + Id resultId = new OpportunityInvoicingController(mockService).handleStartJob(); + + // 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..faae864 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_SObjectUnitOfWork uowMock = (fflib_SObjectUnitOfWork) mocks.mock( + fflib_SObjectUnitOfWork.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.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'); + 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_SObjectUnitOfWork) 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_SObjectUnitOfWork uow = UnitOfWork.newInstance(); + opps.applyDiscounts(new Map{ opp.Id => 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/main/classes/domains/IAccounts.cls b/sfdx-source/apex-common-samplecode/test/classes/restapis/OpportunitiesResourceTest.cls similarity index 66% rename from sfdx-source/apex-common-samplecode/main/classes/domains/IAccounts.cls rename to sfdx-source/apex-common-samplecode/test/classes/restapis/OpportunitiesResourceTest.cls index f801697..ad6109c 100644 --- a/sfdx-source/apex-common-samplecode/main/classes/domains/IAccounts.cls +++ b/sfdx-source/apex-common-samplecode/test/classes/restapis/OpportunitiesResourceTest.cls @@ -24,9 +24,28 @@ * 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 +/** + * Unit tests for {@link OpportunitiesResource} using fflib-apex-mocks. + */ +@IsTest +private class OpportunitiesResourceTest { - List getAccounts(); - void setDescription(String description); - void updateOpportunityActivity(); -} \ No newline at end of file + @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 + 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..cccfe2b 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,70 @@ * 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_SObjectUnitOfWork unitOfWorkMock = (fflib_SObjectUnitOfWork) mocks.mock(fflib_SObjectUnitOfWork.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; - // WHEN we update the opportunity Activity - System.Test.startTest(); - new AccountsServiceImpl().updateOpportunityActivity(accountIds); - System.Test.stopTest(); + Test.startTest(); + new AccountsService(selectorMock).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_SObjectUnitOfWork) mocks.verify(unitOfWorkMock)).registerDirty(accounts); + ((fflib_SObjectUnitOfWork) 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..2635947 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,78 @@ * 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 Map{ 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..9c9f869 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,51 @@ * 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_SObjectUnitOfWork uowMock = (fflib_SObjectUnitOfWork) mocks.mock( + fflib_SObjectUnitOfWork.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); + Opportunities.mock = domainMock; + UnitOfWork.mock = uowMock; - // When - OpportunitiesService.applyDiscounts(testOppsSet, 10); + // When discounts are applied through the service + Map discountByOpportunityId = new Map(); + for(Id opportunityId : testOppsSet) + { + discountByOpportunityId.put(opportunityId, 10); + } + new OpportunitiesService(selectorMock).applyDiscounts(discountByOpportunityId); - // 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)).applyDiscounts(discountByOpportunityId, uowMock); + ((fflib_SObjectUnitOfWork) 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