Spectra


  • https://otube.oracle.com/playlist/dedicated/102024782/1_419pe2jh/1_ld5tkapq
  • API Gateway
    • Entry point of all microservices and provides
      • Rate limiting
      • Authentication and so
  • Helidon
    • https://www.youtube.com/watch?v=diUvR6gqHVY
    • https://www.youtube.com/watch?v=5cIYnC2fCAs
    • https://www.youtube.com/watch?v=BG2IB-1T1dk
  • OCI Console
    • User Interface to access and manage OCI 
  • OCI Native services
    • OKE
    • Streaming Service(equivalent to Kafka)
      • To ingest continuous high volume of data
      • used in applns where we want event driven implementation
    • Object Storage
      • High performance storage platform service(replacement for UCM to store files and unstructured data)
    • Data Flow
      • To run apache spark applications
      • This is next gen soln for running import jobs that we run in fa
    • Data integration Service
      • Helps data engineers and ETL developers. 
      • Also can be used for Orchestration services
    • Virtual Cloud Networks
  • Rondy Key Note
    • Vision of finance dept
      • Touchless processing
      • Continuous insights
      • Proactive collaboration & actions
    • End-end processing
      • Entire electronic apis from procurement to automated pay processing with min/no touch
    • Centralize and Standardize approaches
    • Core common Architecture
      • Object modeling approach
    • Operational Excellence
      • use ci/cd for zero down time
  • Overview
    • Spectra Platform
      • Redwood platform
      • Spectra Platform
        • enable for lifecycle pieces in building micro services
      • BOSS
        • Server less platform for building APIs
      • Spectra Batch/Data/Messaging
https://otube.oracle.com/playlist/dedicated/102024782/1_419pe2jh/1_5ektiwub

  • Helidon
    • Collection of Java libraries for writing microservices 
    • Its available in 2 frameworks
      • Helidon SE - Standard edition - microsframework that supports the reactive programming
      • Helidon MP - is an Eclipse micro profile runtime that allows the Jakarta EE community to run mircro services in portable way
      • FA apis are built using BOSS
      • Other popular micro service frameworks - Sprint boot, eclipse vert.x
  • Source Control - GIT
    • Repository per microservice
Demo1 - Helidon Microservices
  • Build and deploy Helidon quick start application locally 
 
  • Automated build and deployment of same quick start application in Spectra world
    • Use quick start appln developed in Day1
    • Add config files required for spectra service deployment
    • Source control Helidon quick start into alm git repo
    • Register project into nextgen fabs url(nextgen FA build system)
    • Modify .java file to add new method with service end point
    • git commit, push
    • review if new build is triggered
W1D2-Demo2-Workshop Prerequisites
  • To build an app with BOSS as backend service and VBCS based UI
  • BOSS workspace setup
    • BOSS cli
    • BOSS Helidon server -> Docker container
    • IDE setup
W1D2-Building apps with Spectra Platform and Services
  • OCI Services 
    • Object Storage, Streaming, Dataflow, logging, Orchestration, ADW
  • Spectra Common Service Tenancies
    • BOSS, Applcore, TM. SPS, etc
  • Core ERP Spectra Services(OKE Cluster)
  • 1 Spectra Service instance is wired with 1 Fusion Classic Instance. One Fusion Classic instance may be wired with multiple spectra service instances like core erp, cx, hed
  •  
Spectra Platform
  • Control Plane equivalent for SaaS Spectra Applications.
Provisioning Flow
  • CPQ -> TAS -> FASSM -> FALCM -> Fusion Classic POD...
Onboarding of Spectra
    • Majority of Fusion REST apis will be implemented using BOSS. Spectra BOSS is a serverless platform focusing on business object REST api development.
  • BOSS
    • Business Object Spectra Service which runs on Spectra Platform
    • Serverless, scalable service. 
  • Why BOSS
    • Common interface regardless of data source(ATP, Elastic, RODS)
    • Declarative query executions
      • N+1 query problem eliminated
    • Allows single source of Update
    • Boss views are readonly
    • API first model
    • Easily extensible by customers
    • Elastic Scaling
    • out of box diagnosting, tracing, observability & monitoring
  • BOSS OVerview
    • API developer uploads boss meta data to BOSS server platform. Boss FWK interprets meta data and generates the sql at runtime.
  • BOSS Architecture
    • BOSS runtime encapsulates the common functionality that every FA product requires to implement their Business Object as a service.
    • Meta data
    • Service Protocols
    • Programming Language
    • Data Source
    • Customizations
  • Module
    • Encompasses underlying business objects
    • Defines transaction boundary
    • analogous to Spectra subservice
  • Triggers
    • Custom life cycle methods in ADF BC
    • Read trigger
      • BeforeRead, InitiatilizeParameters, AfterRead
    • Writer Trigger
      • PrepareData, Cascade, Rollup
    • Validation
      • Validate
  • Enum
    • Analogous to Static View Objects used as a LOV in ADF BC
  • Business Object
    • Fields
    • Business logic/Triggers
    • View
      • Default
      • Named
    • Security
    • relationships
    • Datastore mapping
File Structure
  • model/modules
    • expenses
      • modules.json5
      • api
      • businessObjects
      • views
      • scripts
      • secutiry.json
      • resource/translations
      • enums
