Parsware
All articles

Parsware Platform

Call an outside service

callService lets a server-side script post to another system, like a payment provider. The script never holds the address, only a service name. Each environment sets where that name points, so Test can use a sandbox and Production the real thing, and an environment that sets nothing reaches nothing. We took payments in a sample order app, and saw an answer, a refusal and a missing setting.

The Order payment script calling callService with par_payment_service and a JSON body, then checking answer.ok, next to the title "Call an outside service"

A plugin step, a Custom API or a scheduled job can call another system with callService. It posts a body to a service and gives you back the answer. We used it to take a payment before an order is saved.

We used the Calling out: a payment service and an email sample. Import it from samples/solutions/business-rules-outbound-calls. We imported it into our Test environment, because it has its own Order table.

A service name, not an address

Here is the call in the sample:

var answer = callService('par_payment_service', body);

The first argument isn't a URL. It's the name of an environment variable, and the variable holds the address. The solution declares the variable; each environment sets its own value:

The Environment variables screen of the Calling out solution in the Test environment: Mail service, par_mail_service, Text, Not set; and Payment service, par_payment_service, Text, Not set

Just after import, both say Not set. A solution carries the declaration, never the value, because the value is different in every environment: a payment sandbox in Test, the real provider in Production.

This is also what keeps outbound calls safe. A script can only reach an address that an administrator put into a variable in this environment. There's no function that takes a URL, so a script can't be pointed at an internal server by whoever writes it.

With nothing set, nothing is called

We added an order, ORD-1001 for 250, set Status to Paying and saved:

The New Order form with ORD-1001, 250, dana@example.com and Paying, and a red message: The environment variable 'par_payment_service' has no value in this environment, so there is nowhere to call. An administrator sets it under Environment Variables.

The save is refused, and the message names the variable and says who can fix it.

Set the address

Select the variable and choose Set value:

The Set value dialog for Payment service: This value belongs to this environment alone. It is never exported with the solution, and importing the solution again will not overwrite it. The Value box holds http://localhost:5990/pay

The value belongs to this environment only. It isn't exported with the solution, and importing the solution again won't overwrite it.

For this article we pointed it at a small stand-in provider running on our machine. It answers 200 with a payment reference, or 402 for a declined card, depending on the address.

Taking the payment

We saved ORD-1001 as Paying again:

The saved order ORD-1001: Status Paid, Payment reference PAY-338481, and Confirmation email: Not sent, because par_mail_service has no value

The step called the service, and the order came back Paid, with the reference the provider sent. The provider received:

{"reference":"ORD-1001","amount":250}

(Confirmation email says Not sent, because we haven't set the mail service yet. That's the next article.)

The script

The code is in a solution script, Order payment:

The Order payment script in the editor: func takePayment() checks the status is Paying and there is no reference yet, builds a JSON body with a template, calls callService('par_payment_service', body), then if answer.ok sets par_payment_reference to answer.body and par_status to Paid, else throwValidationError with the status

callService gives back an object:

Property What it holds
ok true for a 2xx answer
status the HTTP status, like 200 or 402
body what the service sent back, as text
truncated true if the answer was too long and was cut

The body you send is text. Here it's JSON, built with a template so the amount goes in as a number.

Before the write, on purpose

The payment is taken in a PreOperation step: before the order is written, inside the same save. That matters. Taking the money and recording that it was taken are one act. If the call fails, the save is refused and nothing is stored. A step that saved first and paid after could leave an order marked Paid with no payment behind it.

The cost is that the save waits for the provider. A call gets ten seconds, and no more.

The sample has two such steps, one when an order is created and one when it's changed, because a plugin step watches one kind of save. Both import takePayment from the script:

The plugin steps of the solution's 1.1.0 version: Take payment before saving a new order (Create, Before the write) and Email the confirmation for a new paid order (Create, After the write)

An answer isn't a failure

We pointed the variable at the address that declines, and saved a new order, ORD-1002:

The New Order form for ORD-1002 with Status Paying and the message The payment service refused this order (402).

A 402 is an answer: the provider said no. callService doesn't stop the script; it hands the answer back, and the script decides. This one refuses the save with the status in the message, and ORD-1002 isn't stored.

Only when there's no answer at all does callService stop the script by itself: no variable, nobody listening, or no answer in ten seconds. A script can't do anything useful with those.

Then we set the variable to not-a-url:

The same form with the message The endpoint configured for 'par_payment_service' is not an absolute http or https address.

A different message, because it's a different problem. A declined card is the customer's; a missing or broken address is for whoever runs the environment.

Two bugs we found and fixed

Writing this article, we found two problems in the sample itself:

  • A new order saved as Paying was never charged. The sample only had the step for changed orders, but the form's own hint said to set Paying and save. So the first thing you'd try stored an unpaid order and said nothing. The sample now has a step for new orders too, and both import the same script, which is why the code above is a solution script.
  • A failed email made a saved order look unsaved. That one's in the next article.

Both are fixed in version 1.1.0 of the sample, with tests.

Where it works

callService is available in plugin steps, Custom APIs and scheduled jobs, which run on the server. It isn't in form rules or command buttons, which run in the browser, or in process steps yet.

Try it

Import the sample, set the payment service to any address that answers 200, and save an order as Paying. Then point it somewhere that answers 402 and try again.

Next: send an email from a script, with sendEmail.