Technology Blogs by SAP
Learn how to extend and personalize SAP applications. Follow the SAP technology blog for insights into SAP BTP, ABAP, SAP Analytics Cloud, SAP HANA, and more.
cancel
Showing results for 
Search instead for 
Did you mean: 
aschlosser
Employee
Employee
You've maybe seen Matt's post where he describes how Successfactors content can be mobilized and consumed using SAP Mobile Cards. This is a great first step in mobilizing content as it doesn't require any particular mobile app development skills, and no backend modifications either.

What if you wanted a bit more? Using the SAP Cloud Platform SDK for iOS you can consume the Successfactors OData APIs and build very flexible native apps as extensions to the Successfactors core system. I've expanded one of the Mobile Cards scenarios and built a mobile app that exposes payroll information to employees.

Backend and proxy classes


First, you need the backend. A sandbox API is made available at https://sandbox.api.sap.com/successfactors/odata/v2 (requires a valid API key from SAP API Business Hub). In general, you can either configure Mobile Services yourself to point to the destination and then start an app from scratch, or you could use the SAP Cloud Platform SDK for iOS Assistant to do all that for you. One particular problem with the Successfactors metadata is, that at the time of writing it doesn't specify the 'Scale' facet for its Decimal properties correctly (the scale is omitted, which means it defaults to '0'; however, in reality it is used for properties like amounts with fraction values). So, I decided to fix this manually and downloaded the metadata from https://sandbox.api.sap.com/successfactors/odata/v2/$metadata. I then fixed up the metadata and added the Scale="2" facet to the relevant property that I wanted to access from the EmployeePayrollRunResultsItems collection:
<Property Name="amount" Type="Edm.Decimal" Scale="2" Nullable="true" ...

Depending on your scenario, you may run into other similar problems; until the metadata is fixed centrally, just go ahead and modify the metadata locally so it matches the reality from the server. In case you missed a property and the server returns data that is not valid relative to the metadata, our OData parser will throw meaningful error messages that help you fix things up in iterations until all is good.

With that slightly modified metadata document I could go ahead and generate the proxy classes using our command line tool (installed from Assistant main menu -> Install Tools):
sapcpsdk-proxygenerator -m ./sf-metadata.xml  -d successfactors/Proxy\ Classes/successfactors/

The metadata of Successfactors is massive, so this will generate quite a bunch of proxy classes, which used to be a problem in earlier Xcode versions. However, the new build system shipping in Xcode 9.4 handles this quite well, and once you've been patient enough for the whole batch to compile okay once, subsequent delta builds will be fast as usual.

Connectivity


I'm using OAuth2SAMLBearerAssertion to establish single-sign-on across my SAP Cloud Platform account and the Successfactors instance that I'm testing with. This part is not possible with the APIs published on Business Hub - there you'll probably just want to experiment with a technical user.

This requires now establishing trust across the two cloud systems, which means copying some keys and certificates back and forth. We start in the SAP Cloud Platform Cockpit, under Security->Trust we need to copy the signing certificate. (In case you run with default configuration, i.e. SAP IAS - click 'edit' and change to 'custom' so you see all this data; then once you've copied the certificate please click 'cancel').



Now, we switch to the Successfactors admin console and create a new OAuth client for our native app.



This will give us the API Key that we'll now need to provide in the Mobile Services Cockpit on Cloud Platform to establish mutual trust. When setting up the connectivity for the mobile app, we'll set up the destination to point to our Successfactors API endpoint. The API key goes into the 'Client Key' and 'SAML Assertion Issuer' field, the SF company id goes into the 'Company Identifier'.



With this, we're done. When the app connects to Mobile Services, the server will produce a SAML assertion, sign it with the key that is trusted by Successfactors (as we've uploaded the certificate), Successfactors will return an OAuth2 token, and with that we can then make API calls in the context of the currently logged in user.