Boss Object Modeling
  • Identify logical modules/sub-services
  • Identify dependencies(functiona/non-functional)
  • Define shapes
  • Identify Business Objects
  • Define relationships
  • Identify appropriate triggers
  • erpselfservice
    • src/main/resources -> root for Boss Objects
      • self/model/modules/expenses/
        • api
        • businessobjects/<bo name>/bo.json5, views, scripts, security
        • enums
        • resources
        • module.json5
  • bo.json5
    • dataStoreMapping -> table, where condition
    • fields
    • identifiedBy  -> primary key for the table
  • views.json5
    • fields
    • accessors
    • parameters
    • collection and filter
  • security.json5
    • privilege
    • grants
  • Demo

    • boss metadata package   //creates zip file in resources directory
  • Translation
    • self/model/modules/<module name>/resources/translations/<lang>/...MessageBundle-i18n.json

  • Testing
  • Security
    • Spectra Authorization Service(SAS)
      • Identity store(similar to LDAP) -> IDCS 
      • Policy Store(similar to Jazn) -> Boss Security policies
      • Authorization engine/Policy decision point(similar to ADFSecurityContext)
        • Engine which returns list of conditions applicable for a given privilege on a business object
      • Policy endorsement point
      • Policy Adminstration point
  • Common Components
    • ERP Redwood LOV
    • Currency Components
  • Deployment
  • Integrated pipeline
  • Build Configuration
  • Artifactory
  • Deployment pipeline
  • Architecture
  • Security



https://confluence.oraclecorp.com/confluence/display/fintech/Produce+Business+Object
  • Initialize workspace
    • git clone ... https:/.../erp-boss-example.git
    • set proxy
    • Modify userconfig.env to update DB details
    • set env variables for BOSS_MODEL_HOME & Boss tools
    • Update the super user role mapping for 'boss' service in /scratch/$USER/rwddev/rwd_dev/bin/docker-compose.yaml
    • Restart the docker containers
  • Create Module
    • boss module create -m oraErpExpenses ora_erp_expenses
  • Create BusinessObject & default view
    • boss bo create -bo Expense -t EXM_EXPENSES -m oraErpExpenses ora_erp_expenses
    • Modify json file to add person_id condition
    • Modify api/v1.json5 to update endpoint definition
    • Add additional fields to Expense/views/default.json5
  • Building and deploying Metadata
    • Create a file called app-package.json5 and place it in the sources directory of the package.
    • Create a package deployment file  $BOSS_MODEL_HOME/config/application.json
  • boss metadata package -a ora_erp_expenses
  • Deploy the package to your local APM service.
    • curl -vLk --noproxy "*" -u jack:password -X POST 'http://localhost:8000/api/rwdinfra/apm/v1/applications?force=Enabled' -d @$BOSS_MODEL_HOME/config/application.json
  • Test the BOSS endpoints
    • //Open api
    • curl --location --request GET 'http://<host>:<port>/api/boss/data/objects/oraErpExpenses/v1:<deployment_id>/$openapi/expenses' --header 'Authorization: Basic c3VwZXJfdXNlcjp3ZWxjb21lMQ=='
    • //Get Expenses
    • curl --location --request GET 'http://<host>:<port>/api/boss/data/objects/oraErpExpenses/v1:<deployment_id>/expenses' --header 'Authorization: Basic c3VwZXJfdXNlcjp3ZWxjb21lMQ=='

Define Sort Order On Expense BO

  • Update the fields to add property as sortable=true for reimbursableAmount & timeCreated in Expense/bo.json5
  • Modify the default view to add the default order by as reimbursableAmount descending and creationDate desc

Update the Deployment

  • Update the package configuration file app-package.json5 to increment the version
  • Update the package deployment file application.json to increment the version of module
  • boss metadata package -a ora_erp_expenses
  • Deploy the package
  • Test the endpoints

Monitor & Troubleshoot Your Application

  • Find the trace id from the previous request on postman. Locate the opc-request-id header. 
  • http://<host>:16686/search

Introduce Expression Based Attributes & Modify Field Properties

  • In Expense/bo.json5 file , add transient attributes with the sql expression as follows; Update sortable to true

  •    expenseCategoryCodeOrder : {
         type : "string",
         accessModifier : "public",
         dataStoreMapping : {
           rdbms : {
             column : "CASE EXPENSE_TYPE_CATEGORY_CODE WHEN 'ACCOMMODATIONS' THEN 1 WHEN 'AIRFARE' THEN 2 WHEN 'CAR_RENTAL' THEN 3 WHEN 'MEALS' THEN 4 WHEN 'ENTERTAINMENT' THEN 5 WHEN 'MISC' THEN 6 ELSE 7 END",         sortable : true
          }
         },
         readable : "public",
         nullable : true,
         creatable : "module",
         updatable : "never",
         fieldSecurityEnabled : false
       },
  • Update order by clause to add the new transient attribute  to default view.json5
  • Update the deployment & build the project
  • Deploy the package to your local APM service, and record the new deployment ID.
  • Test the BOSS endpoints
Define Relationships & Accessors
  • Create reference BO
    • boss bo create -bo ExpenseType -t EXM_EXPENSE_TYPES -m oraErpExpenses ora_erp_expenses
    • Add the fieldList  to the default view as follows ExpenseType/views/default.json5
    • Update the deployment & build the project
    • Deploy and test
  • Create a Many to one relationship between expense and expense types
    • boss bo addrelation -m oraErpExpenses -bo Expense -tbo ExpenseType -fm expenseTypeId:expenseTypeId -c ManyToOne -acc expenseType -j leftOuterJoin ora_erp_expenses
  • Add searchable=true for the attribute name in ExpenseType BO
  • Update the expenseType accessor  in the Expense default view as follows
    •   accessors: {
          expenseType: {
            fields: [
              "expenseTypeId",
              "name"
            ]
          },
        },
  • Update the deployment & build the project
  • Deploy the package to your local APM service, and record the new deployment ID.
  • Test the BOSS endpoints
    • Filter Based on Accessor Attribute as filter=expenseType.name='Car Rental'
Init Transient Variables In AfterRead Trigger
  • Add a transient variable as expenseTypeName in Expense/bo.json5 
  • Create 2 java files for before & after read triggers as ExpenseBeforeReadTrigger.java & ExpenseAfterReadTrigger.java under triggers directory
  • Add the code to add the reference BO as part of the child query in case expenseTypeName field is present in the query list as below

  • Add the code init expenseTypeName from child reference expenseType.name as below
