Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 77 additions & 0 deletions docs/plans/2026-04-10-get-order-status-design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
# Get Order Status — Design

**Issue:** #16
**Date:** 2026-04-10

## Summary

Add a `GET /orders/{uuid}` operation exposed through a domain-scoped facade:
`MontonioClient(config).orders().get(uuid)` returning the existing `OrderResponse`.

## Public API

```java
MontonioClient client = new MontonioClient(configuration);
OrderResponse order = client.orders().get("order-uuid");
```

### Classes Introduced

| Class | Package | Role |
|-------|---------|------|
| `MontonioClient` | `ee.bitweb.montonio.sdk` | Top-level facade, owns `MontonioHttpClient`, lazy-creates domain services |
| `OrderService` | `ee.bitweb.montonio.sdk.order` | Validates input, delegates `GET /orders/{uuid}` to HTTP client |

No new models — reuses `OrderResponse`, `PaymentIntent`, `PaymentStatus`, and all supporting types from #14.

### Dependency Flow

```text
MontonioClient → MontonioHttpClient → HttpClient (java.net)
OrderService (receives MontonioHttpClient)
```

## MontonioClient

- Constructor takes `MontonioSdkConfiguration`, creates `MontonioHttpClient` internally.
- Package-private constructor overload accepts `MontonioHttpClient` for testing.
- `orders()` uses lazy-cached initialization: creates `OrderService` on first call, reuses it after.
- Not thread-safe on `orders()` — benign race at worst. Matches typical single-threaded SDK usage.

## OrderService

- Package-private constructor receives `MontonioHttpClient` — users go through `MontonioClient`.
- `get(String uuid)` validates null/blank (throws `MontonioValidationException` with field `"uuid"`), then delegates to `httpClient.get("/orders/" + uuid, OrderResponse.class)`.
- No additional error handling — `MontonioHttpClient` already maps 4xx/5xx to `MontonioApiException` and network failures to `MontonioNetworkException`.

## Testing

### OrderServiceTest

Unit tests with stubbed HTTP client (same pattern as `MontonioHttpClientTest`):

| Test case | Setup | Assertion |
|-----------|-------|-----------|
| Successful retrieval | Stub 200, full JSON | All fields deserialized, including nested paymentIntents |
| Null UUID | `get(null)` | `MontonioValidationException` with field `"uuid"` |
| Blank UUID | `get(" ")` | `MontonioValidationException` with field `"uuid"` |
| Order not found | Stub 404 with error JSON | `MontonioApiException` with status 404 |
| Multiple payment intents | Stub 200, 2+ intents | List size and individual fields correct |
| Various payment statuses | Stub 200, PAID/PENDING/VOIDED | `PaymentStatus` enum deserialized correctly |

### MontonioClientTest

| Test case | Assertion |
|-----------|-----------|
| `orders()` returns non-null | Basic wiring |
| `orders()` returns same instance | Lazy-caching works |
| Constructor rejects null config | Validation error |

## Decisions

- **Domain-scoped facade** (`client.orders().get()`) over flat methods or standalone services — scales as more domains are added.
- **Lazy-cached domain services** — avoids allocating services never used, avoids re-allocating on every call.
- **Full `OrderResponse` return** — no slimmer DTO; callers pick fields they need.
- **Null/blank validation only** — no UUID format regex; let the API decide validity beyond that.
- **Package-private constructors** — testability without exposing internals to SDK consumers.
38 changes: 38 additions & 0 deletions src/main/java/ee/bitweb/montonio/sdk/MontonioClient.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package ee.bitweb.montonio.sdk;

import ee.bitweb.montonio.sdk.http.MontonioHttpClient;
import ee.bitweb.montonio.sdk.order.OrderService;

public class MontonioClient {

private final MontonioHttpClient httpClient;
private volatile OrderService orderService;

public MontonioClient(MontonioSdkConfiguration configuration) {
if (configuration == null) {
throw new NullPointerException("configuration must not be null");
}
this.httpClient = new MontonioHttpClient(configuration);
}

MontonioClient(MontonioHttpClient httpClient) {
if (httpClient == null) {
throw new NullPointerException("httpClient must not be null");
}
this.httpClient = httpClient;
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

public OrderService orders() {
OrderService local = orderService;
if (local == null) {
synchronized (this) {
local = orderService;
if (local == null) {
local = new OrderService(httpClient);
orderService = local;
}
}
}
return local;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,8 @@ public MontonioHttpClient(MontonioSdkConfiguration configuration) {
this.tokenProvider = new MontonioTokenProvider(configuration, objectMapper);
}

MontonioHttpClient(MontonioSdkConfiguration configuration, HttpClient httpClient,
MontonioTokenProvider tokenProvider) {
public MontonioHttpClient(MontonioSdkConfiguration configuration, HttpClient httpClient,
MontonioTokenProvider tokenProvider) {
this.configuration = configuration;
this.httpClient = httpClient;
this.objectMapper = createObjectMapper();
Expand Down
28 changes: 28 additions & 0 deletions src/main/java/ee/bitweb/montonio/sdk/order/OrderService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package ee.bitweb.montonio.sdk.order;

import ee.bitweb.montonio.sdk.exception.MontonioValidationException;
import ee.bitweb.montonio.sdk.http.MontonioHttpClient;
import ee.bitweb.montonio.sdk.order.response.OrderResponse;

public class OrderService {

private final MontonioHttpClient httpClient;

public OrderService(MontonioHttpClient httpClient) {
if (httpClient == null) {
throw new NullPointerException("httpClient must not be null");
}
this.httpClient = httpClient;
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

public OrderResponse get(String uuid) {
if (uuid == null) {
throw new MontonioValidationException("uuid", "must not be null or blank");
}
String trimmedUuid = uuid.trim();
if (trimmedUuid.isEmpty()) {
throw new MontonioValidationException("uuid", "must not be null or blank");
}
return httpClient.get("/orders/" + trimmedUuid, OrderResponse.class);
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
40 changes: 40 additions & 0 deletions src/test/java/ee/bitweb/montonio/sdk/MontonioClientTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package ee.bitweb.montonio.sdk;

import ee.bitweb.montonio.sdk.order.OrderService;
import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.*;

class MontonioClientTest {

@Test
void constructorRejectsNullConfiguration() {
MontonioSdkConfiguration nullConfig = null;
assertThrows(NullPointerException.class, () -> new MontonioClient(nullConfig));
}

@Test
void ordersReturnsNonNull() {
MontonioClient client = createClient();

assertNotNull(client.orders());
}

@Test
void ordersReturnsSameInstanceOnRepeatedCalls() {
MontonioClient client = createClient();

OrderService first = client.orders();
OrderService second = client.orders();

assertSame(first, second);
}

private MontonioClient createClient() {
MontonioSdkConfiguration configuration = MontonioSdkConfiguration.builder()
.accessKey("test-access-key")
.secretKey("test-secret-key-that-is-long-enough")
.build();
return new MontonioClient(configuration);
}
}
Loading