In my case, I was using SAP IAS, so the user that I used to connect to Mobile Services (a P# user) has to exist in SF now as well. I simply created a test employee in my case with that Person Id. As all these API calls now happen in the context of the end user, you also need to be careful with the permissions these users have. You definitely don't want folks to see other employees payroll data. So, when setting up the permission roles, make sure you're not exposing the OData API in admin, but in user mode and assign permissions only for 'everyone in self' target. Here's more information on permissions in Successfactors.


Retrieving the data


Now, I'm reproducing the same OData query that we're using for the SAP Mobile Card showing payroll information.
/odata/v2/EmployeePayrollRunResults?$format=json&$filter=userId%20eq%20'XXX'&$expand=employeePayrollRunResultsItems,userIdNav/empInfo/paymentInformationNav/toPaymentInformationDetailV3/bankNav&$top=1&$orderby=payDate%20desc

I use our fluent query API and proxy classes here.
        let query = DataQuery()

.filter(EmployeePayrollRunResults.userID == currentUser)

.expand(EmployeePayrollRunResults.employeePayrollRunResultsItems, EmployeePayrollRunResults.userIdNav.path(User.empInfo).path(EmpEmployment.paymentInformationNav).path(PaymentInformationV3.toPaymentInformationDetailV3).path(PaymentInformationDetailV3.bankNav))

.select(
EmployeePayrollRunResults.userIdNav.path(User.defaultFullName),
EmployeePayrollRunResults.payDate,
EmployeePayrollRunResults.userIdNav.path(User.empInfo).path(EmpEmployment.paymentInformationNav).path(PaymentInformationV3.toPaymentInformationDetailV3).path(PaymentInformationDetailV3.bankNav).path(Bank.bankName),
EmployeePayrollRunResults.userIdNav.path(User.empInfo).path(EmpEmployment.paymentInformationNav).path(PaymentInformationV3.toPaymentInformationDetailV3).path(PaymentInformationDetailV3.accountNumber),
EmployeePayrollRunResults.currency,
EmployeePayrollRunResults.employeePayrollRunResultsItems.path(EmployeePayrollRunResultsItems.amount),
EmployeePayrollRunResults.employeePayrollRunResultsItems.path(EmployeePayrollRunResultsItems.wageType)
)
.orderBy(EmployeePayrollRunResults.payDate, .descending)

This looks a bit verbose, but this is mainly do to the nested nature of the underlying OData service. This query expands the payroll items as well as the employee information and bank details. As these data structures contain quite a few properties, I'm selecting only what I need then, and order it all by the pay date (newest first).

Data visualization


My idea of visualizing this data is to use an object header to show some basic information on the user and target bank account for the pay out, as well as using the header to also show salary development over the last couple months in a chart. The table itself would have multiple sections and show the same payroll details that we saw on the Card for a single payroll run.

This is the object header definition:
        //Programmatically add an Object header to tableview
let objectHeader = FUIObjectHeader()
self.tableView.tableHeaderView = objectHeader

objectHeader.detailImageView.image = imageLiteral(resourceName: "Cash")
objectHeader.headlineLabel.text = "My Pay Check"
objectHeader.subheadlineLabel.text = (result.userIdNav?.defaultFullName)!
objectHeader.bodyLabel.text = (result.userIdNav?.empInfo?.paymentInformationNav[0].toPaymentInformationDetailV3[0].bankNav?.bankName)! + " (#" + (result.userIdNav?.empInfo?.paymentInformationNav[0].toPaymentInformationDetailV3[0].accountNumber!)! + ")"

let view = FUIObjectHeaderChartView()
view.title.text = "Historical Overview"
view.subtitle.text = "my net payout"
view.chartView.dataSource = self
view.isEnabled = false

objectHeader.detailContentView = view

and the implementation for the chart data source
     // MARK: - FUIChartViewDataSource

func chartView(_ chartView: FUIChartView, valueForSeries seriesIndex: Int, category categoryIndex: Int, dimension dimensionIndex: Int) -> Double? {
for item in payrollResults![payrollResults!.count - categoryIndex - 1].employeePayrollRunResultsItems {
if item.wageType == "NETPAY" {
return item.amount?.doubleValue()
}
}
return 0
}


func chartView(_ chartView: FUIChartView, numberOfValuesInSeries seriesIndex: Int) -> Int {
return payrollResults!.count
}


func numberOfSeries(in: FUIChartView) -> Int {
return 1
}

The table itself uses a simple text field form cell to show each payroll run in a separate section
    // MARK: - Table view data source

override func numberOfSections(in tableView: UITableView) -> Int {
return payrollResults!.count
}

override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return payrollResults![section].employeePayrollRunResultsItems.count
}

override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
return "Date: \(payrollResults![section].payDate!.month)/\(payrollResults![section].payDate!.year)"
}

override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: FUITextFieldFormCell.reuseIdentifier, for: indexPath) as! FUITextFieldFormCell

cell.isEditable = false
cell.keyName = (payrollResults![indexPath.section].employeePayrollRunResultsItems[indexPath.row].wageType ?? "n/a").capitalized
cell.value = (payrollResults![indexPath.section].employeePayrollRunResultsItems[indexPath.row].amount?.toString())! + payrollResults![indexPath.section].currency!

return cell
}

I've taken a few shortcuts here when it comes to data validation and force-unwrapping. This serves better readability of the code, in reality you may want to do more careful checking and fallback in case some properties aren't available.

The outcome


This was all relatively straight-forward use of SDK components to handle the OData backend and data visualization. Additionally - I didn't describe this here - I've also used the Flows framework to handle onboarding for me; simply the stuff that is generated by the Assistant, nothing special and in particular no extra work. I took the Card that showed the most recent payroll run, enhanced this by showing the full history, added nice visualization in a line chart, and now taking this further, adding more features, or enriching this with other data is a matter of spending time in Xcode and hacking away.

Here are a couple of screenshots how this looks like:


Next steps


This little experiment showed a few things. On the pro side, building an interface with slick controls, nice charts, and a great overall appearance was really easy. Using the existing controls and wiring it all up per our API documentation, I could get the visual part going very fast. Leveraging the Assistant, Mobile Services, and built-in Flows, also the infrastructure aspect of enabling access to the backend in a secure way was a walk in the park. Interacting with the OData service using generated proxy classes and our fluent query API was straight-forward as well, and with code completion in Xcode even this very complex data model was manageable. This leads to the not so easy part, though. In order to build Successfactors extensions, you really got to have a good handle on the underlying data model and functionality - otherwise you'll just get lost. I'm not blaming anybody here, this is just the way it is with complex business solutions. Just to give it a positive spin towards the end, once you got the hang of the backend model, going into native app building will feel like a walk in the park.

So, if you're a Successfactors expert and this inspired you to building a great native app, I'd be happy to learn about it. Feedback very welcome.

Andreas
8 Comments