Skip to content
Merged
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
1 change: 1 addition & 0 deletions .github/CODEOWNERS
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
* @BitWeb/tech
1 change: 1 addition & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ repositories {

dependencies {
implementation 'tools.jackson.core:jackson-databind:3.0.1'
implementation 'jakarta.annotation:jakarta.annotation-api:3.0.0'
implementation 'com.auth0:java-jwt:4.5.0'

testImplementation platform('org.junit:junit-bom:5.14.3')
Expand Down
129 changes: 129 additions & 0 deletions docs/plans/2026-04-10-payment-order-models-design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
# Payment Order Models Design

**Date:** 2026-04-10
**Issue:** #14 — Payment order models — requests, responses, and enums
**Status:** Implemented

## Overview

Define all DTOs and enums needed for the Montonio payment order API. This covers the
`POST /orders` request/response and the `GET /orders/{uuid}` response, including nested
models for payment details, addresses, line items, and payment intents.

## Sources

Field definitions were derived by cross-referencing:

- [montonio/montonio-js](https://github.com/montonio/montonio-js) — official front-end SDK (enums, payment statuses)
- [maitkaa/montonio-client](https://github.com/maitkaa/montonio-client) — community TypeScript client (full Order, OrderResponse, PaymentIntent types)
- [montonio/montonio-shopware-php-sdk](https://github.com/montonio/montonio-shopware-php-sdk) — official PHP SDK (PaymentData, Address, LineItem, Payment structs)
- Issue #14 requirements

## Design Decisions

### Package layout — feature-based

```text
ee.bitweb.montonio.sdk.model/ — shared enums (Currency, Locale, etc.)
ee.bitweb.montonio.sdk.order.model/ — order-scoped models and enums
ee.bitweb.montonio.sdk.order.request/ — request DTOs
ee.bitweb.montonio.sdk.order.response/ — response DTOs
```

Reusable types (`Currency`, `Locale`, `PaymentMethodType`, `CardPaymentMethod`,
`WalletProvider`) live in `sdk.model`. Order-specific types (`PaymentStatus`, `Address`,
`LineItem`, `Payment`, `PaymentMethodOptions`) live under `sdk.order.model`.

### Enum serialization — `@JsonValue` with wire-value field

Each enum constant stores its API wire value in a `String value` field, exposed via
`@JsonValue`. This decouples Java naming conventions (UPPER_CASE) from the API's mixed
casing (e.g., `paymentInitiation`, `cardPayments`, `EUR`, `en`).

### Request DTOs — Lombok `@Builder` with constructor validation

Request DTOs use `@Builder` + `@Getter` with fail-fast validation in a `@JsonCreator`
constructor, consistent with `MontonioSdkConfiguration`. Required fields throw
`MontonioValidationException` if null/blank.

### Response DTOs — immutable with `@JsonCreator`

Response DTOs are `final` classes with all-args `@JsonCreator` constructors, `@Getter`,
and final fields. The `-parameters` compiler flag lets Jackson resolve constructor
parameter names without `@JsonProperty` annotations.

### `@JsonCreator` over `@Jacksonized`

Lombok's `@Jacksonized` generates `@com.fasterxml.jackson.databind.annotation.JsonDeserialize`,
but Jackson 3 moved that annotation to `tools.jackson.databind.annotation`. To avoid
compatibility issues, all classes use explicit `@JsonCreator` constructors.

### Monetary amounts — `BigDecimal` for requests, `String` for responses

Request DTOs use `BigDecimal` to avoid floating-point precision issues. Response DTOs
use `String` for `grandTotal`, `amount`, and `serviceFee` because that is what the API
returns on the wire.

### Nullability — `@jakarta.annotation.Nullable`

Optional fields are annotated with `@Nullable`. Absence of the annotation implies
non-null by contract.

### `PaymentMethodOptions` — single flat class

Rather than a type hierarchy mirroring the TypeScript union type
(`PaymentInitiationOptions | CardPaymentsOptions | ...`), all method-specific options
are merged into one class with all fields nullable. This avoids polymorphic
deserialization complexity for no real gain — the fields don't conflict.

### Refund fields — omitted

`OrderResponse` in the TypeScript client includes `refunds`, `availableForRefund`, and
`isRefundableType`. Refund-related fields are mostly omitted (only `isRefundableType`
is included) pending follow-up issue #30 for refund models. Jackson ignores unknown fields.

## File Inventory

### Shared enums (`sdk.model`)

| File | Wire values |
|------|-------------|
| `Currency.java` | `EUR`, `PLN` |
| `Locale.java` | `de`, `en`, `et`, `fi`, `lt`, `lv`, `pl`, `ru` |
| `PaymentMethodType.java` | `paymentInitiation`, `cardPayments`, `blik`, `bnpl`, `hirePurchase` |
| `CardPaymentMethod.java` | `card`, `wallet` |
| `WalletProvider.java` | `applePay`, `googlePay` |

### Order models (`order.model`)

| File | Description |
|------|-------------|
| `PaymentStatus.java` | 10 statuses: PENDING through AUTHORIZED |
| `Address.java` | 15 optional fields (name, email, phone, address, company) |
| `LineItem.java` | `name`, `quantity`, `finalPrice` (all required) |
| `Payment.java` | `method`, `currency`, `amount` (required) + `methodOptions`, `methodDisplay` |
| `PaymentMethodOptions.java` | 8 optional fields covering all payment method types |

### Request DTOs (`order.request`)

| File | Required fields |
|------|----------------|
| `CreateOrderRequest.java` | `merchantReference`, `returnUrl`, `notificationUrl`, `grandTotal`, `currency`, `payment` |

### Response DTOs (`order.response`)

| File | Key fields |
|------|------------|
| `CreateOrderResponse.java` | `uuid`, `paymentUrl` |
| `OrderResponse.java` | 21 fields including nested `paymentIntents`, `lineItems`, addresses |
| `PaymentIntent.java` | `uuid`, `paymentMethodType`, `amount`, `status`, `serviceFee`, etc. |

## Testing

Each class has a corresponding test class covering:

- Enum constant count and wire value assertions
- Serialization/deserialization round-trips via Jackson `ObjectMapper`
- Builder construction with all fields and required-only fields
- Validation — `MontonioValidationException` for null/blank required fields
- Unknown JSON field tolerance (deserialization with `FAIL_ON_UNKNOWN_PROPERTIES` disabled)
20 changes: 20 additions & 0 deletions src/main/java/ee/bitweb/montonio/sdk/model/CardPaymentMethod.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package ee.bitweb.montonio.sdk.model;

import com.fasterxml.jackson.annotation.JsonValue;

public enum CardPaymentMethod {

CARD("card"),
WALLET("wallet");

private final String value;

CardPaymentMethod(String value) {
this.value = value;
}

@JsonValue
public String getValue() {
return value;
}
}
20 changes: 20 additions & 0 deletions src/main/java/ee/bitweb/montonio/sdk/model/Currency.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package ee.bitweb.montonio.sdk.model;

import com.fasterxml.jackson.annotation.JsonValue;

public enum Currency {

EUR("EUR"),
PLN("PLN");

private final String value;

Currency(String value) {
this.value = value;
}

@JsonValue
public String getValue() {
return value;
}
}
26 changes: 26 additions & 0 deletions src/main/java/ee/bitweb/montonio/sdk/model/Locale.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package ee.bitweb.montonio.sdk.model;

import com.fasterxml.jackson.annotation.JsonValue;

public enum Locale {

DE("de"),
EN("en"),
ET("et"),
FI("fi"),
LT("lt"),
LV("lv"),
PL("pl"),
RU("ru");

private final String value;

Locale(String value) {
this.value = value;
}

@JsonValue
public String getValue() {
return value;
}
}
23 changes: 23 additions & 0 deletions src/main/java/ee/bitweb/montonio/sdk/model/PaymentMethodType.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package ee.bitweb.montonio.sdk.model;

import com.fasterxml.jackson.annotation.JsonValue;

public enum PaymentMethodType {

PAYMENT_INITIATION("paymentInitiation"),
CARD_PAYMENTS("cardPayments"),
BLIK("blik"),
BNPL("bnpl"),
HIRE_PURCHASE("hirePurchase");

private final String value;

PaymentMethodType(String value) {
this.value = value;
}

@JsonValue
public String getValue() {
return value;
}
}
20 changes: 20 additions & 0 deletions src/main/java/ee/bitweb/montonio/sdk/model/WalletProvider.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package ee.bitweb.montonio.sdk.model;

import com.fasterxml.jackson.annotation.JsonValue;

public enum WalletProvider {

APPLE_PAY("applePay"),
GOOGLE_PAY("googlePay");

private final String value;

WalletProvider(String value) {
this.value = value;
}

@JsonValue
public String getValue() {
return value;
}
}
91 changes: 91 additions & 0 deletions src/main/java/ee/bitweb/montonio/sdk/order/model/Address.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
package ee.bitweb.montonio.sdk.order.model;

import com.fasterxml.jackson.annotation.JsonCreator;
import jakarta.annotation.Nullable;
import lombok.Builder;
import lombok.Getter;

@Getter
@Builder
public class Address {

@Nullable
private final String firstName;

@Nullable
private final String lastName;

@Nullable
private final String email;

@Nullable
private final String phoneNumber;

@Nullable
private final String phoneCountry;

@Nullable
private final String addressLine1;

@Nullable
private final String addressLine2;

@Nullable
private final String locality;

@Nullable
private final String region;

@Nullable
private final String postalCode;

@Nullable
private final String country;

@Nullable
private final String companyName;

@Nullable
private final String companyLegalName;

@Nullable
private final String companyRegCode;

@Nullable
private final String companyVatNumber;

@JsonCreator
Address(
String firstName,
String lastName,
String email,
String phoneNumber,
String phoneCountry,
String addressLine1,
String addressLine2,
String locality,
String region,
String postalCode,
String country,
String companyName,
String companyLegalName,
String companyRegCode,
String companyVatNumber
) {
this.firstName = firstName;
this.lastName = lastName;
this.email = email;
this.phoneNumber = phoneNumber;
this.phoneCountry = phoneCountry;
this.addressLine1 = addressLine1;
this.addressLine2 = addressLine2;
this.locality = locality;
this.region = region;
this.postalCode = postalCode;
this.country = country;
this.companyName = companyName;
this.companyLegalName = companyLegalName;
this.companyRegCode = companyRegCode;
this.companyVatNumber = companyVatNumber;
}
}
33 changes: 33 additions & 0 deletions src/main/java/ee/bitweb/montonio/sdk/order/model/LineItem.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package ee.bitweb.montonio.sdk.order.model;

import com.fasterxml.jackson.annotation.JsonCreator;
import ee.bitweb.montonio.sdk.exception.MontonioValidationException;
import lombok.Builder;
import lombok.Getter;

import java.math.BigDecimal;

@Getter
@Builder
public class LineItem {

private final String name;
private final BigDecimal quantity;
private final BigDecimal finalPrice;

@JsonCreator
LineItem(String name, BigDecimal quantity, BigDecimal finalPrice) {
if (name == null || name.isBlank()) {
throw new MontonioValidationException("name", "must not be null or blank");
}
if (quantity == null) {
throw new MontonioValidationException("quantity", "must not be null");
}
if (finalPrice == null) {
throw new MontonioValidationException("finalPrice", "must not be null");
}
this.name = name;
this.quantity = quantity;
this.finalPrice = finalPrice;
}
}
Loading