Query Over POST & Named Business Views
  • Update searchable=true for validationStatusCode, personId, expenseSource, expenseReportId
  • Express request as json
{
"fields" : ["expenseId","expenseTypeId","description","location","receiptAmount","receiptCurrencyCode","reimbursableAmount","reimbursementCurrencyCode","receiptDate"],
  "accessors": {
    "expenseType": {
      "fields": ["expenseTypeId","name"]
    }
  },
  "collection": {
    "sortBy": [
      { "reimbursableAmount" : "desc" } ,
      { "timeCreated" : "desc" }
    ],
    "filter" : "personId != NULL AND expenseReportId = NULL AND (validationStatusCode=NULL OR validationStatusCode in ('CLEAN')) AND expenseSource='CASH'"
  }
}

Named Business Views
  • Create a Named View "readyToSubmitExpenses" as follows
    boss view create -m oraErpExpenses -bo Expense -v readyToSubmitExpenses ora_erp_expenses
  • Update accessModifier and readable for  expenseDate as public
  • Add the view definition of 'readyToSubmitExpenses' with
    • field list : expenseType.name, merchantName, description, location, receiptAmount, receiptCurrencyCode, reimbursableAmount, reimbursementCurrencyCode, expenseDate
    • filters :   personId != NULL AND expenseReportId = NULL AND (validationStatusCode=null OR validationStatusCode in ('CLEAN')) AND expenseSource='CASH'
    • accessors: expenseType
Initialise Parameters For NamedViews

  • Update the filter condition in the view to filter by the bindParamValue
    AND expenseSource=:expSourceParam
  • Create initialise parameters script java file as ExpenseInitializeParametersTriggers.java
       String expenseSource="CASH";
       if (context.parameterValue("formOfPaymentsParam").isPresent()) {
           expenseSource=("card".equalsIgnoreCase((String)context.parameterValue("formOfPaymentsParam").get()))?
                   "CORP_CARD":expenseSource;
       }
       context.initParameterValue("expSourceParam", expenseSource);

Defining LOVs
  • Create Dynamic LOV For Expense Type Reference
    • Add the dynamic lookup definition for Expense type

expenseType : {
      type : "object",
      accessModifier : "public",
      target : {
        module : "oraErpExpenses",
        businessObject : "ExpenseType"
      },
      joinType : "leftOuterJoin",
      mapping : {
        expenseTypeId : "expenseTypeId"
      },
      dynamicLookup: {}
    },
  • Define FND Lookup Based LOVs With Translations
    • boss module create -m oraErpApplcore ora_erp_expenses
    • boss bo create -bo FndLookupValue -t FND_LOOKUP_VALUES_B -m oraErpApplcore ora_erp_expenses
    • boss bo addtranslation -bo FndLookupValue -m oraErpApplcore -trm sparse ora_erp_expenses
    • Add the below meaning , description & sourceLang with translatable as true to the FndLookupValue BO
    • Update fndlookupvalue→ fndlookupvalues in v1.json; remove the entry for fndlookupvalueTranslation
  • Uptake Fndlookup Based References For flightclass, flighttype & formOfPayments LOV
    • Create Named views for formOfPayment, flightClass & flightType
      • boss view create -m oraErpApplcore -bo FndLookupValue -v formOfPayment ora_erp_expenses
      • boss view create -m oraErpApplcore -bo FndLookupValue -v flightClass ora_erp_expenses
      • boss view create -m oraErpApplcore -bo FndLookupValue -v flightType ora_erp_expenses
    • Update view definitions as needed

    • Add fndlookupvalue based relationship mapping to formOfPayment, flightClass & flightType as follows
      • boss bo addrelation -m oraErpExpenses -bo Expense -tm oraErpApplcore -tbo FndLookupValue -fm ticketClassCode:lookupCode -c ManyToOne -acc flightClass -j leftOuterJoin ora_erp_expenses
      • boss bo addrelation -m oraErpExpenses -bo Expense -tm oraErpApplcore -tbo FndLookupValue -fm travelType:lookupCode -c ManyToOne -acc flightType -j leftOuterJoin ora_erp_expenses
      • boss bo addrelation -m oraErpExpenses -bo Expense -tm oraErpApplcore -tbo FndLookupValue -fm expenseSource:lookupCode -c ManyToOne -acc formOfPayment -j leftOuterJoin ora_erp_expenses
    • Add relevant view based dynamic lookup references on the accessor definition in Expense BO
      • dynamicLookup : {
        view : "flightClass"
        }
  • Enum Based Lists
    • Create a file named FormOfPayments under <GIT_REPO>/ora_erp_expenses/sources/model/self/modules/oraErpExpenses/enums
    • Add the below definition for the enum created
    • Uptake the enum as lov src in expense BO as below
formOfPaymentEnum: {
      type: "enumeration",
      target: {
        module: "oraErpExpenses",
        enumeration: "FormOfPayments"
      },
      dataStoreMapping: {
        rdbms: {
          column: "EXPENSE_SOURCE",
          searchable: true,
          sortable: true
        }
      },
      accessModifier: "public",
      readable : "public",
      creatable : "module",
      updatable : "never",
      nullable : true
    }

  • Update Field Attributes updatable/creatable As Public 
  • Add  Id Generator Property To Initialize Primary Key
  • Add  historyType Attribute For WHO Columns
  • security.json5 - add update/create privilege for super_user

