# Java SDK

The Inovio gateway SDK for Java 11, with zero runtime dependencies and a BigDecimal money type.

Source: https://developer.inoviopay.com/sdks/java.html  
Markdown: https://developer.inoviopay.com/sdks/java.md
Repository: https://github.com/Inoviopay/inovio-gateway-sdk-java

The Inovio payment gateway for Java 11 and newer. Card transactions covering
authorize, capture, refund and tokenize, with a typed API and no runtime
dependencies.

> **The SDK is alpha**
> Version 0.1.0-alpha and **not published to Maven Central**. Build it from the
> public GitHub repository as shown below, and pin a commit until a tagged
> release lands.

## Status and install

**Build and install locally.** This is the path that works today, because the
repository has no published release tag for a build service to resolve:

```bash
git clone https://github.com/Inoviopay/inovio-gateway-sdk-java.git
cd inovio-gateway-sdk-java
mvn install
```

That puts `com.inoviopay:inovio-gateway-sdk:0.1.0-alpha.0` in your local
repository. Then depend on it:

```xml
<dependency>
  <groupId>com.inoviopay</groupId>
  <artifactId>inovio-gateway-sdk</artifactId>
  <version>0.1.0-alpha.0</version>
</dependency>
```

Or in Gradle:

```groovy
repositories { mavenLocal(); mavenCentral() }
dependencies { implementation 'com.inoviopay:inovio-gateway-sdk:0.1.0-alpha.0' }
```

**JitPack**, which builds Maven artifacts on demand straight from a public
GitHub repository, is the way to consume it without a local install once the
repository carries a release tag or you are willing to resolve a commit. Add
the repository and depend on the coordinate JitPack derives from the GitHub
path:

```xml
<repositories>
  <repository>
    <id>jitpack.io</id>
    <url>https://jitpack.io</url>
  </repository>
</repositories>

<dependency>
  <groupId>com.github.Inoviopay</groupId>
  <artifactId>inovio-gateway-sdk-java</artifactId>
  <version><!-- a commit sha, or a tag once one is published --></version>
</dependency>
```

Note that JitPack's coordinate is `com.github.Inoviopay:inovio-gateway-sdk-java`,
derived from the repository path, not the `com.inoviopay:inovio-gateway-sdk`
coordinate in the project's own `pom.xml`. Only the import statements are
unaffected; the package names come from the source either way.

## Requirements

**Java 11 or newer.** Zero runtime dependencies: HTTP goes through
`java.net.http`, and the flat key-value response is read by a small inline JSON
reader rather than pulling in Jackson for a response shape that does not need
it. JUnit is a test-scope dependency only.

## Quick start

```java
InovioClient client = new InovioClient(
    new InovioClient.Credentials(user, password, "123"));

TransactionRequest req = new TransactionRequest(
    PaymentMethods.card("4111111111111111", "122030", "123"),
    new LineItem("SKU-1", 1, Money.of("10.00", "USD")))
    .idempotency("ORDER-555");          // retry-safe by default

TransactionResult r = client.sale(req);

switch (r.status()) {
    case APPROVED: /* fulfil */ break;
    case DECLINED: /* r.outcome().service(), r.serviceClassification() */ break;
    case PENDING:  /* r.nextAction() — 3DS challenge, redirect, voucher */ break;
    case RUNNING:
    case FAILED:   break;
}
```

Configuration goes through a mutable `Options` object rather than a long
constructor:

```java
InovioClient.Options o = new InovioClient.Options();
o.environment = Transport.Environment.PRODUCTION;
o.endpoint    = "http://localhost:8080/payment/pmt_service.cfm";  // overrides environment
o.timeoutMs   = 30_000;
o.httpClient  = myInstrumentedClient;
o.siteKey     = System.getenv("INOVIO_SITE_KEY");   // required only for tokenize()

InovioClient client = new InovioClient(creds, o);
```

## Java 11 and the sealed hierarchy