{
  $dt_version : "2310.0.550",
  dataSecurityEnabled : true,
  dataSecurity : {
    rules : [ {
      privilege : "read",
      grants : [ {
        role : "SUPER_USER_ROLE"
      } ]
    }, {
      privilege : "create",
      grants : [ {
        role : "SUPER_USER_ROLE"
      } ]
    }, {
      privilege : "update",
      grants : [ {
        role : "SUPER_USER_ROLE"
      } ]
    } ]
  },
  allowAnonymousAccess : false,
  allowSkipDataSecurityViaAccessor : false,
  faStripeName : "fscm"
}

  • add below java files under businessObjects/Expense/scripts/expenses/expense/triggers
    • ExpensePrepareDataTrigger.java 
    • ExpenseBeforeReadTrigger.java 
    • ExpenseInitializeParametersTriggers.java 
    • ExpenseAfterReadTrigger.java 
    • ExpenseCascadeTrigger.java 
    • ExpenseRollupTrigger.java
  • Declarative Field Value Defaulting 
    • Add the defaultValue as 0 for  personalReceiptAmount field in BO definition
  • Programmatic Field Value Defaulting for ReimbursementAmount, ReimbursableCurrencyCode / ReceiptCurrencyCode, ExpenseLocation, ExpenseSource, ExchangeRate, ExpenseTypeCaetgoryCode
      • Create ExpenseFields.java file under businessObjects/Expense/scripts/oraErpExpenses/expense/constants
    • Add reimbursableAmount,reimbursementCurrencyCode ,locationId,expenseSource , exchangeRate fields to querycontext as part of preparedata trigger , to ensure the fields are fetched for the expense update use case
    • Create enum as ExchangeRates.java to add predefined set of exchange rates from receipt currency code to reimbursementCurrencyCode
      • Add the java file under Expense/scripts/oraErpExpenses/expense/constants
    • Update the Cascade trigger with the defaulting logic as mentioned in the use case section
    • Add defaulting logic for create use cases 
      • update the expense id to negated value of the rowid value as generated by the mid-tier. As this training data is created against the actual dev dbs, it's better to distinguish the actual transaction data used on day to day basis v/s training app specific data. 
      • default the reimbursement currency code & receipt currency code to USD . 
      • Update the Default values for expenseSource, LocationId in case its not added in the payload
      • Derive the exchange rate from enum defined above ,if not included in the payload
      • Initialise startDate to enddate in case startdate is not specified in the paylaod
      • Derive Reimbursement Amount based as (ReceiptAmount-Personal)*ExchangeRate 
  • Field Value Derivation Based On Adhoc Query -
    • Derive the following field values for expense BO for a given personid
      • [orgid ,assignmentid]
    • The org and assignment information is saved in PER_ALL_ASSIGNMENTS_M table.
      • Note : Since the cross family BO delivery mechanism is not clear , for now lets go ahead and create a BO for thr HCM BO in our training repo. Ideally this should be delivered as part of hcm person module related BOSS service artefacts.
    • boss module create -m oraErpHcm ora_erp_expenses
      boss bo create -bo PersonAssignment -t PER_ALL_ASSIGNMENTS_M -m oraErpHcm ora_erp_expenses
    • Update the where clause on the PersonAssignment BO as below
              where : "PRIMARY_FLAG = 'Y' AND ASSIGNMENT_STATUS_TYPE ='ACTIVE'"
  • Update default view definition as needed
  • Add PersonInfoComputator class under businessObjects/Expense/scripts/oraErpExpenses/expense/computators/
    Add the code to derive the assignmentid,orgid from the new BO using adhocquery builder
  • Update the defaultOrgAssigmentAndPersonId method in Cascade trigger to invoke the above computator
  • Update the searchable attribute to true for in PersonAssignment BO as follows 
  • Add the adhoc query to fetch the assignmentid & orgid from the PersonAssignment BO 
    try (var personAssignments = context
                   .newQueryBuilder("oraErpHcm.PersonAssignment")
                   .filter(filtercondition)
                   .build()
                   .read()) {
               StreamSupport.stream(personAssignments.spliterator(), false).forEach(personAssignment -> {
                   Number orgId=(Number) personAssignment.fieldValueAsNumber("organizationId").orElse(dftOrgId);
                   Number assignmentId=(Number) personAssignment.fieldValueAsNumber("assignmentId").orElse(dftAssignmentId);
                   context.logger().info("In expense.PersonInfoComputator::compute:[[derived orgid - "+ orgId + ", derived assignmentId - "+  assignmentId+"]]");
                   expense.setFieldValue(ExpenseFields.ORG_ID,orgId);
                   expense.setFieldValue(ExpenseFields.ASSIGNMENT_ID,assignmentId );
               });
           }
Adding Business Specific Validations
  • Declarative Validation
    • Data type validations - User should not be allowed to set string value  on date or number type fields
    • ReceiptAmount – add a maximum property to 1000 and minimum to 0
    • Description should be at least 3 chars long & within 30 char limit-- Add the maxLength and minLength constraints
    • lov field validation on value set
    • Required field validators- ExpenseTypeid, endDate, receiptAmount, receiptCurrencyCode
  • Data type validations 
    • Section 1 : Data Type Based Validations
      • User should not be allowed to  set string value  on date or number type fields
      •  endDate : {
             type : "date",...

    • Section 2 :  Maximum & Minimum Value Based Validations
      • receiptAmount : {
              type : "big-decimal",
              minimum : "0",
              maximum : "1000"
            },
    • Section 3 : Maximum & Minimum Length Based Validations
      •   description : {
             type : "string",
             maxLength : 30,
             minLength : 3
           },
    • Section 4 : Lov Value Validation
      • Value exists validation on lov field type via reference BO
      • If we have the reference BO added via the relationship , in the POST/PATCH payload if we pass the FK as a part of the reference BO $id , BOSS fwk would implicitly check if the value sent exists
      • In the reference BO dataset and report errors in case of invalid data is set
    • Section 5 : Required Field Validation
      • nullable property in BOSS defines if the field can accept null values or not. By default this property is set to false for all fields
      • nullable : false,
    • Section 6 : Commit to Create a Checkpoint
      • git commit
  • Business Rule Validation Using Validate Trigger
    • Section 1 : Required Field Validators
      • Create a new Java class as ExpenseValidateTrigger.java under /triggers

      • public final class ExpenseValidateTrigger {
      •     @Validate
      •     public static void validate(final ValidateContext context) {
      •         context.logger().info("In expense.ExpenseValidateTrigger::validate:START");
      •          
      •         //Add the validation logic here
      •          
      •         context.logger().info("In expense.ExpenseValidateTrigger::validate:END");
      •  
      •     }
      • }
    • Create a java class as RequiredFieldValidator.java under /validations

      • public final class RequiredFieldValidator { 
      •     public static void validate(TransactionBusinessObject expense,final ValidateContext context){
      •         context.logger().info("In expense.RequiredFieldValidator::validate::START");
      •  
      •        /*
      •                     add required field validation code here
      •         */
      •         context.logger().info("In expense.RequiredFieldValidator::validate::END");
      •     }
      • }

    • Update the required field validator with the field value check code as below

      • private static void requiredFieldValidation(String fieldName, TransactionBusinessObject expense,ValidateContext context) {
      •        context.logger().info("In expense.RequiredFieldValidator::requiredFieldValidation::START"+expense.fieldValue(fieldName).orElse(null));
      •  
      •        if (expense.changedFields().contains(fieldName)) {
      •            if (expense.fieldValue(fieldName).isEmpty())  {
      •                context.logger().info(fieldName + " Value set is null");
      •                context.raiseError("Enter a value", expense, fieldName);
      •            }
      •        } else {
      •            context.logger().info(fieldName + " field is not defined");
      •            context.raiseError("Enter a value", expense, fieldName);
      •        }
      •  
      •        context.logger().info("In expense.RequiredFieldValidator::requiredFieldValidation::END");
      •  
      •    }
      •   
    • Added requiredFieldValidator checks for expenseSource, expenseTypeId , endDate and locationId as follows

      • public static void validate(TransactionBusinessObject expense,final ValidateContext context){
      •       context.logger().info("In expense.RequiredFieldValidator::validate::START");
      •  
      •       if(hasChanged(expense, ExpenseFields.EXPENSE_SOURCE)) {
      •           requiredFieldValidation(ExpenseFields.EXPENSE_SOURCE,expense,context);
      •       }
      •       if(hasChanged(expense, ExpenseFields.LOCATION_ID)) {
      •           requiredFieldValidation(ExpenseFields.LOCATION_ID,expense,context);
      •       }
      •       if(hasChanged(expense, ExpenseFields.EXPENSE_TYPE_ID)) {
      •           requiredFieldValidation(ExpenseFields.EXPENSE_TYPE_ID,expense,context);
      •       }
      •       if(hasChanged(expense, ExpenseFields.END_DATE)) {
      •           requiredFieldValidation(ExpenseFields.END_DATE,expense,context);
      •       }
      •       context.logger().info("In expense.RequiredFieldValidator::validate::END");
      •   }
      • invoke the RequireFieldValidator in the validate trigger as below

      • //Add the validation logic here
      •        stream(context.businessObjects())
      •                .forEach(bo -> {
      •            RequiredFieldValidator.validate(bo,context);
      •        });
    • Section 2 : Conditional hidden/required Field Validations
      • Use case & References
        • flighttype, class are required for airfare but hidden for others
        • start & end date is required for accommodation, end date(rename as expense date in UI)  for others
        • For hidden fields, Check if the non applicable fields for category are set in payload
      • Create a method to report required field errors based on category code for airfare fields and startDate in RequiredFieldValidator.java  as follows

        if("ACCOMODATIONS".equalsIgnoreCase(expensetypeCategoryCode)){
            if(hasChanged(expense, ExpenseFields.START_DATE)) {
                requiredFieldValidation(ExpenseFields.START_DATE, expense, context);
            }
        }
        else if("AIRFARE".equalsIgnoreCase(expensetypeCategoryCode)){
            if(hasChanged(expense, ExpenseFields.FLIGHT_TYPE)) {
                requiredFieldValidation(ExpenseFields.FLIGHT_TYPE, expense, context);
            }
            if(hasChanged(expense, ExpenseFields.FLIGHT_CLASS)) {
                requiredFieldValidation(ExpenseFields.FLIGHT_CLASS, expense, context);
            }
        }
      • Create a method to check if the value is set for non applicable fields for a given category
              if (expense.changedFields().contains(fieldName)) {
                   if (!expense.fieldValue(fieldName).isEmpty())  {
                       context.logger().info(fieldName+" Value set is not null : "+expense.fieldValue(fieldName));
                       context.raiseError(fieldName+" is not applicable for category "+expensetypeCategoryCode, expense, fieldName);
                   }
               else {
                   context.logger().info(fieldName + " field is not defined");
               }
         
               context.logger().info("In expense.RequiredFieldValidator::hiddenFieldValidation::END");
      • Add ExpensetypeCategoryCode in the fields list in preparedata trigger as follows, as we need to fetch the expensetyepcategorycode for validation trigger
      • if(!fieldNames.contains(ExpenseFields.EXPENSE_TYPE_CATEGORY_CODE)){
            query.fieldNames(List.of(ExpenseFields.EXPENSE_TYPE_CATEGORY_CODE));
        }
    • Section 3 : Conditional Updatable Check Based Validations
      • Use case
        • Enable or disable field based on form of payment selected
        • when expenseSource=CORP_CARD, fields as receiptamount, dates, merchantname, expensetype, location  are not updatable, but description can be updated
        • Check if the payload has modified values for non modifiable fields
        • Ref: Expense Training Application 
      • Create a file as FormFieldValidator.java under /expense/validations
        • package oraErpExpenses.expense.validations;
           
          import oracle.boss.script.ValidateContext;
          import oracle.boss.script.TransactionBusinessObject;
           
          public final class FormFieldValidator {
           
              public static void validate(TransactionBusinessObject expense,final ValidateContext context){
                  /*
                      conditional hidden value set
                   */
           
              }
          }
        • Add the business logic to check if the non modifiable fields are updated in the request payload if expense source is CORP_CARD
        • Update the validate Trigger ExpenseValidateTrigger.java to invoke the FormFieldValidator as below
    • Section 4 : Reference BO Value Exists Validators Using Adhoc Queries
      • lov Value validation based on date effectivity and value set
        • ex: flight class , type, form of payments
        • Note :Adhoc queries are allowed in the validation trigger.
      • Create a java file LovValueValidator.java /Expense/scripts/oraErpExpenses/expense/validations
        package oraErpExpenses.expense.validations;
         
        import oracle.boss.script.ValidateContext;
        import oracle.boss.script.TransactionBusinessObject;
        import oracle.boss.script.TransactionBusinessObject.TransactionState;
        import oraErpExpenses.expense.constants.ExpenseFields;
         
        public final class LovValueValidator {
         
            public static void validate(TransactionBusinessObject expense,final ValidateContext context){
                context.logger().info("In expense.LovValueValidator::validate::START");
         
                context.logger().info("In expense.LovValueValidator::validate::END");
         
            }
         
        }
      • Invoke the LovValueValidator in the ExpenseValidateTrigger.java as follows
        stream(context.businessObjects())
                        .forEach(bo -> {
         
                    RequiredFieldValidator.validate(bo,context);
                    FormFieldValidator.validate(bo,context);
                    LovValueValidator.validate(bo,context);
         
        });
      • Add adhoc query to fetch flight class/type/form of payments lookup based on the ticketclasscode,traveltype and expensesource set in request payload
          boolean valueExists = false;
               try (var lookupCodes = context.newQueryBuilder("oraErpApplcore.FndLookupValue")
                       .fieldNames(Arrays.asList(new String[]{"lookupCode","meaning"}))
                       .orderBy(List.of(orderByField))
                       .filter("lookupType = '"+lookupType+"' AND enabledFlag='Y' AND viewApplicationId in (0, 10016) and lookupCode='"+lookupCode+"'")
                       .build()
                       .read()) {
                   Iterator<BusinessObject> lookupCodeItreator = lookupCodes.iterator();
                   if (lookupCodeItreator.hasNext()) {
                       valueExists = true;
                   }
               }        
    • Section 5 : Custom Business Logic Based Validations
      • usecase
        • Description should be at least 3 chars
        • Business Rule check as 0< ReimburseableAmount<1000
        • Exchange Rate limit validations
          exchange Rate exists for to and from currency Code , tolerance of 3% is allowed else error
          else exchange Rate >100 
      • Add the description validator DescriptionValidator.java as follows
        • package oraErpExpenses.expense.validations;
           
          import oracle.boss.script.ValidateContext;
          import oracle.boss.script.TransactionBusinessObject;
          import oraErpExpenses.expense.constants.ExpenseFields;
           
           
          public final class DescriptionValidator {
              public static void validate(TransactionBusinessObject expense,final ValidateContext context){
                  context.logger().info("In expense.DescriptionValidator::validate::START");
                  if(expense.changedFields().contains(ExpenseFields.DESCRIPTION)) {
                      String description = (String)expense.fieldValue(ExpenseFields.DESCRIPTION).orElse(null);
                      if(description==null || description.length()<=3){
                          context.logger().info(ExpenseFields.DESCRIPTION+" Value set should be at least 3 chars long : "+description);
                          context.raiseError(ExpenseFields.DESCRIPTION+" should be more than 3 chars", expense, ExpenseFields.DESCRIPTION);
                      }
                  }
                  context.logger().info("In expense.DescriptionValidator::validate::END");
              }
           
          }
      • Add the amount validator AmountValidator.java as follows
      • package oraErpExpenses.expense.validations;
        import oracle.boss.script.ValidateContext;
        import oracle.boss.script.TransactionBusinessObject;
        import oraErpExpenses.expense.constants.ExpenseFields;
         
        import java.math.BigDecimal;
         
        public final class AmountValidator {
            public static void validate(TransactionBusinessObject expense,final ValidateContext context){
                context.logger().info("In expense.AmountValidator::validate::START");
                if(expense.changedFields().contains(ExpenseFields.RECEIPT_AMOUNT)) {
                    BigDecimal amount = (BigDecimal)expense.fieldValueAsBigDecimal(ExpenseFields.REIMBURSABLE_AMOUNT).orElse(null);
                    if(amount !=null && amount.doubleValue()<0 ){
                        context.logger().info(ExpenseFields.REIMBURSABLE_AMOUNT+" should be >0 : "+amount);
                        context.raiseError("Invalid amount entered", expense, ExpenseFields.RECEIPT_AMOUNT);
                    }
                    else if(amount !=null && amount.doubleValue()>1000){
                        context.logger().info(ExpenseFields.REIMBURSABLE_AMOUNT+" Value set should be <1000 "+amount);
                        context.raiseError(" Reimbursable amount exceeds 1000$ limit", expense, ExpenseFields.RECEIPT_AMOUNT);
                    }
                }
                context.logger().info("In expense.AmountValidator::validate::END");
            }
         
        }
      • Add the exchange rate validator ExchangeRateValidator.java as follows
      • package oraErpExpenses.expense.validations;
         
        import oracle.boss.script.ValidateContext;
        import oracle.boss.script.TransactionBusinessObject;
        import oraErpExpenses.expense.constants.ExpenseFields;
        import oraErpExpenses.expense.constants.ExchangeRates;
         
        import java.math.BigDecimal;
         
         
        public final class ExchangeRateValidator {
            public static void validate(TransactionBusinessObject expense,final ValidateContext context) {
                context.logger().info("In expense.ExchangeRateValidator::validate::START");
                if(expense.changedFields().contains(ExpenseFields.EXCHANGE_RATE)) {
                    String receiptCurrencyCode = (String) expense.fieldValue(ExpenseFields.RECEIPT_CURRENCY_CODE).orElse(null);
                    expense.fieldValueAsBigDecimal(ExpenseFields.EXCHANGE_RATE)
                            .ifPresent(v -> {
                                if (v != null) {
                                    BigDecimal systemGeneratedRate = ExchangeRates.getRate(receiptCurrencyCode);
                                    BigDecimal exchgRatesetAtBO = (BigDecimal) v;
                                    if (systemGeneratedRate.doubleValue() == 1.0 && !"USD".equalsIgnoreCase(receiptCurrencyCode)) {
                                        /*
                                            default generated rate
                                         */
                                        if (exchgRatesetAtBO.doubleValue() > 100) {
                                            context.logger().info(systemGeneratedRate.doubleValue() + " No rate exists for the receiptcurrency code, exchange rate entered exceeds the approved limit of 100  " + exchgRatesetAtBO.doubleValue());
                                            context.raiseError("Invalid value ,Exchange rate entered exceeds the approved limit of 100", expense, ExpenseFields.EXCHANGE_RATE);
                                        }
         
                                    else if (systemGeneratedRate.compareTo(exchgRatesetAtBO) < 0 && (1.03 * systemGeneratedRate.doubleValue() < exchgRatesetAtBO.doubleValue())) {
         
                                        context.logger().info(exchgRatesetAtBO.doubleValue() + " exceeds the system generated exchaneg rate with 3% talerance " + (1.03 * systemGeneratedRate.doubleValue()));
                                        context.raiseError("Invalid value ,Exchange rate entered exceeds the approved limit of " 1.03 * systemGeneratedRate.doubleValue(), expense, ExpenseFields.EXCHANGE_RATE);
         
                                    else if (exchgRatesetAtBO.doubleValue() <= 0) {
         
                                        context.logger().info(exchgRatesetAtBO.doubleValue() + " exchange rate is negative ");
                                        context.raiseError("Invalid value , Enter a value >0", expense, ExpenseFields.EXCHANGE_RATE);
         
                                    }
                                }
                            });
                }
         
                context.logger().info("In expense.ExchangeRateValidator::validate::END");
         
            }
        }
      • Invoke the validators from the validate trigger
      • stream(context.businessObjects())
                        .forEach(bo -> {
         
                    RequiredFieldValidator.validate(bo,context);
                    FormFieldValidator.validate(bo,context);
                    LovValueValidator.validate(bo,context);
                    DescriptionValidator.validate(bo,context);
                    AmountValidator.validate(bo,context);
                    ExchangeRateValidator.validate(bo,context);
         
                });
    • Section 6 : Commit to Create a Checkpoint
Delete Business Object Using REST API
  • Update the security.json5 file with the below entry for super_user
    {
      privilege : "delete",
      grants : [ {
        role : "SUPER_USER_ROLE"
      } ]
    }
Translation Uptake In BOSS Metadata
  • Section 1: Error Message With Token Uptake For Custom Error Message
    • Create a Message bundle file as "ExpenseErrorMessageBundle-i18n.json" under /oraErpExpenses/resources/translations
    • Update the module.json5 to add the new resource bundle entry as follows
    • {
        $dt_version : "2310.0.550",
        allowAnonymousAccess : false,
        translations : {
          self : {
            path : "oraErpExpensesMBundle-i18n.json"
          },
          errorMsgs: {
            path : "ExpenseErrorMessageBundle-i18n.json"
          }
        },
        dataStore : {
          rdbms : {
            name : "ApplicationDBDS"
          }
        }
      }
    •  Add the below entry for string definition with tokens
    • {
      "ValidateExchangeRateLimitMsg" "Invalid value ,Exchange rate entered exceeds the approved limit of {rateLimit}",
        "@ValidateExchangeRateLimitMsg" : {
          "x-StrId""226016",
          "x-StrType""Validation",
          "x-SRKey""Validation.Get.ValidateExchangeRateLimitMsg",
          "placeholders": {
            "rateLimit": {
              "type""Number",
              "required""true"
            }
          }
        }
      }
    • Uptake the translatable string in the Validation Error msg(ExchangeRateValidator.java) thrown as follows
    • else if (systemGeneratedRate.compareTo(exchgRatesetAtBO) < 0 && (1.03 * systemGeneratedRate.doubleValue() < exchgRatesetAtBO.doubleValue())) {
       
          context.logger().info(exchgRatesetAtBO.doubleValue() + " exceeds the system generated exchange rate with 3% talerance " + (1.03 * systemGeneratedRate.doubleValue()));
          try{
              context.raiseError(
                      context.messageBundle("errorMsgs")
                              .builder("ValidateExchangeRateLimitMsg")
                              .token("rateLimit",( 1.03 * systemGeneratedRate.doubleValue()))
                              .format()
                      , expense, ExpenseFields.EXCHANGE_RATE);
          }catch (RequiredParameterException
                  | MissingStringException
                  | InvalidParameterException e){
              throw new RuntimeException(e);
          }
       
      }
  • Translatable BO Field Labels , Description , Hints
    • Create a Message bundle file as "ExpenseErrorMessageBundle-i18n.json" under translations/ko folder
    • Update the translations for the strings in Korean as follows
  • Translatable BO Field Labels , Description , Hints
  • Create a Message bundle file as "oraErpExpensesMBundle-i18n.json" under translations/ko folder
  • Add the string entries in base resource model
  • Uptake the resource string in the BO metadata of Expence
Implementing Security
  • Update the user role mapping for 'boss' service in /scratch/$USER/rwddev/rwd_dev/bin/docker-compose.yaml
  • security.providers.0.http-basic-auth.users.1.login: FINUSER1
    security.providers.0.http-basic-auth.users.1.password: Welcome1
    security.providers.0.http-basic-auth.users.1.roles: ORA_EMPLOYEE_ABSTRACT
    security.providers.0.http-basic-auth.users.2.login: FINUSER2
    security.providers.0.http-basic-auth.users.2.password: Welcome1
    security.providers.0.http-basic-auth.users.2.roles: ORA_EMPLOYEE_ABSTRACT
    security.providers.0.http-basic-auth.users.3.login: FINUSER3
    security.providers.0.http-basic-auth.users.3.password: Welcome1
    security.providers.0.http-basic-auth.users.3.roles: ORA_EMPLOYEE_ABSTRACT
    security.providers.0.http-basic-auth.users.4.login: FINUSER4
    security.providers.0.http-basic-auth.users.4.password: Welcome1
    security.providers.0.http-basic-auth.users.4.roles: ORA_EMPLOYEE_ABSTRACT
  • Restart docker containers

  • Update Security Construct With Read Access For Expense Users 
    • Create a module for oraErpHcm if not already created. The module naming should be in accordance with the BO Specifications and the BOSS Taxonomy
    • Create a BO on PER_USERS table and default view with userid parameter. The BO naming should be in accordance with the BO Specifications and the BOSS Taxonomy
    • Add a where clause for the Person BO as follows
    • rdbms : {
      PER_USERS : {
      table : "PER_USERS",
      where : "ACTIVE_FLAG = 'Y'"
      }
      }
  • Update searchable to true for username on persons BO
  • Update the accessModifier for Person BO as public
    • accessModifier : "public"
  • Create a relationship between expense to person Bo
    • boss bo addrelation -m oraErpExpenses -bo Expense -tm oraErpHcm -tbo Person -fm personId:personId -c OneToOne -acc exmOwner -j innerJoin ora_erp_expense
  • Add accessor to defaultview of Expense as follows
    exmOwner: {
      fields: [
        "personId",
        "userId"
        ]
    }
  • Update security side car file(i.e Expense/security.json5) with ORA_EMPLOYEE_ABSTRACT role based grant for CRUD access
    {
      $dt_version : "2310.0.550",
      dataSecurityEnabled : true,
      dataSecurity : {
        rules : [ {
          privilege : "read",
          grants : [ {
            role : "ORA_EMPLOYEE_ABSTRACT"
          } ]
        }, {
          privilege : "create",
          grants : [ {
            role : "SUPER_USER_ROLE"
          } ]
        }, {
          privilege : "update",
          grants : [ {
            role : "SUPER_USER_ROLE"
          } ]
        }, {
          privilege : "delete",
          grants : [ {
            role : "SUPER_USER_ROLE"
          } ]
        } ]
      },
      allowAnonymousAccess : false,
      allowSkipDataSecurityViaAccessor : false,
      faStripeName : "fscm"
    }
  • Add the Security/policy construct to filter the expenses by logged in userid in security side car file(i.e security.json5) for Expense BO
    • {
        $dt_version : "2310.0.550",
        dataSecurityEnabled : true,
        dataSecurity : {
          conditions: [
            {
              name: "EXM_LOGGEDINUSER_BASED_ACCESS",
              condition: "exmOwner.username=@securityContext.username",
            }
          ],
          rules : [ {
            privilege : "read",
            grants : [ {
              role : "ORA_EMPLOYEE_ABSTRACT",
              grantConditions: [
                  {
                    condition: "EXM_LOGGEDINUSER_BASED_ACCESS",
                    description: "Normal employee should be able to view his expenses"
                  }
                ]
            } ]
          }, {
            privilege : "create",
            grants : [ {
              role : "SUPER_USER_ROLE"
            } ]
          }, {
            privilege : "update",
            grants : [ {
              role : "SUPER_USER_ROLE"
            } ]
          }, {
            privilege : "delete",
            grants : [ {
              role : "SUPER_USER_ROLE"
            } ]
          } ]
        },
        allowAnonymousAccess : false,
        allowSkipDataSecurityViaAccessor : false,
        faStripeName : "fscm"
      }
  • Update the deployment & build the project

  • Update Security Construct With Create, Update And Delete Access For Expense Users 
    • Add the Security/policy construct to filter the expenses by logged in userid in security side car file(i.e security.json5) for Expense BO for update , delete . Just add the role based grant for create . Since we have no way to enforce personid based filter on the create use case
    • {
        $dt_version : "2310.0.550",
        dataSecurityEnabled : true,
        dataSecurity : {
          conditions: [
            {
              name: "EXM_LOGGEDINUSER_BASED_ACCESS",
              condition: "exmOwner.username=@securityContext.username",
            }
          ],
          rules : [ {
            privilege : "read",
            grants : [ {
              role : "ORA_EMPLOYEE_ABSTRACT",
              grantConditions: [
                  {
                    condition: "EXM_LOGGEDINUSER_BASED_ACCESS",
                    description: "Normal employee should be able to view his expenses"
                  }
                ]
            } ]
          }, {
            privilege : "create",
            grants : [ {
              role : "ORA_EMPLOYEE_ABSTRACT"
            } ]
          }, {
            privilege : "update",
            grants : [ {
              role : "ORA_EMPLOYEE_ABSTRACT",
              grantConditions: [
                    {
                      condition: "EXM_LOGGEDINUSER_BASED_ACCESS",
                      description: "Normal employee should be able to view his expenses"
                    }
              ]
            } ]
          }, {
            privilege : "delete",
            grants : [ {
              role : "ORA_EMPLOYEE_ABSTRACT",
              grantConditions: [
                    {
                      condition: "EXM_LOGGEDINUSER_BASED_ACCESS",
                      description: "Normal employee should be able to view his expenses"
                    }
              ]
            } ]
          } ]
        },
        allowAnonymousAccess : false,
        allowSkipDataSecurityViaAccessor : false,
        faStripeName : "fscm"
      }
  • Update security.json5 for all businessObjects except Expense with read access for 'ORA_EMPLOYEE_ABSTRACT'
    {
      $dt_version : "2310.0.550",
      dataSecurityEnabled : true,
      dataSecurity : {
        rules : [ {
          privilege : "read",
          grants : [ {
            role : "ORA_EMPLOYEE_ABSTRACT"
          } ]
        } ]
      },
      allowAnonymousAccess : false,
      allowSkipDataSecurityViaAccessor : false,
      faStripeName : "fscm"
    }
  • In previous section 'Produce Business Object' we had added a filter on Expense/bo.json5 to retrieve expenses of a predefined user. As we have implemented Security/policy construct which retrieves expenses only of the logged in user, we can remove this filter.
    where : "person_id = 100010026335799"
  • Update the deployment & build the project
BOSS Testing

https://confluence.oraclecorp.com/confluence/display/SPECTRA/Spectra+Weekly
https://confluence.oraclecorp.com/confluence/display/FFT/ERPM+Spectra+Training+2023

Comments