This SDK targets **Java 11** for enterprise reach rather than newer-JDK
ergonomics. The cost is real and worth knowing:

| Type | Java 17+ would be | What this SDK does |
|---|---|---|
| `PaymentMethod` sealed | `sealed interface ... permits` | interface plus `final` implementations with **package-private constructors** |
| `Money`, refs, value types | `record` | hand-written `final` classes |
| Exhaustive status handling | switch expressions | `switch` plus an explicit `default` |

`PaymentMethod` is therefore **sealed by convention**. Every implementation is
`final` and its constructor is package-private, so the only way to build one is
`PaymentMethods.card(...)`, `.token(...)` or `.savedCard(...)`, and outside code
cannot add a variant. What is lost is *compiler-enforced* exhaustiveness: you
get no error for an unhandled variant in a `switch`, so write the `default`
branch yourself.

## Java-specific notes

**Amounts are `BigDecimal`.** `Money.of` has three overloads: `String`,
`BigDecimal`, and a `double` overload that exists **purely to reject floating
point** with a clear message. `Money.of(1.25, "USD")` throws; pass `"1.25"` or a
`BigDecimal`. `Money.equals` uses `compareTo`, so `"1.5"` equals `"1.50"`.

**All exceptions are unchecked.** `InovioException` extends `RuntimeException`,
so nothing forces a `throws` clause onto your call sites. Catch deliberately.

**The timeout exception is `GatewayTimeoutException`,** not
`TimeoutException`, so it is never confused with
`java.util.concurrent.TimeoutException`. It subclasses `TransportException` and
carries `xtlOrderId()` and `recoveryHint()`.

**`Card.toString()` prints only the last four digits**, so a card object landing
in a log line does not leak a PAN.

**Overloads stand in for optional arguments.** `capture()`, `refund()`,
`tokenize()` and `status()` each have a shorter overload. `status()` in
particular is overloaded on `Refs.OrderRef` and `Refs.XtlOrderId` rather than
taking a union type.

## Operations

Every public method on `InovioClient`, with the `REQUEST_ACTION` it maps to:

| Method | Action | Notes |
|---|---|---|
| `TransactionResult sale(TransactionRequest req)` | `CCAUTHCAP` | Authorize and capture in one step. See [Sale](https://developer.inoviopay.com/api/sale.md). |
| `TransactionResult authorize(TransactionRequest req)` | `CCAUTHORIZE` | Hold funds; capture later. See [Authorize](https://developer.inoviopay.com/api/authorize.md). |
| `TransactionResult capture(Refs.OrderRef order, Money amount)` | `CCCAPTURE` | Partial capture. See [Capture](https://developer.inoviopay.com/api/capture.md). |
| `TransactionResult capture(Refs.OrderRef order)` | `CCCAPTURE` | Capture in full. |
| `TransactionResult captureLineItem(Refs.OrderRef order, Refs.LineItemRef item, Money amount)` | `CCCAPTURE` | All three arguments are required. |
| `TransactionResult reverse(Refs.OrderRef order)` | `CCREVERSE` | Void the original authorization. See [Reversal](https://developer.inoviopay.com/api/reversal.md). |
| `TransactionResult reverseCapture(Refs.OrderRef order)` | `CCREVERSECAP` | Void a capture rather than the original auth. |
| `TransactionResult refund(Refs.OrderRef order, Money amount)` | `CCCREDIT` | Partial refund. See [Credit](https://developer.inoviopay.com/api/credit.md). |
| `TransactionResult refund(Refs.OrderRef order)` | `CCCREDIT` | Refund in full. |
| `TransactionResult forceCredit(TransactionRequest req)` | `CCCREDIT` + `FORCE_CREDIT` | Credit with no referenced original. Needs MID provisioning. |
| `OrderStatus status(Refs.OrderRef order)` | `CCSTATUS` | Order-level net position. See [Status](https://developer.inoviopay.com/api/status.md). |
| `OrderStatus status(Refs.XtlOrderId xtlOrder)` | `CCSTATUS` | The same, keyed by your own order id. |
| `TransactionResult updateOrder(Refs.OrderRef order, OrderUpdate update)` | `CCTRANSUPDATE` | Attach a receipt and UDF metadata. |
| `Tokenize.Result tokenize(Card card)` | token service | Needs `Options.siteKey`. See [Tokenization](https://developer.inoviopay.com/api/tokenization.md). |
| `Tokenize.Result tokenize(Card card, String uniqueId)` | token service | The same, with your own request id. |
| `HealthResult testAuth()` | `TESTAUTH` | Verify credentials without transacting. |
| `HealthResult testAvailability()` | `TESTGW` | Verify gateway availability. Safe to poll. |

`captureLineItem()` requires the parent order as well as the line item. The
gateway rejects `REQUEST_REF_PO_LI_ID` on its own with API 113 "Invalid Data",
and `LineItemRef` does not carry its order, so both must be passed. This was
verified against the live gateway.

> **Two capabilities are PHP-only in this alpha**
> `reverse()` and `reverseCapture()` take only the order reference here and
> never send `CREDIT_ON_FAIL`, and there is no `threeDSecure()` sub-client. See
> [Reverse versus credit](https://developer.inoviopay.com/sdks/concepts.md#reverse-versus-credit) and
> [3D Secure](https://developer.inoviopay.com/sdks/concepts.md#3d-secure) for what to do instead.

## Handling results

`r.status()` carries the answer. A decline is a return value, not an exception,
so branch on the status rather than wrapping in try/catch for the payment
outcome:

```java
TransactionResult r = client.sale(req);

show("status", r.status());
show("order", r.orderRef() == null ? "-" : r.orderRef().poId());
show("amount", r.amount() == null ? "-"
    : r.amount().toWire() + " " + r.amount().currency());

switch (r.status()) {
    case APPROVED:
        show("next", "fulfil the order");
        break;
    case DECLINED:
        // The service tier carries the decline taxonomy dunning needs.
        boolean retry = r.serviceClassification() != null
            && r.serviceClassification().retryable();
        show("next", retry ? "retry later" : "do not retry");
        break;
    case PENDING:
        show("next", "complete "
            + (r.nextAction() == null ? "?" : r.nextAction().kind()));
        break;
    default:
        show("next", "inspect result.outcome()");
}
```

Because the status enum is not compiler-checked for exhaustiveness on Java 11,
that `default` branch is doing real work. Do not omit it.

Reference keys sit flat on the result, not inside a nested bag, because they are
the ones you reach for most: `client.capture(r.orderRef(), amount)`. The
available accessors are `orderRef()`, `xtlOrderRef()`, `transactionId()`,
`requestId()`, `batchId()`, `customerRef()`, `savedCardRef()`,
`membershipRef()` and `lineItemRefs()`.

The gateway sends codes; the SDK adds labels. `r.serviceClassification().retryable()`,
`.terminal()` and `.stopRecurring()`, plus `r.avs().classification()`, are labels
the SDK derives from the response codes, not values the gateway sent. The codes
themselves are on `r.outcome()` and the untouched wire fields on `r.raw()`. See
[Outcome tiers](https://developer.inoviopay.com/sdks/concepts.md#outcome-tiers).

Exceptions extend `InovioException`, itself a `RuntimeException`:
`AuthenticationException`, `ValidationException` (carrying the offending
`refField`), `ConfigurationException`, `TransportException`,
`GatewayTimeoutException` (a subclass of `TransportException`) and
`RateLimitException`.

## Tokenization

`tokenize()` exchanges a PAN for a single-use `TOKEN_GUID` that replaces
`PMT_NUMB` on a later sale or authorize. It hits `token_service.cfm` with HMAC
header authentication rather than username and password, so it needs
`Options.siteKey`. Without it the call throws a `ValidationException` before any
network traffic, and the service itself would answer error 121.

```java
// Tokenize on the site that holds the HMAC key...
Tokenize.Result t = tokenClient().tokenize(
    PaymentMethods.card(PAN, EXPIRY, CVV));

t.token().guid();
t.tokenReqId();     // quote this to support

// BIN metadata is best-effort — null when the BIN is not in the table.
t.card().brand;
t.card().bank;

// The token replaces the PAN ONLY: expiry (and CVV) still travel with it,
// which tokenize() carries forward for you.
TransactionRequest req = Harness.request("TOK", "10.00");
req.paymentMethod = t.token();
TransactionResult sale = client().sale(req);
```

The signed message **excludes the PAN**, contrary to what the v4.14 PDF says.
The gateway validates `hmac_sha256(timestamp || unique_id || site_id, site_key)`,
verified against the live token service. The SDK signs the way the gateway
behaves.

For a merchant-hosted signature endpoint that lets a browser post the PAN
directly, use the static helpers on their own:

```java
import com.inoviopay.gateway.Tokenize;

String timestamp = Tokenize.timestamp();                                  // YYYYMMDDHHMMSS UTC
String signature = Tokenize.signRequest(siteKey, timestamp, uniqueId, siteId);
// Return {signature, timestamp, siteId} to the browser. Never the site key.
```

> **tokenize() runs on your server**
> The card number passes through your infrastructure. The browser Hosted Fields
> client that would keep it in the cardholder's browser is not yet available.
> See [Where the card number goes](https://developer.inoviopay.com/sdks/index.md#where-the-card-number-goes).

## 3D Secure

This SDK carries the request-side and result-side 3DS surface, but not the
server-leg driver.

What is here:

- **`RequestParts.BrowserData`** on a request, which the request builder maps to
  the `P3DS_BROWSER_LANGUAGE`, `USER_AGENT_XTL` and `P3DS_BROWSER_HEADER`
  fields. **These are required for gateway 3DS**: without them the gateway
  silently skips authentication entirely.
- **The challenge next action.** When a result is `PENDING` because a challenge
  is required, `r.nextAction().kind()` is `"threeDSChallenge"` and the object
  carries the redirect URL, the JWT and the processor transaction id, which is
  everything your page needs to post into the visible iframe.

```java
req.browser = new RequestParts.BrowserData(language, userAgent, acceptHeader);
TransactionResult r = client.sale(req);

if (r.status() == TransactionStatus.PENDING
        && r.nextAction() != null
        && "threeDSChallenge".equals(r.nextAction().kind())) {
    r.nextAction().redirectUrl;
    r.nextAction().jwt;
    // Browser: POST jwt to redirectUrl in a visible iframe.
}
```

> **The 3DS server legs are PHP-only in this alpha**
> There is no `client.threeDSecure()` here. The prepare leg against
> `3dsrequest.cfm` and the completion leg after the ACS challenge have to be
> driven against [the 3DS endpoints](https://developer.inoviopay.com/api/3ds-gateway.md) directly, or through
> the [PHP SDK](https://developer.inoviopay.com/sdks/php.md#3d-secure), which implements both. This SDK's
> `BrowserData` also omits the optional EMVCo device fields (colour depth,
> screen dimensions, time-zone offset) that the PHP one carries.

## Timeouts and reconciliation

A timeout means the state is **unknown**, not failed. The gateway may have
approved the charge and lost the response.

```java
try {
    client.sale(req.idempotency("ORDER-555"));
} catch (GatewayTimeoutException e) {
    log.warn(e.recoveryHint());
    OrderStatus actual = client.status(Refs.xtlOrder(e.xtlOrderId()));
    // Do NOT retry blindly — that risks a double charge.
}
```

`idempotency("ORDER-555")` defaults the mode to `RETURN_ORIGINAL`, so a retry of
the same request returns the original result rather than charging twice.

`status()` is also the reconciliation primitive. Partial captures, refunds and
voids are separate transactions sharing one order, so net position is an
order-level question:

```java
// Build a multi-leg order: authorize 100, capture 60, refund 10.
TransactionResult order = Harness.seedOrder(c, "STATUS", false, "100.00");
c.capture(order.orderRef(), Money.of("60.00", "USD"));
c.refund(order.orderRef(), Money.of("10.00", "USD"));

OrderStatus s = c.status(order.orderRef());

s.transactions().size();      // every leg: auth, captures, refunds, voids
s.authorized().toWire();
s.captured().toWire();
s.refunded().toWire();
s.net().toWire();             // captured - refunded
s.outstanding().toWire();     // authorized - captured

// You can also look an order up by YOUR id:
//   c.status(Refs.xtlOrder("ORDER-555"));
```

## Running the conformance tests

```bash
mvn test                              # 19 conformance tests
python3 scripts/generate_enums.py     # regenerate enums from spec/spec-enums.json
```

The suite replays the shared cross-language corpus in
`spec/conformance-fixtures.json`, the same fixtures the other three SDKs run,
which is what keeps the four honest about producing identically shaped results.

The enums under `src/main/java/com/inoviopay/gateway/enums/` are generated from
`spec/spec-enums.json`, extracted from the v4.14 specification appendices. Do
not hand-edit them; regenerate.

## Examples in the repo

Fourteen runnable files in `examples/`, one per operation. They are real,
executed code rather than markdown snippets, so they cannot silently drift from
the API.

```bash
javac -cp target/classes -d target/examples examples/*.java \
  && java -cp target/classes:target/examples RunAll     # all 14, mock transport

java -cp target/classes:target/examples Example03Sale   # just one
```

| File | Operation |
|---|---|
| `Example01TestAvailability.java` | `testAvailability()`, the `TESTGW` health check |
| `Example02TestAuth.java` | `testAuth()`, credential verification with no transaction |
| `Example03Sale.java` | `sale()`, authorize and capture in one step |
| `Example04Authorize.java` | `authorize()`, holding funds and keeping the order ref |
| `Example05Capture.java` | `capture()`, full or partial |
| `Example06CaptureLineItem.java` | `captureLineItem()`, which needs order, item and amount |
| `Example07Reverse.java` | `reverse()`, voiding an authorization |
| `Example08ReverseCapture.java` | `reverseCapture()`, voiding a capture pre-settlement |
| `Example09Refund.java` | `refund()`, returning captured funds |
| `Example10ForceCredit.java` | `forceCredit()`, which needs MID provisioning |
| `Example11Status.java` | `status()`, reconciliation and net position |
| `Example12UpdateOrder.java` | `updateOrder()`, attaching receipts for compliance |
| `Example13Tokenize.java` | `tokenize()`, PAN to single-use token |
| `Example14TimeoutRecovery.java` | The pattern that prevents double charges |

`Harness.java` holds the shared mock transport and request builder;
`RunAll.java` runs the set.

By default they run against a mock transport: no credentials, no network, no
money moves, safe in CI. Set `INOVIO_LIVE=1` plus credentials to run the same
code against a real gateway:

```bash
INOVIO_LIVE=1 \
INOVIO_USER=... INOVIO_PASS=... INOVIO_SITE_ID=... INOVIO_MERCH_ACCT_ID=... \
INOVIO_SITE_KEY=...            # token service HMAC key
INOVIO_TOKEN_SITE_ID=...       # only if tokenizing on a different site
java -cp target/classes:target/examples RunAll
```

Live mode creates **real transactions**, so point it at a test environment.
Running live is worth doing before you trust an integration: mocks only replay
responses you already believed in, so they cannot catch request-side errors.
Every example that operates on an existing order builds its own first, rather
than hardcoding an id that resolves only against a mock.

Two examples need provisioning that a fresh account will not have.
`forceCredit` fails with API 104 "Invalid service action" unless the MID has
`FORCE_CREDIT` enabled, which is an authentication-tier error rather than a
decline. `tokenize` needs the per-site HMAC key from Inovio support, which may
live on a different site than your gateway credentials.
