From fc494d7e37aab945af87c62102e2d4765d73562f Mon Sep 17 00:00:00 2001 From: Rain Ramm Date: Fri, 10 Apr 2026 08:21:55 +0000 Subject: [PATCH 1/5] =?UTF-8?q?Add=20payment=20order=20models=20=E2=80=94?= =?UTF-8?q?=20requests,=20responses,=20and=20enums?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implements all DTOs and enums needed for the Montonio payment order API: - Shared enums: Currency, Locale, PaymentMethodType, CardPaymentMethod, WalletProvider - Order models: PaymentStatus, Address, LineItem, Payment, PaymentMethodOptions - Request DTO: CreateOrderRequest with builder and fail-fast validation - Response DTOs: CreateOrderResponse, OrderResponse, PaymentIntent (immutable) Field definitions cross-referenced from the official PHP SDK, JS SDK, and community TypeScript client. Uses @JsonCreator for Jackson 3 deserialization (Lombok @Jacksonized incompatible with Jackson 3). Closes #14 Co-Authored-By: Claude Opus 4.6 (1M context) --- build.gradle | 1 + .../2026-04-10-payment-order-models-design.md | 129 ++++++++++ .../montonio/sdk/model/CardPaymentMethod.java | 20 ++ .../bitweb/montonio/sdk/model/Currency.java | 20 ++ .../ee/bitweb/montonio/sdk/model/Locale.java | 26 ++ .../montonio/sdk/model/PaymentMethodType.java | 23 ++ .../montonio/sdk/model/WalletProvider.java | 20 ++ .../montonio/sdk/order/model/Address.java | 91 +++++++ .../montonio/sdk/order/model/LineItem.java | 33 +++ .../montonio/sdk/order/model/Payment.java | 50 ++++ .../sdk/order/model/PaymentMethodOptions.java | 59 +++++ .../sdk/order/model/PaymentStatus.java | 28 +++ .../sdk/order/request/CreateOrderRequest.java | 82 +++++++ .../order/response/CreateOrderResponse.java | 17 ++ .../sdk/order/response/OrderResponse.java | 88 +++++++ .../sdk/order/response/PaymentIntent.java | 49 ++++ .../sdk/model/CardPaymentMethodTest.java | 40 +++ .../montonio/sdk/model/CurrencyTest.java | 47 ++++ .../bitweb/montonio/sdk/model/LocaleTest.java | 46 ++++ .../sdk/model/PaymentMethodTypeTest.java | 43 ++++ .../sdk/model/WalletProviderTest.java | 40 +++ .../montonio/sdk/order/model/AddressTest.java | 111 +++++++++ .../sdk/order/model/LineItemTest.java | 101 ++++++++ .../order/model/PaymentMethodOptionsTest.java | 100 ++++++++ .../sdk/order/model/PaymentStatusTest.java | 48 ++++ .../montonio/sdk/order/model/PaymentTest.java | 117 +++++++++ .../order/request/CreateOrderRequestTest.java | 229 ++++++++++++++++++ .../response/CreateOrderResponseTest.java | 70 ++++++ .../sdk/order/response/OrderResponseTest.java | 210 ++++++++++++++++ .../sdk/order/response/PaymentIntentTest.java | 127 ++++++++++ 30 files changed, 2065 insertions(+) create mode 100644 docs/plans/2026-04-10-payment-order-models-design.md create mode 100644 src/main/java/ee/bitweb/montonio/sdk/model/CardPaymentMethod.java create mode 100644 src/main/java/ee/bitweb/montonio/sdk/model/Currency.java create mode 100644 src/main/java/ee/bitweb/montonio/sdk/model/Locale.java create mode 100644 src/main/java/ee/bitweb/montonio/sdk/model/PaymentMethodType.java create mode 100644 src/main/java/ee/bitweb/montonio/sdk/model/WalletProvider.java create mode 100644 src/main/java/ee/bitweb/montonio/sdk/order/model/Address.java create mode 100644 src/main/java/ee/bitweb/montonio/sdk/order/model/LineItem.java create mode 100644 src/main/java/ee/bitweb/montonio/sdk/order/model/Payment.java create mode 100644 src/main/java/ee/bitweb/montonio/sdk/order/model/PaymentMethodOptions.java create mode 100644 src/main/java/ee/bitweb/montonio/sdk/order/model/PaymentStatus.java create mode 100644 src/main/java/ee/bitweb/montonio/sdk/order/request/CreateOrderRequest.java create mode 100644 src/main/java/ee/bitweb/montonio/sdk/order/response/CreateOrderResponse.java create mode 100644 src/main/java/ee/bitweb/montonio/sdk/order/response/OrderResponse.java create mode 100644 src/main/java/ee/bitweb/montonio/sdk/order/response/PaymentIntent.java create mode 100644 src/test/java/ee/bitweb/montonio/sdk/model/CardPaymentMethodTest.java create mode 100644 src/test/java/ee/bitweb/montonio/sdk/model/CurrencyTest.java create mode 100644 src/test/java/ee/bitweb/montonio/sdk/model/LocaleTest.java create mode 100644 src/test/java/ee/bitweb/montonio/sdk/model/PaymentMethodTypeTest.java create mode 100644 src/test/java/ee/bitweb/montonio/sdk/model/WalletProviderTest.java create mode 100644 src/test/java/ee/bitweb/montonio/sdk/order/model/AddressTest.java create mode 100644 src/test/java/ee/bitweb/montonio/sdk/order/model/LineItemTest.java create mode 100644 src/test/java/ee/bitweb/montonio/sdk/order/model/PaymentMethodOptionsTest.java create mode 100644 src/test/java/ee/bitweb/montonio/sdk/order/model/PaymentStatusTest.java create mode 100644 src/test/java/ee/bitweb/montonio/sdk/order/model/PaymentTest.java create mode 100644 src/test/java/ee/bitweb/montonio/sdk/order/request/CreateOrderRequestTest.java create mode 100644 src/test/java/ee/bitweb/montonio/sdk/order/response/CreateOrderResponseTest.java create mode 100644 src/test/java/ee/bitweb/montonio/sdk/order/response/OrderResponseTest.java create mode 100644 src/test/java/ee/bitweb/montonio/sdk/order/response/PaymentIntentTest.java diff --git a/build.gradle b/build.gradle index 88f2c57..15acdb9 100644 --- a/build.gradle +++ b/build.gradle @@ -30,6 +30,7 @@ repositories { dependencies { implementation 'tools.jackson.core:jackson-databind:3.0.1' + implementation 'jakarta.annotation:jakarta.annotation-api:3.0.0' testImplementation platform('org.junit:junit-bom:5.14.3') testImplementation 'org.junit.jupiter:junit-jupiter' diff --git a/docs/plans/2026-04-10-payment-order-models-design.md b/docs/plans/2026-04-10-payment-order-models-design.md new file mode 100644 index 0000000..d720531 --- /dev/null +++ b/docs/plans/2026-04-10-payment-order-models-design.md @@ -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 + +``` +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 a future refund models issue. 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) diff --git a/src/main/java/ee/bitweb/montonio/sdk/model/CardPaymentMethod.java b/src/main/java/ee/bitweb/montonio/sdk/model/CardPaymentMethod.java new file mode 100644 index 0000000..7dc77b7 --- /dev/null +++ b/src/main/java/ee/bitweb/montonio/sdk/model/CardPaymentMethod.java @@ -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; + } +} diff --git a/src/main/java/ee/bitweb/montonio/sdk/model/Currency.java b/src/main/java/ee/bitweb/montonio/sdk/model/Currency.java new file mode 100644 index 0000000..b566713 --- /dev/null +++ b/src/main/java/ee/bitweb/montonio/sdk/model/Currency.java @@ -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; + } +} diff --git a/src/main/java/ee/bitweb/montonio/sdk/model/Locale.java b/src/main/java/ee/bitweb/montonio/sdk/model/Locale.java new file mode 100644 index 0000000..79c607d --- /dev/null +++ b/src/main/java/ee/bitweb/montonio/sdk/model/Locale.java @@ -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; + } +} diff --git a/src/main/java/ee/bitweb/montonio/sdk/model/PaymentMethodType.java b/src/main/java/ee/bitweb/montonio/sdk/model/PaymentMethodType.java new file mode 100644 index 0000000..a52b7ef --- /dev/null +++ b/src/main/java/ee/bitweb/montonio/sdk/model/PaymentMethodType.java @@ -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; + } +} diff --git a/src/main/java/ee/bitweb/montonio/sdk/model/WalletProvider.java b/src/main/java/ee/bitweb/montonio/sdk/model/WalletProvider.java new file mode 100644 index 0000000..1ff61e1 --- /dev/null +++ b/src/main/java/ee/bitweb/montonio/sdk/model/WalletProvider.java @@ -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; + } +} diff --git a/src/main/java/ee/bitweb/montonio/sdk/order/model/Address.java b/src/main/java/ee/bitweb/montonio/sdk/order/model/Address.java new file mode 100644 index 0000000..d16d774 --- /dev/null +++ b/src/main/java/ee/bitweb/montonio/sdk/order/model/Address.java @@ -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; + } +} diff --git a/src/main/java/ee/bitweb/montonio/sdk/order/model/LineItem.java b/src/main/java/ee/bitweb/montonio/sdk/order/model/LineItem.java new file mode 100644 index 0000000..db0bebf --- /dev/null +++ b/src/main/java/ee/bitweb/montonio/sdk/order/model/LineItem.java @@ -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; + } +} diff --git a/src/main/java/ee/bitweb/montonio/sdk/order/model/Payment.java b/src/main/java/ee/bitweb/montonio/sdk/order/model/Payment.java new file mode 100644 index 0000000..e52aee0 --- /dev/null +++ b/src/main/java/ee/bitweb/montonio/sdk/order/model/Payment.java @@ -0,0 +1,50 @@ +package ee.bitweb.montonio.sdk.order.model; + +import com.fasterxml.jackson.annotation.JsonCreator; +import ee.bitweb.montonio.sdk.exception.MontonioValidationException; +import ee.bitweb.montonio.sdk.model.Currency; +import ee.bitweb.montonio.sdk.model.PaymentMethodType; +import jakarta.annotation.Nullable; +import lombok.Builder; +import lombok.Getter; + +import java.math.BigDecimal; + +@Getter +@Builder +public class Payment { + + private final PaymentMethodType method; + private final Currency currency; + private final BigDecimal amount; + + @Nullable + private final PaymentMethodOptions methodOptions; + + @Nullable + private final String methodDisplay; + + @JsonCreator + Payment( + PaymentMethodType method, + Currency currency, + BigDecimal amount, + PaymentMethodOptions methodOptions, + String methodDisplay + ) { + if (method == null) { + throw new MontonioValidationException("method", "must not be null"); + } + if (currency == null) { + throw new MontonioValidationException("currency", "must not be null"); + } + if (amount == null) { + throw new MontonioValidationException("amount", "must not be null"); + } + this.method = method; + this.currency = currency; + this.amount = amount; + this.methodOptions = methodOptions; + this.methodDisplay = methodDisplay; + } +} diff --git a/src/main/java/ee/bitweb/montonio/sdk/order/model/PaymentMethodOptions.java b/src/main/java/ee/bitweb/montonio/sdk/order/model/PaymentMethodOptions.java new file mode 100644 index 0000000..f1fffa9 --- /dev/null +++ b/src/main/java/ee/bitweb/montonio/sdk/order/model/PaymentMethodOptions.java @@ -0,0 +1,59 @@ +package ee.bitweb.montonio.sdk.order.model; + +import com.fasterxml.jackson.annotation.JsonCreator; +import ee.bitweb.montonio.sdk.model.CardPaymentMethod; +import ee.bitweb.montonio.sdk.model.Locale; +import ee.bitweb.montonio.sdk.model.WalletProvider; +import jakarta.annotation.Nullable; +import lombok.Builder; +import lombok.Getter; + +@Getter +@Builder +public class PaymentMethodOptions { + + @Nullable + private final String preferredProvider; + + @Nullable + private final String preferredCountry; + + @Nullable + private final Locale preferredLocale; + + @Nullable + private final CardPaymentMethod preferredMethod; + + @Nullable + private final WalletProvider preferredWallet; + + @Nullable + private final String paymentDescription; + + @Nullable + private final String paymentReference; + + @Nullable + private final Integer period; + + @JsonCreator + PaymentMethodOptions( + String preferredProvider, + String preferredCountry, + Locale preferredLocale, + CardPaymentMethod preferredMethod, + WalletProvider preferredWallet, + String paymentDescription, + String paymentReference, + Integer period + ) { + this.preferredProvider = preferredProvider; + this.preferredCountry = preferredCountry; + this.preferredLocale = preferredLocale; + this.preferredMethod = preferredMethod; + this.preferredWallet = preferredWallet; + this.paymentDescription = paymentDescription; + this.paymentReference = paymentReference; + this.period = period; + } +} diff --git a/src/main/java/ee/bitweb/montonio/sdk/order/model/PaymentStatus.java b/src/main/java/ee/bitweb/montonio/sdk/order/model/PaymentStatus.java new file mode 100644 index 0000000..768766a --- /dev/null +++ b/src/main/java/ee/bitweb/montonio/sdk/order/model/PaymentStatus.java @@ -0,0 +1,28 @@ +package ee.bitweb.montonio.sdk.order.model; + +import com.fasterxml.jackson.annotation.JsonValue; + +public enum PaymentStatus { + + PENDING("PENDING"), + PAID("PAID"), + VOIDED("VOIDED"), + PARTIALLY_REFUNDED("PARTIALLY_REFUNDED"), + REFUNDED("REFUNDED"), + CANCELED("CANCELED"), + ABANDONED("ABANDONED"), + DECLINED("DECLINED"), + SETTLED("SETTLED"), + AUTHORIZED("AUTHORIZED"); + + private final String value; + + PaymentStatus(String value) { + this.value = value; + } + + @JsonValue + public String getValue() { + return value; + } +} diff --git a/src/main/java/ee/bitweb/montonio/sdk/order/request/CreateOrderRequest.java b/src/main/java/ee/bitweb/montonio/sdk/order/request/CreateOrderRequest.java new file mode 100644 index 0000000..877e145 --- /dev/null +++ b/src/main/java/ee/bitweb/montonio/sdk/order/request/CreateOrderRequest.java @@ -0,0 +1,82 @@ +package ee.bitweb.montonio.sdk.order.request; + +import com.fasterxml.jackson.annotation.JsonCreator; +import ee.bitweb.montonio.sdk.exception.MontonioValidationException; +import ee.bitweb.montonio.sdk.model.Currency; +import ee.bitweb.montonio.sdk.model.Locale; +import ee.bitweb.montonio.sdk.order.model.Address; +import ee.bitweb.montonio.sdk.order.model.LineItem; +import ee.bitweb.montonio.sdk.order.model.Payment; +import jakarta.annotation.Nullable; +import lombok.Builder; +import lombok.Getter; + +import java.math.BigDecimal; +import java.util.List; + +@Getter +@Builder +public class CreateOrderRequest { + + private final String merchantReference; + private final String returnUrl; + private final String notificationUrl; + private final BigDecimal grandTotal; + private final Currency currency; + private final Payment payment; + + @Nullable + private final Locale locale; + + @Nullable + private final Address billingAddress; + + @Nullable + private final Address shippingAddress; + + @Nullable + private final List lineItems; + + @JsonCreator + CreateOrderRequest( + String merchantReference, + String returnUrl, + String notificationUrl, + BigDecimal grandTotal, + Currency currency, + Payment payment, + Locale locale, + Address billingAddress, + Address shippingAddress, + List lineItems + ) { + if (merchantReference == null || merchantReference.isBlank()) { + throw new MontonioValidationException("merchantReference", "must not be null or blank"); + } + if (returnUrl == null || returnUrl.isBlank()) { + throw new MontonioValidationException("returnUrl", "must not be null or blank"); + } + if (notificationUrl == null || notificationUrl.isBlank()) { + throw new MontonioValidationException("notificationUrl", "must not be null or blank"); + } + if (grandTotal == null) { + throw new MontonioValidationException("grandTotal", "must not be null"); + } + if (currency == null) { + throw new MontonioValidationException("currency", "must not be null"); + } + if (payment == null) { + throw new MontonioValidationException("payment", "must not be null"); + } + this.merchantReference = merchantReference; + this.returnUrl = returnUrl; + this.notificationUrl = notificationUrl; + this.grandTotal = grandTotal; + this.currency = currency; + this.payment = payment; + this.locale = locale; + this.billingAddress = billingAddress; + this.shippingAddress = shippingAddress; + this.lineItems = lineItems; + } +} diff --git a/src/main/java/ee/bitweb/montonio/sdk/order/response/CreateOrderResponse.java b/src/main/java/ee/bitweb/montonio/sdk/order/response/CreateOrderResponse.java new file mode 100644 index 0000000..3db5b64 --- /dev/null +++ b/src/main/java/ee/bitweb/montonio/sdk/order/response/CreateOrderResponse.java @@ -0,0 +1,17 @@ +package ee.bitweb.montonio.sdk.order.response; + +import com.fasterxml.jackson.annotation.JsonCreator; +import lombok.Getter; + +@Getter +public final class CreateOrderResponse { + + private final String uuid; + private final String paymentUrl; + + @JsonCreator + public CreateOrderResponse(String uuid, String paymentUrl) { + this.uuid = uuid; + this.paymentUrl = paymentUrl; + } +} diff --git a/src/main/java/ee/bitweb/montonio/sdk/order/response/OrderResponse.java b/src/main/java/ee/bitweb/montonio/sdk/order/response/OrderResponse.java new file mode 100644 index 0000000..7ad9974 --- /dev/null +++ b/src/main/java/ee/bitweb/montonio/sdk/order/response/OrderResponse.java @@ -0,0 +1,88 @@ +package ee.bitweb.montonio.sdk.order.response; + +import com.fasterxml.jackson.annotation.JsonCreator; +import ee.bitweb.montonio.sdk.model.Currency; +import ee.bitweb.montonio.sdk.model.Locale; +import ee.bitweb.montonio.sdk.model.PaymentMethodType; +import ee.bitweb.montonio.sdk.order.model.Address; +import ee.bitweb.montonio.sdk.order.model.LineItem; +import ee.bitweb.montonio.sdk.order.model.PaymentStatus; +import jakarta.annotation.Nullable; +import lombok.Getter; + +import java.util.List; + +@Getter +public final class OrderResponse { + + private final String uuid; + private final PaymentStatus paymentStatus; + private final Locale locale; + private final String merchantReference; + private final String merchantReferenceDisplay; + private final String merchantReturnUrl; + private final String merchantNotificationUrl; + private final String grandTotal; + private final Currency currency; + private final PaymentMethodType paymentMethodType; + private final String storeUuid; + private final List paymentIntents; + private final List lineItems; + private final Address billingAddress; + private final Address shippingAddress; + private final String expiresAt; + private final String createdAt; + private final String storeName; + private final String businessName; + private final String paymentUrl; + + @Nullable + private final Boolean isRefundableType; + + @JsonCreator + public OrderResponse( + String uuid, + PaymentStatus paymentStatus, + Locale locale, + String merchantReference, + String merchantReferenceDisplay, + String merchantReturnUrl, + String merchantNotificationUrl, + String grandTotal, + Currency currency, + PaymentMethodType paymentMethodType, + String storeUuid, + List paymentIntents, + List lineItems, + Address billingAddress, + Address shippingAddress, + String expiresAt, + String createdAt, + String storeName, + String businessName, + String paymentUrl, + Boolean isRefundableType + ) { + this.uuid = uuid; + this.paymentStatus = paymentStatus; + this.locale = locale; + this.merchantReference = merchantReference; + this.merchantReferenceDisplay = merchantReferenceDisplay; + this.merchantReturnUrl = merchantReturnUrl; + this.merchantNotificationUrl = merchantNotificationUrl; + this.grandTotal = grandTotal; + this.currency = currency; + this.paymentMethodType = paymentMethodType; + this.storeUuid = storeUuid; + this.paymentIntents = paymentIntents; + this.lineItems = lineItems; + this.billingAddress = billingAddress; + this.shippingAddress = shippingAddress; + this.expiresAt = expiresAt; + this.createdAt = createdAt; + this.storeName = storeName; + this.businessName = businessName; + this.paymentUrl = paymentUrl; + this.isRefundableType = isRefundableType; + } +} diff --git a/src/main/java/ee/bitweb/montonio/sdk/order/response/PaymentIntent.java b/src/main/java/ee/bitweb/montonio/sdk/order/response/PaymentIntent.java new file mode 100644 index 0000000..b1e2edd --- /dev/null +++ b/src/main/java/ee/bitweb/montonio/sdk/order/response/PaymentIntent.java @@ -0,0 +1,49 @@ +package ee.bitweb.montonio.sdk.order.response; + +import com.fasterxml.jackson.annotation.JsonCreator; +import ee.bitweb.montonio.sdk.model.Currency; +import ee.bitweb.montonio.sdk.model.PaymentMethodType; +import ee.bitweb.montonio.sdk.order.model.PaymentStatus; +import jakarta.annotation.Nullable; +import lombok.Getter; + +import java.util.Map; + +@Getter +public final class PaymentIntent { + + private final String uuid; + private final PaymentMethodType paymentMethodType; + private final String amount; + private final Currency currency; + private final PaymentStatus status; + private final String serviceFee; + private final Currency serviceFeeCurrency; + private final String createdAt; + + @Nullable + private final Map paymentMethodMetadata; + + @JsonCreator + public PaymentIntent( + String uuid, + PaymentMethodType paymentMethodType, + String amount, + Currency currency, + PaymentStatus status, + String serviceFee, + Currency serviceFeeCurrency, + String createdAt, + Map paymentMethodMetadata + ) { + this.uuid = uuid; + this.paymentMethodType = paymentMethodType; + this.amount = amount; + this.currency = currency; + this.status = status; + this.serviceFee = serviceFee; + this.serviceFeeCurrency = serviceFeeCurrency; + this.createdAt = createdAt; + this.paymentMethodMetadata = paymentMethodMetadata; + } +} diff --git a/src/test/java/ee/bitweb/montonio/sdk/model/CardPaymentMethodTest.java b/src/test/java/ee/bitweb/montonio/sdk/model/CardPaymentMethodTest.java new file mode 100644 index 0000000..1324fae --- /dev/null +++ b/src/test/java/ee/bitweb/montonio/sdk/model/CardPaymentMethodTest.java @@ -0,0 +1,40 @@ +package ee.bitweb.montonio.sdk.model; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; +import tools.jackson.databind.ObjectMapper; +import tools.jackson.databind.json.JsonMapper; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class CardPaymentMethodTest { + + private final ObjectMapper mapper = new JsonMapper(); + + @Test + void enumContainsExpectedValues() { + assertEquals(2, CardPaymentMethod.values().length); + } + + @Test + void wireValues() { + assertEquals("card", CardPaymentMethod.CARD.getValue()); + assertEquals("wallet", CardPaymentMethod.WALLET.getValue()); + } + + @ParameterizedTest + @EnumSource(CardPaymentMethod.class) + void serializesToWireValue(CardPaymentMethod method) throws Exception { + String json = mapper.writeValueAsString(method); + assertEquals("\"" + method.getValue() + "\"", json); + } + + @ParameterizedTest + @EnumSource(CardPaymentMethod.class) + void deserializesFromWireValue(CardPaymentMethod method) throws Exception { + String json = "\"" + method.getValue() + "\""; + CardPaymentMethod result = mapper.readValue(json, CardPaymentMethod.class); + assertEquals(method, result); + } +} diff --git a/src/test/java/ee/bitweb/montonio/sdk/model/CurrencyTest.java b/src/test/java/ee/bitweb/montonio/sdk/model/CurrencyTest.java new file mode 100644 index 0000000..705b427 --- /dev/null +++ b/src/test/java/ee/bitweb/montonio/sdk/model/CurrencyTest.java @@ -0,0 +1,47 @@ +package ee.bitweb.montonio.sdk.model; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; +import tools.jackson.databind.ObjectMapper; +import tools.jackson.databind.json.JsonMapper; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +class CurrencyTest { + + private final ObjectMapper mapper = new JsonMapper(); + + @Test + void enumContainsExpectedValues() { + assertEquals(2, Currency.values().length); + assertNotNull(Currency.valueOf("EUR")); + assertNotNull(Currency.valueOf("PLN")); + } + + @Test + void eurHasCorrectWireValue() { + assertEquals("EUR", Currency.EUR.getValue()); + } + + @Test + void plnHasCorrectWireValue() { + assertEquals("PLN", Currency.PLN.getValue()); + } + + @ParameterizedTest + @EnumSource(Currency.class) + void serializesToWireValue(Currency currency) throws Exception { + String json = mapper.writeValueAsString(currency); + assertEquals("\"" + currency.getValue() + "\"", json); + } + + @ParameterizedTest + @EnumSource(Currency.class) + void deserializesFromWireValue(Currency currency) throws Exception { + String json = "\"" + currency.getValue() + "\""; + Currency result = mapper.readValue(json, Currency.class); + assertEquals(currency, result); + } +} diff --git a/src/test/java/ee/bitweb/montonio/sdk/model/LocaleTest.java b/src/test/java/ee/bitweb/montonio/sdk/model/LocaleTest.java new file mode 100644 index 0000000..344303f --- /dev/null +++ b/src/test/java/ee/bitweb/montonio/sdk/model/LocaleTest.java @@ -0,0 +1,46 @@ +package ee.bitweb.montonio.sdk.model; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; +import tools.jackson.databind.ObjectMapper; +import tools.jackson.databind.json.JsonMapper; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class LocaleTest { + + private final ObjectMapper mapper = new JsonMapper(); + + @Test + void enumContainsExpectedValues() { + assertEquals(8, Locale.values().length); + } + + @Test + void wireValuesAreLowercase() { + assertEquals("de", Locale.DE.getValue()); + assertEquals("en", Locale.EN.getValue()); + assertEquals("et", Locale.ET.getValue()); + assertEquals("fi", Locale.FI.getValue()); + assertEquals("lt", Locale.LT.getValue()); + assertEquals("lv", Locale.LV.getValue()); + assertEquals("pl", Locale.PL.getValue()); + assertEquals("ru", Locale.RU.getValue()); + } + + @ParameterizedTest + @EnumSource(Locale.class) + void serializesToWireValue(Locale locale) throws Exception { + String json = mapper.writeValueAsString(locale); + assertEquals("\"" + locale.getValue() + "\"", json); + } + + @ParameterizedTest + @EnumSource(Locale.class) + void deserializesFromWireValue(Locale locale) throws Exception { + String json = "\"" + locale.getValue() + "\""; + Locale result = mapper.readValue(json, Locale.class); + assertEquals(locale, result); + } +} diff --git a/src/test/java/ee/bitweb/montonio/sdk/model/PaymentMethodTypeTest.java b/src/test/java/ee/bitweb/montonio/sdk/model/PaymentMethodTypeTest.java new file mode 100644 index 0000000..53175b8 --- /dev/null +++ b/src/test/java/ee/bitweb/montonio/sdk/model/PaymentMethodTypeTest.java @@ -0,0 +1,43 @@ +package ee.bitweb.montonio.sdk.model; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; +import tools.jackson.databind.ObjectMapper; +import tools.jackson.databind.json.JsonMapper; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class PaymentMethodTypeTest { + + private final ObjectMapper mapper = new JsonMapper(); + + @Test + void enumContainsExpectedValues() { + assertEquals(5, PaymentMethodType.values().length); + } + + @Test + void wireValuesAreCamelCase() { + assertEquals("paymentInitiation", PaymentMethodType.PAYMENT_INITIATION.getValue()); + assertEquals("cardPayments", PaymentMethodType.CARD_PAYMENTS.getValue()); + assertEquals("blik", PaymentMethodType.BLIK.getValue()); + assertEquals("bnpl", PaymentMethodType.BNPL.getValue()); + assertEquals("hirePurchase", PaymentMethodType.HIRE_PURCHASE.getValue()); + } + + @ParameterizedTest + @EnumSource(PaymentMethodType.class) + void serializesToWireValue(PaymentMethodType type) throws Exception { + String json = mapper.writeValueAsString(type); + assertEquals("\"" + type.getValue() + "\"", json); + } + + @ParameterizedTest + @EnumSource(PaymentMethodType.class) + void deserializesFromWireValue(PaymentMethodType type) throws Exception { + String json = "\"" + type.getValue() + "\""; + PaymentMethodType result = mapper.readValue(json, PaymentMethodType.class); + assertEquals(type, result); + } +} diff --git a/src/test/java/ee/bitweb/montonio/sdk/model/WalletProviderTest.java b/src/test/java/ee/bitweb/montonio/sdk/model/WalletProviderTest.java new file mode 100644 index 0000000..7486cb1 --- /dev/null +++ b/src/test/java/ee/bitweb/montonio/sdk/model/WalletProviderTest.java @@ -0,0 +1,40 @@ +package ee.bitweb.montonio.sdk.model; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; +import tools.jackson.databind.ObjectMapper; +import tools.jackson.databind.json.JsonMapper; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class WalletProviderTest { + + private final ObjectMapper mapper = new JsonMapper(); + + @Test + void enumContainsExpectedValues() { + assertEquals(2, WalletProvider.values().length); + } + + @Test + void wireValues() { + assertEquals("applePay", WalletProvider.APPLE_PAY.getValue()); + assertEquals("googlePay", WalletProvider.GOOGLE_PAY.getValue()); + } + + @ParameterizedTest + @EnumSource(WalletProvider.class) + void serializesToWireValue(WalletProvider provider) throws Exception { + String json = mapper.writeValueAsString(provider); + assertEquals("\"" + provider.getValue() + "\"", json); + } + + @ParameterizedTest + @EnumSource(WalletProvider.class) + void deserializesFromWireValue(WalletProvider provider) throws Exception { + String json = "\"" + provider.getValue() + "\""; + WalletProvider result = mapper.readValue(json, WalletProvider.class); + assertEquals(provider, result); + } +} diff --git a/src/test/java/ee/bitweb/montonio/sdk/order/model/AddressTest.java b/src/test/java/ee/bitweb/montonio/sdk/order/model/AddressTest.java new file mode 100644 index 0000000..4fd6d28 --- /dev/null +++ b/src/test/java/ee/bitweb/montonio/sdk/order/model/AddressTest.java @@ -0,0 +1,111 @@ +package ee.bitweb.montonio.sdk.order.model; + +import org.junit.jupiter.api.Test; +import tools.jackson.databind.DeserializationFeature; +import tools.jackson.databind.ObjectMapper; +import tools.jackson.databind.json.JsonMapper; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +class AddressTest { + + private final ObjectMapper mapper = JsonMapper.builder() + .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES) + .build(); + + @Test + void buildWithAllFields() { + Address address = Address.builder() + .firstName("John") + .lastName("Doe") + .email("john@example.com") + .phoneNumber("123456789") + .phoneCountry("EE") + .addressLine1("Kai 1") + .addressLine2("Apt 2") + .locality("Tallinn") + .region("Harjumaa") + .postalCode("10111") + .country("EE") + .companyName("Acme") + .companyLegalName("Acme OÜ") + .companyRegCode("12345678") + .companyVatNumber("EE123456789") + .build(); + + assertEquals("John", address.getFirstName()); + assertEquals("Doe", address.getLastName()); + assertEquals("john@example.com", address.getEmail()); + assertEquals("123456789", address.getPhoneNumber()); + assertEquals("EE", address.getPhoneCountry()); + assertEquals("Kai 1", address.getAddressLine1()); + assertEquals("Apt 2", address.getAddressLine2()); + assertEquals("Tallinn", address.getLocality()); + assertEquals("Harjumaa", address.getRegion()); + assertEquals("10111", address.getPostalCode()); + assertEquals("EE", address.getCountry()); + assertEquals("Acme", address.getCompanyName()); + assertEquals("Acme OÜ", address.getCompanyLegalName()); + assertEquals("12345678", address.getCompanyRegCode()); + assertEquals("EE123456789", address.getCompanyVatNumber()); + } + + @Test + void buildWithNoFieldsDefaultsToNull() { + Address address = Address.builder().build(); + + assertNull(address.getFirstName()); + assertNull(address.getLastName()); + assertNull(address.getEmail()); + assertNull(address.getPhoneNumber()); + assertNull(address.getPhoneCountry()); + assertNull(address.getAddressLine1()); + assertNull(address.getAddressLine2()); + assertNull(address.getLocality()); + assertNull(address.getRegion()); + assertNull(address.getPostalCode()); + assertNull(address.getCountry()); + assertNull(address.getCompanyName()); + assertNull(address.getCompanyLegalName()); + assertNull(address.getCompanyRegCode()); + assertNull(address.getCompanyVatNumber()); + } + + @Test + void serializationRoundTrip() throws Exception { + Address address = Address.builder() + .firstName("John") + .lastName("Doe") + .email("john@example.com") + .addressLine1("Kai 1") + .locality("Tallinn") + .region("Harjumaa") + .postalCode("10111") + .country("EE") + .build(); + + String json = mapper.writeValueAsString(address); + Address deserialized = mapper.readValue(json, Address.class); + + assertEquals(address.getFirstName(), deserialized.getFirstName()); + assertEquals(address.getLastName(), deserialized.getLastName()); + assertEquals(address.getEmail(), deserialized.getEmail()); + assertEquals(address.getAddressLine1(), deserialized.getAddressLine1()); + assertEquals(address.getLocality(), deserialized.getLocality()); + assertEquals(address.getRegion(), deserialized.getRegion()); + assertEquals(address.getPostalCode(), deserialized.getPostalCode()); + assertEquals(address.getCountry(), deserialized.getCountry()); + } + + @Test + void deserializesWithUnknownFieldsIgnored() throws Exception { + String json = """ + {"firstName":"John","unknownField":"value"} + """; + + Address address = mapper.readValue(json, Address.class); + + assertEquals("John", address.getFirstName()); + } +} diff --git a/src/test/java/ee/bitweb/montonio/sdk/order/model/LineItemTest.java b/src/test/java/ee/bitweb/montonio/sdk/order/model/LineItemTest.java new file mode 100644 index 0000000..38e9b3d --- /dev/null +++ b/src/test/java/ee/bitweb/montonio/sdk/order/model/LineItemTest.java @@ -0,0 +1,101 @@ +package ee.bitweb.montonio.sdk.order.model; + +import ee.bitweb.montonio.sdk.exception.MontonioValidationException; +import org.junit.jupiter.api.Test; +import tools.jackson.databind.DeserializationFeature; +import tools.jackson.databind.ObjectMapper; +import tools.jackson.databind.json.JsonMapper; + +import java.math.BigDecimal; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +class LineItemTest { + + private final ObjectMapper mapper = JsonMapper.builder() + .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES) + .build(); + + @Test + void buildWithAllFields() { + LineItem item = LineItem.builder() + .name("Hoverboard") + .quantity(new BigDecimal("2")) + .finalPrice(new BigDecimal("49.99")) + .build(); + + assertEquals("Hoverboard", item.getName()); + assertEquals(new BigDecimal("2"), item.getQuantity()); + assertEquals(new BigDecimal("49.99"), item.getFinalPrice()); + } + + @Test + void buildWithNullNameThrows() { + MontonioValidationException exception = assertThrows( + MontonioValidationException.class, + () -> LineItem.builder() + .quantity(BigDecimal.ONE) + .finalPrice(BigDecimal.TEN) + .build() + ); + + assertEquals("name", exception.getField()); + } + + @Test + void buildWithBlankNameThrows() { + MontonioValidationException exception = assertThrows( + MontonioValidationException.class, + () -> LineItem.builder() + .name(" ") + .quantity(BigDecimal.ONE) + .finalPrice(BigDecimal.TEN) + .build() + ); + + assertEquals("name", exception.getField()); + } + + @Test + void buildWithNullQuantityThrows() { + MontonioValidationException exception = assertThrows( + MontonioValidationException.class, + () -> LineItem.builder() + .name("Item") + .finalPrice(BigDecimal.TEN) + .build() + ); + + assertEquals("quantity", exception.getField()); + } + + @Test + void buildWithNullFinalPriceThrows() { + MontonioValidationException exception = assertThrows( + MontonioValidationException.class, + () -> LineItem.builder() + .name("Item") + .quantity(BigDecimal.ONE) + .build() + ); + + assertEquals("finalPrice", exception.getField()); + } + + @Test + void serializationRoundTrip() throws Exception { + LineItem item = LineItem.builder() + .name("Hoverboard") + .quantity(new BigDecimal("1")) + .finalPrice(new BigDecimal("100.00")) + .build(); + + String json = mapper.writeValueAsString(item); + LineItem deserialized = mapper.readValue(json, LineItem.class); + + assertEquals(item.getName(), deserialized.getName()); + assertEquals(0, item.getQuantity().compareTo(deserialized.getQuantity())); + assertEquals(0, item.getFinalPrice().compareTo(deserialized.getFinalPrice())); + } +} diff --git a/src/test/java/ee/bitweb/montonio/sdk/order/model/PaymentMethodOptionsTest.java b/src/test/java/ee/bitweb/montonio/sdk/order/model/PaymentMethodOptionsTest.java new file mode 100644 index 0000000..c7912e3 --- /dev/null +++ b/src/test/java/ee/bitweb/montonio/sdk/order/model/PaymentMethodOptionsTest.java @@ -0,0 +1,100 @@ +package ee.bitweb.montonio.sdk.order.model; + +import ee.bitweb.montonio.sdk.model.CardPaymentMethod; +import ee.bitweb.montonio.sdk.model.Locale; +import ee.bitweb.montonio.sdk.model.WalletProvider; +import org.junit.jupiter.api.Test; +import tools.jackson.databind.DeserializationFeature; +import tools.jackson.databind.ObjectMapper; +import tools.jackson.databind.json.JsonMapper; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +class PaymentMethodOptionsTest { + + private final ObjectMapper mapper = JsonMapper.builder() + .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES) + .build(); + + @Test + void buildWithNoFieldsDefaultsToNull() { + PaymentMethodOptions options = PaymentMethodOptions.builder().build(); + + assertNull(options.getPreferredProvider()); + assertNull(options.getPreferredCountry()); + assertNull(options.getPreferredLocale()); + assertNull(options.getPreferredMethod()); + assertNull(options.getPreferredWallet()); + assertNull(options.getPaymentDescription()); + assertNull(options.getPaymentReference()); + assertNull(options.getPeriod()); + } + + @Test + void buildPaymentInitiationOptions() { + PaymentMethodOptions options = PaymentMethodOptions.builder() + .preferredProvider("LHVBEE22") + .preferredCountry("EE") + .preferredLocale(Locale.ET) + .paymentDescription("Payment for order 123") + .paymentReference("RF123456") + .build(); + + assertEquals("LHVBEE22", options.getPreferredProvider()); + assertEquals("EE", options.getPreferredCountry()); + assertEquals(Locale.ET, options.getPreferredLocale()); + assertEquals("Payment for order 123", options.getPaymentDescription()); + assertEquals("RF123456", options.getPaymentReference()); + } + + @Test + void buildCardPaymentOptions() { + PaymentMethodOptions options = PaymentMethodOptions.builder() + .preferredMethod(CardPaymentMethod.CARD) + .preferredLocale(Locale.EN) + .build(); + + assertEquals(CardPaymentMethod.CARD, options.getPreferredMethod()); + assertEquals(Locale.EN, options.getPreferredLocale()); + } + + @Test + void buildWalletOptions() { + PaymentMethodOptions options = PaymentMethodOptions.builder() + .preferredMethod(CardPaymentMethod.WALLET) + .preferredWallet(WalletProvider.APPLE_PAY) + .preferredLocale(Locale.EN) + .build(); + + assertEquals(CardPaymentMethod.WALLET, options.getPreferredMethod()); + assertEquals(WalletProvider.APPLE_PAY, options.getPreferredWallet()); + } + + @Test + void buildBnplOptions() { + PaymentMethodOptions options = PaymentMethodOptions.builder() + .period(3) + .build(); + + assertEquals(3, options.getPeriod()); + } + + @Test + void serializationRoundTrip() throws Exception { + PaymentMethodOptions options = PaymentMethodOptions.builder() + .preferredProvider("LHVBEE22") + .preferredCountry("EE") + .preferredLocale(Locale.ET) + .paymentDescription("Payment for order 123") + .build(); + + String json = mapper.writeValueAsString(options); + PaymentMethodOptions deserialized = mapper.readValue(json, PaymentMethodOptions.class); + + assertEquals(options.getPreferredProvider(), deserialized.getPreferredProvider()); + assertEquals(options.getPreferredCountry(), deserialized.getPreferredCountry()); + assertEquals(options.getPreferredLocale(), deserialized.getPreferredLocale()); + assertEquals(options.getPaymentDescription(), deserialized.getPaymentDescription()); + } +} diff --git a/src/test/java/ee/bitweb/montonio/sdk/order/model/PaymentStatusTest.java b/src/test/java/ee/bitweb/montonio/sdk/order/model/PaymentStatusTest.java new file mode 100644 index 0000000..60e3695 --- /dev/null +++ b/src/test/java/ee/bitweb/montonio/sdk/order/model/PaymentStatusTest.java @@ -0,0 +1,48 @@ +package ee.bitweb.montonio.sdk.order.model; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; +import tools.jackson.databind.ObjectMapper; +import tools.jackson.databind.json.JsonMapper; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class PaymentStatusTest { + + private final ObjectMapper mapper = new JsonMapper(); + + @Test + void enumContainsExpectedValues() { + assertEquals(10, PaymentStatus.values().length); + } + + @Test + void wireValuesAreUpperCase() { + assertEquals("PENDING", PaymentStatus.PENDING.getValue()); + assertEquals("PAID", PaymentStatus.PAID.getValue()); + assertEquals("VOIDED", PaymentStatus.VOIDED.getValue()); + assertEquals("PARTIALLY_REFUNDED", PaymentStatus.PARTIALLY_REFUNDED.getValue()); + assertEquals("REFUNDED", PaymentStatus.REFUNDED.getValue()); + assertEquals("CANCELED", PaymentStatus.CANCELED.getValue()); + assertEquals("ABANDONED", PaymentStatus.ABANDONED.getValue()); + assertEquals("DECLINED", PaymentStatus.DECLINED.getValue()); + assertEquals("SETTLED", PaymentStatus.SETTLED.getValue()); + assertEquals("AUTHORIZED", PaymentStatus.AUTHORIZED.getValue()); + } + + @ParameterizedTest + @EnumSource(PaymentStatus.class) + void serializesToWireValue(PaymentStatus status) throws Exception { + String json = mapper.writeValueAsString(status); + assertEquals("\"" + status.getValue() + "\"", json); + } + + @ParameterizedTest + @EnumSource(PaymentStatus.class) + void deserializesFromWireValue(PaymentStatus status) throws Exception { + String json = "\"" + status.getValue() + "\""; + PaymentStatus result = mapper.readValue(json, PaymentStatus.class); + assertEquals(status, result); + } +} diff --git a/src/test/java/ee/bitweb/montonio/sdk/order/model/PaymentTest.java b/src/test/java/ee/bitweb/montonio/sdk/order/model/PaymentTest.java new file mode 100644 index 0000000..7e7b0e7 --- /dev/null +++ b/src/test/java/ee/bitweb/montonio/sdk/order/model/PaymentTest.java @@ -0,0 +1,117 @@ +package ee.bitweb.montonio.sdk.order.model; + +import ee.bitweb.montonio.sdk.exception.MontonioValidationException; +import ee.bitweb.montonio.sdk.model.Currency; +import ee.bitweb.montonio.sdk.model.Locale; +import ee.bitweb.montonio.sdk.model.PaymentMethodType; +import org.junit.jupiter.api.Test; +import tools.jackson.databind.DeserializationFeature; +import tools.jackson.databind.ObjectMapper; +import tools.jackson.databind.json.JsonMapper; + +import java.math.BigDecimal; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; + +class PaymentTest { + + private final ObjectMapper mapper = JsonMapper.builder() + .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES) + .build(); + + @Test + void buildWithRequiredFieldsOnly() { + Payment payment = Payment.builder() + .method(PaymentMethodType.PAYMENT_INITIATION) + .currency(Currency.EUR) + .amount(new BigDecimal("100.00")) + .build(); + + assertEquals(PaymentMethodType.PAYMENT_INITIATION, payment.getMethod()); + assertEquals(Currency.EUR, payment.getCurrency()); + assertEquals(new BigDecimal("100.00"), payment.getAmount()); + assertNull(payment.getMethodOptions()); + assertNull(payment.getMethodDisplay()); + } + + @Test + void buildWithAllFields() { + PaymentMethodOptions options = PaymentMethodOptions.builder() + .preferredCountry("EE") + .preferredLocale(Locale.ET) + .build(); + + Payment payment = Payment.builder() + .method(PaymentMethodType.PAYMENT_INITIATION) + .currency(Currency.EUR) + .amount(new BigDecimal("100.00")) + .methodOptions(options) + .methodDisplay("Bank transfer") + .build(); + + assertEquals(PaymentMethodType.PAYMENT_INITIATION, payment.getMethod()); + assertEquals(Currency.EUR, payment.getCurrency()); + assertEquals(new BigDecimal("100.00"), payment.getAmount()); + assertEquals(options, payment.getMethodOptions()); + assertEquals("Bank transfer", payment.getMethodDisplay()); + } + + @Test + void buildWithNullMethodThrows() { + MontonioValidationException exception = assertThrows( + MontonioValidationException.class, + () -> Payment.builder() + .currency(Currency.EUR) + .amount(BigDecimal.TEN) + .build() + ); + + assertEquals("method", exception.getField()); + } + + @Test + void buildWithNullCurrencyThrows() { + MontonioValidationException exception = assertThrows( + MontonioValidationException.class, + () -> Payment.builder() + .method(PaymentMethodType.CARD_PAYMENTS) + .amount(BigDecimal.TEN) + .build() + ); + + assertEquals("currency", exception.getField()); + } + + @Test + void buildWithNullAmountThrows() { + MontonioValidationException exception = assertThrows( + MontonioValidationException.class, + () -> Payment.builder() + .method(PaymentMethodType.CARD_PAYMENTS) + .currency(Currency.EUR) + .build() + ); + + assertEquals("amount", exception.getField()); + } + + @Test + void serializationRoundTrip() throws Exception { + Payment payment = Payment.builder() + .method(PaymentMethodType.CARD_PAYMENTS) + .currency(Currency.EUR) + .amount(new BigDecimal("50.00")) + .methodDisplay("Card") + .build(); + + String json = mapper.writeValueAsString(payment); + Payment deserialized = mapper.readValue(json, Payment.class); + + assertEquals(payment.getMethod(), deserialized.getMethod()); + assertEquals(payment.getCurrency(), deserialized.getCurrency()); + assertEquals(0, payment.getAmount().compareTo(deserialized.getAmount())); + assertEquals(payment.getMethodDisplay(), deserialized.getMethodDisplay()); + } +} diff --git a/src/test/java/ee/bitweb/montonio/sdk/order/request/CreateOrderRequestTest.java b/src/test/java/ee/bitweb/montonio/sdk/order/request/CreateOrderRequestTest.java new file mode 100644 index 0000000..c8c05be --- /dev/null +++ b/src/test/java/ee/bitweb/montonio/sdk/order/request/CreateOrderRequestTest.java @@ -0,0 +1,229 @@ +package ee.bitweb.montonio.sdk.order.request; + +import ee.bitweb.montonio.sdk.exception.MontonioValidationException; +import ee.bitweb.montonio.sdk.model.Currency; +import ee.bitweb.montonio.sdk.model.Locale; +import ee.bitweb.montonio.sdk.model.PaymentMethodType; +import ee.bitweb.montonio.sdk.order.model.Address; +import ee.bitweb.montonio.sdk.order.model.LineItem; +import ee.bitweb.montonio.sdk.order.model.Payment; +import org.junit.jupiter.api.Test; +import tools.jackson.databind.DeserializationFeature; +import tools.jackson.databind.ObjectMapper; +import tools.jackson.databind.json.JsonMapper; + +import java.math.BigDecimal; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; + +class CreateOrderRequestTest { + + private final ObjectMapper mapper = JsonMapper.builder() + .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES) + .build(); + + private Payment validPayment() { + return Payment.builder() + .method(PaymentMethodType.PAYMENT_INITIATION) + .currency(Currency.EUR) + .amount(new BigDecimal("100.00")) + .build(); + } + + @Test + void buildWithRequiredFieldsOnly() { + CreateOrderRequest request = CreateOrderRequest.builder() + .merchantReference("order-123") + .returnUrl("https://example.com/return") + .notificationUrl("https://example.com/notify") + .grandTotal(new BigDecimal("100.00")) + .currency(Currency.EUR) + .payment(validPayment()) + .build(); + + assertEquals("order-123", request.getMerchantReference()); + assertEquals("https://example.com/return", request.getReturnUrl()); + assertEquals("https://example.com/notify", request.getNotificationUrl()); + assertEquals(new BigDecimal("100.00"), request.getGrandTotal()); + assertEquals(Currency.EUR, request.getCurrency()); + assertNotNull(request.getPayment()); + assertNull(request.getLocale()); + assertNull(request.getBillingAddress()); + assertNull(request.getShippingAddress()); + assertNull(request.getLineItems()); + } + + @Test + void buildWithAllFields() { + Address address = Address.builder() + .firstName("John") + .lastName("Doe") + .email("john@example.com") + .build(); + + LineItem item = LineItem.builder() + .name("Hoverboard") + .quantity(BigDecimal.ONE) + .finalPrice(new BigDecimal("100.00")) + .build(); + + CreateOrderRequest request = CreateOrderRequest.builder() + .merchantReference("order-123") + .returnUrl("https://example.com/return") + .notificationUrl("https://example.com/notify") + .grandTotal(new BigDecimal("100.00")) + .currency(Currency.EUR) + .payment(validPayment()) + .locale(Locale.ET) + .billingAddress(address) + .shippingAddress(address) + .lineItems(List.of(item)) + .build(); + + assertEquals(Locale.ET, request.getLocale()); + assertNotNull(request.getBillingAddress()); + assertNotNull(request.getShippingAddress()); + assertEquals(1, request.getLineItems().size()); + } + + @Test + void buildWithNullMerchantReferenceThrows() { + MontonioValidationException exception = assertThrows( + MontonioValidationException.class, + () -> CreateOrderRequest.builder() + .returnUrl("https://example.com/return") + .notificationUrl("https://example.com/notify") + .grandTotal(BigDecimal.TEN) + .currency(Currency.EUR) + .payment(validPayment()) + .build() + ); + + assertEquals("merchantReference", exception.getField()); + } + + @Test + void buildWithBlankMerchantReferenceThrows() { + MontonioValidationException exception = assertThrows( + MontonioValidationException.class, + () -> CreateOrderRequest.builder() + .merchantReference(" ") + .returnUrl("https://example.com/return") + .notificationUrl("https://example.com/notify") + .grandTotal(BigDecimal.TEN) + .currency(Currency.EUR) + .payment(validPayment()) + .build() + ); + + assertEquals("merchantReference", exception.getField()); + } + + @Test + void buildWithNullReturnUrlThrows() { + MontonioValidationException exception = assertThrows( + MontonioValidationException.class, + () -> CreateOrderRequest.builder() + .merchantReference("order-123") + .notificationUrl("https://example.com/notify") + .grandTotal(BigDecimal.TEN) + .currency(Currency.EUR) + .payment(validPayment()) + .build() + ); + + assertEquals("returnUrl", exception.getField()); + } + + @Test + void buildWithNullNotificationUrlThrows() { + MontonioValidationException exception = assertThrows( + MontonioValidationException.class, + () -> CreateOrderRequest.builder() + .merchantReference("order-123") + .returnUrl("https://example.com/return") + .grandTotal(BigDecimal.TEN) + .currency(Currency.EUR) + .payment(validPayment()) + .build() + ); + + assertEquals("notificationUrl", exception.getField()); + } + + @Test + void buildWithNullGrandTotalThrows() { + MontonioValidationException exception = assertThrows( + MontonioValidationException.class, + () -> CreateOrderRequest.builder() + .merchantReference("order-123") + .returnUrl("https://example.com/return") + .notificationUrl("https://example.com/notify") + .currency(Currency.EUR) + .payment(validPayment()) + .build() + ); + + assertEquals("grandTotal", exception.getField()); + } + + @Test + void buildWithNullCurrencyThrows() { + MontonioValidationException exception = assertThrows( + MontonioValidationException.class, + () -> CreateOrderRequest.builder() + .merchantReference("order-123") + .returnUrl("https://example.com/return") + .notificationUrl("https://example.com/notify") + .grandTotal(BigDecimal.TEN) + .payment(validPayment()) + .build() + ); + + assertEquals("currency", exception.getField()); + } + + @Test + void buildWithNullPaymentThrows() { + MontonioValidationException exception = assertThrows( + MontonioValidationException.class, + () -> CreateOrderRequest.builder() + .merchantReference("order-123") + .returnUrl("https://example.com/return") + .notificationUrl("https://example.com/notify") + .grandTotal(BigDecimal.TEN) + .currency(Currency.EUR) + .build() + ); + + assertEquals("payment", exception.getField()); + } + + @Test + void serializationRoundTrip() throws Exception { + CreateOrderRequest request = CreateOrderRequest.builder() + .merchantReference("order-123") + .returnUrl("https://example.com/return") + .notificationUrl("https://example.com/notify") + .grandTotal(new BigDecimal("100.00")) + .currency(Currency.EUR) + .payment(validPayment()) + .locale(Locale.ET) + .build(); + + String json = mapper.writeValueAsString(request); + CreateOrderRequest deserialized = mapper.readValue(json, CreateOrderRequest.class); + + assertEquals(request.getMerchantReference(), deserialized.getMerchantReference()); + assertEquals(request.getReturnUrl(), deserialized.getReturnUrl()); + assertEquals(request.getNotificationUrl(), deserialized.getNotificationUrl()); + assertEquals(0, request.getGrandTotal().compareTo(deserialized.getGrandTotal())); + assertEquals(request.getCurrency(), deserialized.getCurrency()); + assertEquals(request.getLocale(), deserialized.getLocale()); + assertEquals(request.getPayment().getMethod(), deserialized.getPayment().getMethod()); + } +} diff --git a/src/test/java/ee/bitweb/montonio/sdk/order/response/CreateOrderResponseTest.java b/src/test/java/ee/bitweb/montonio/sdk/order/response/CreateOrderResponseTest.java new file mode 100644 index 0000000..61839aa --- /dev/null +++ b/src/test/java/ee/bitweb/montonio/sdk/order/response/CreateOrderResponseTest.java @@ -0,0 +1,70 @@ +package ee.bitweb.montonio.sdk.order.response; + +import org.junit.jupiter.api.Test; +import tools.jackson.databind.DeserializationFeature; +import tools.jackson.databind.ObjectMapper; +import tools.jackson.databind.json.JsonMapper; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class CreateOrderResponseTest { + + private final ObjectMapper mapper = JsonMapper.builder() + .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES) + .build(); + + @Test + void constructorSetsFields() { + CreateOrderResponse response = new CreateOrderResponse( + "550e8400-e29b-41d4-a716-446655440000", + "https://sandbox-stargate.montonio.com/pay" + ); + + assertEquals("550e8400-e29b-41d4-a716-446655440000", response.getUuid()); + assertEquals("https://sandbox-stargate.montonio.com/pay", response.getPaymentUrl()); + } + + @Test + void deserializesFromJson() throws Exception { + String json = """ + { + "uuid": "550e8400-e29b-41d4-a716-446655440000", + "paymentUrl": "https://sandbox-stargate.montonio.com/pay" + } + """; + + CreateOrderResponse response = mapper.readValue(json, CreateOrderResponse.class); + + assertEquals("550e8400-e29b-41d4-a716-446655440000", response.getUuid()); + assertEquals("https://sandbox-stargate.montonio.com/pay", response.getPaymentUrl()); + } + + @Test + void deserializesWithUnknownFieldsIgnored() throws Exception { + String json = """ + { + "uuid": "550e8400-e29b-41d4-a716-446655440000", + "paymentUrl": "https://example.com/pay", + "extraField": "ignored" + } + """; + + CreateOrderResponse response = mapper.readValue(json, CreateOrderResponse.class); + + assertEquals("550e8400-e29b-41d4-a716-446655440000", response.getUuid()); + } + + @Test + void serializationRoundTrip() throws Exception { + CreateOrderResponse original = new CreateOrderResponse( + "550e8400-e29b-41d4-a716-446655440000", + "https://example.com/pay" + ); + + String json = mapper.writeValueAsString(original); + CreateOrderResponse deserialized = mapper.readValue(json, CreateOrderResponse.class); + + assertEquals(original.getUuid(), deserialized.getUuid()); + assertEquals(original.getPaymentUrl(), deserialized.getPaymentUrl()); + } +} diff --git a/src/test/java/ee/bitweb/montonio/sdk/order/response/OrderResponseTest.java b/src/test/java/ee/bitweb/montonio/sdk/order/response/OrderResponseTest.java new file mode 100644 index 0000000..e91fa36 --- /dev/null +++ b/src/test/java/ee/bitweb/montonio/sdk/order/response/OrderResponseTest.java @@ -0,0 +1,210 @@ +package ee.bitweb.montonio.sdk.order.response; + +import ee.bitweb.montonio.sdk.model.Currency; +import ee.bitweb.montonio.sdk.model.Locale; +import ee.bitweb.montonio.sdk.model.PaymentMethodType; +import ee.bitweb.montonio.sdk.order.model.Address; +import ee.bitweb.montonio.sdk.order.model.LineItem; +import ee.bitweb.montonio.sdk.order.model.PaymentStatus; +import org.junit.jupiter.api.Test; +import tools.jackson.databind.DeserializationFeature; +import tools.jackson.databind.ObjectMapper; +import tools.jackson.databind.json.JsonMapper; + +import java.math.BigDecimal; +import java.util.List; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class OrderResponseTest { + + private final ObjectMapper mapper = JsonMapper.builder() + .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES) + .build(); + + @Test + void deserializesFullOrderResponse() throws Exception { + String json = """ + { + "uuid": "order-uuid", + "paymentStatus": "PAID", + "locale": "en", + "merchantReference": "Order1234567", + "merchantReferenceDisplay": "Order 1234567", + "merchantReturnUrl": "http://localhost:3000/return", + "merchantNotificationUrl": "http://example.com/notify", + "grandTotal": "100.00", + "currency": "EUR", + "paymentMethodType": "cardPayments", + "storeUuid": "store-uuid", + "paymentIntents": [ + { + "uuid": "intent-uuid", + "paymentMethodType": "cardPayments", + "paymentMethodMetadata": { + "preferredCountry": "US", + "preferredProvider": "Visa", + "paymentDescription": "Payment for Order 1234567" + }, + "amount": "100.00", + "currency": "EUR", + "status": "PAID", + "serviceFee": "0.00", + "serviceFeeCurrency": "EUR", + "createdAt": "2026-04-10T12:00:00Z" + } + ], + "lineItems": [ + { + "name": "Test Item", + "quantity": 1, + "finalPrice": 100 + } + ], + "billingAddress": { + "firstName": "John", + "lastName": "Doe", + "email": "john@example.com", + "country": "US" + }, + "shippingAddress": { + "firstName": "John", + "lastName": "Doe", + "country": "US" + }, + "expiresAt": "2026-04-10T12:10:00Z", + "createdAt": "2026-04-10T12:00:00Z", + "storeName": "Test Store", + "businessName": "Test Business", + "paymentUrl": "https://sandbox-stargate.montonio.com/pay", + "isRefundableType": true + } + """; + + OrderResponse response = mapper.readValue(json, OrderResponse.class); + + assertEquals("order-uuid", response.getUuid()); + assertEquals(PaymentStatus.PAID, response.getPaymentStatus()); + assertEquals(Locale.EN, response.getLocale()); + assertEquals("Order1234567", response.getMerchantReference()); + assertEquals("Order 1234567", response.getMerchantReferenceDisplay()); + assertEquals("http://localhost:3000/return", response.getMerchantReturnUrl()); + assertEquals("http://example.com/notify", response.getMerchantNotificationUrl()); + assertEquals("100.00", response.getGrandTotal()); + assertEquals(Currency.EUR, response.getCurrency()); + assertEquals(PaymentMethodType.CARD_PAYMENTS, response.getPaymentMethodType()); + assertEquals("store-uuid", response.getStoreUuid()); + + assertNotNull(response.getPaymentIntents()); + assertEquals(1, response.getPaymentIntents().size()); + assertEquals("intent-uuid", response.getPaymentIntents().get(0).getUuid()); + assertEquals(PaymentStatus.PAID, response.getPaymentIntents().get(0).getStatus()); + + assertNotNull(response.getLineItems()); + assertEquals(1, response.getLineItems().size()); + assertEquals("Test Item", response.getLineItems().get(0).getName()); + + assertEquals("John", response.getBillingAddress().getFirstName()); + assertEquals("John", response.getShippingAddress().getFirstName()); + + assertEquals("2026-04-10T12:10:00Z", response.getExpiresAt()); + assertEquals("2026-04-10T12:00:00Z", response.getCreatedAt()); + assertEquals("Test Store", response.getStoreName()); + assertEquals("Test Business", response.getBusinessName()); + assertEquals("https://sandbox-stargate.montonio.com/pay", response.getPaymentUrl()); + assertTrue(response.getIsRefundableType()); + } + + @Test + void deserializesWithUnknownFieldsIgnored() throws Exception { + String json = """ + { + "uuid": "order-uuid", + "paymentStatus": "PENDING", + "locale": "et", + "merchantReference": "ref", + "merchantReferenceDisplay": "ref", + "merchantReturnUrl": "http://example.com", + "merchantNotificationUrl": "http://example.com", + "grandTotal": "10.00", + "currency": "EUR", + "paymentMethodType": "paymentInitiation", + "storeUuid": "store", + "paymentIntents": [], + "lineItems": [], + "billingAddress": {}, + "shippingAddress": {}, + "expiresAt": "2026-04-10T12:10:00Z", + "createdAt": "2026-04-10T12:00:00Z", + "storeName": "Store", + "businessName": "Business", + "paymentUrl": "", + "refunds": [], + "availableForRefund": 0, + "unknownField": "ignored" + } + """; + + OrderResponse response = mapper.readValue(json, OrderResponse.class); + + assertEquals("order-uuid", response.getUuid()); + assertEquals(PaymentStatus.PENDING, response.getPaymentStatus()); + } + + @Test + void serializationRoundTrip() throws Exception { + OrderResponse original = new OrderResponse( + "order-uuid", + PaymentStatus.PAID, + Locale.EN, + "ref-123", + "Ref 123", + "http://example.com/return", + "http://example.com/notify", + "100.00", + Currency.EUR, + PaymentMethodType.CARD_PAYMENTS, + "store-uuid", + List.of(new PaymentIntent( + "intent-uuid", + PaymentMethodType.CARD_PAYMENTS, + "100.00", + Currency.EUR, + PaymentStatus.PAID, + "0.00", + Currency.EUR, + "2026-04-10T12:00:00Z", + Map.of("preferredCountry", "EE") + )), + List.of(LineItem.builder() + .name("Item") + .quantity(BigDecimal.ONE) + .finalPrice(new BigDecimal("100.00")) + .build()), + Address.builder().firstName("John").build(), + Address.builder().firstName("John").build(), + "2026-04-10T12:10:00Z", + "2026-04-10T12:00:00Z", + "Test Store", + "Test Business", + "https://example.com/pay", + true + ); + + String json = mapper.writeValueAsString(original); + OrderResponse deserialized = mapper.readValue(json, OrderResponse.class); + + assertEquals(original.getUuid(), deserialized.getUuid()); + assertEquals(original.getPaymentStatus(), deserialized.getPaymentStatus()); + assertEquals(original.getLocale(), deserialized.getLocale()); + assertEquals(original.getMerchantReference(), deserialized.getMerchantReference()); + assertEquals(original.getGrandTotal(), deserialized.getGrandTotal()); + assertEquals(original.getCurrency(), deserialized.getCurrency()); + assertEquals(original.getPaymentMethodType(), deserialized.getPaymentMethodType()); + assertEquals(1, deserialized.getPaymentIntents().size()); + assertEquals(1, deserialized.getLineItems().size()); + } +} diff --git a/src/test/java/ee/bitweb/montonio/sdk/order/response/PaymentIntentTest.java b/src/test/java/ee/bitweb/montonio/sdk/order/response/PaymentIntentTest.java new file mode 100644 index 0000000..39ab933 --- /dev/null +++ b/src/test/java/ee/bitweb/montonio/sdk/order/response/PaymentIntentTest.java @@ -0,0 +1,127 @@ +package ee.bitweb.montonio.sdk.order.response; + +import ee.bitweb.montonio.sdk.model.Currency; +import ee.bitweb.montonio.sdk.model.PaymentMethodType; +import ee.bitweb.montonio.sdk.order.model.PaymentStatus; +import org.junit.jupiter.api.Test; +import tools.jackson.databind.DeserializationFeature; +import tools.jackson.databind.ObjectMapper; +import tools.jackson.databind.json.JsonMapper; + +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; + +class PaymentIntentTest { + + private final ObjectMapper mapper = JsonMapper.builder() + .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES) + .build(); + + @Test + void constructorSetsAllFields() { + Map metadata = Map.of( + "preferredCountry", "EE", + "preferredProvider", "LHVBEE22" + ); + + PaymentIntent intent = new PaymentIntent( + "intent-uuid", + PaymentMethodType.PAYMENT_INITIATION, + "100.00", + Currency.EUR, + PaymentStatus.PAID, + "0.50", + Currency.EUR, + "2026-04-10T12:00:00Z", + metadata + ); + + assertEquals("intent-uuid", intent.getUuid()); + assertEquals(PaymentMethodType.PAYMENT_INITIATION, intent.getPaymentMethodType()); + assertEquals("100.00", intent.getAmount()); + assertEquals(Currency.EUR, intent.getCurrency()); + assertEquals(PaymentStatus.PAID, intent.getStatus()); + assertEquals("0.50", intent.getServiceFee()); + assertEquals(Currency.EUR, intent.getServiceFeeCurrency()); + assertEquals("2026-04-10T12:00:00Z", intent.getCreatedAt()); + assertNotNull(intent.getPaymentMethodMetadata()); + assertEquals("EE", intent.getPaymentMethodMetadata().get("preferredCountry")); + } + + @Test + void constructorWithNullMetadata() { + PaymentIntent intent = new PaymentIntent( + "intent-uuid", + PaymentMethodType.CARD_PAYMENTS, + "50.00", + Currency.EUR, + PaymentStatus.PENDING, + "0.00", + Currency.EUR, + "2026-04-10T12:00:00Z", + null + ); + + assertNull(intent.getPaymentMethodMetadata()); + } + + @Test + void deserializesFromJson() throws Exception { + String json = """ + { + "uuid": "intent-uuid", + "paymentMethodType": "cardPayments", + "amount": "100.00", + "currency": "EUR", + "status": "PAID", + "serviceFee": "0.50", + "serviceFeeCurrency": "EUR", + "createdAt": "2026-04-10T12:00:00Z", + "paymentMethodMetadata": { + "preferredCountry": "EE", + "preferredProvider": "Visa", + "paymentDescription": "Payment for order" + } + } + """; + + PaymentIntent intent = mapper.readValue(json, PaymentIntent.class); + + assertEquals("intent-uuid", intent.getUuid()); + assertEquals(PaymentMethodType.CARD_PAYMENTS, intent.getPaymentMethodType()); + assertEquals("100.00", intent.getAmount()); + assertEquals(Currency.EUR, intent.getCurrency()); + assertEquals(PaymentStatus.PAID, intent.getStatus()); + assertEquals("0.50", intent.getServiceFee()); + assertEquals("EE", intent.getPaymentMethodMetadata().get("preferredCountry")); + } + + @Test + void serializationRoundTrip() throws Exception { + PaymentIntent original = new PaymentIntent( + "intent-uuid", + PaymentMethodType.PAYMENT_INITIATION, + "100.00", + Currency.EUR, + PaymentStatus.AUTHORIZED, + "0.25", + Currency.EUR, + "2026-04-10T12:00:00Z", + Map.of("preferredCountry", "EE") + ); + + String json = mapper.writeValueAsString(original); + PaymentIntent deserialized = mapper.readValue(json, PaymentIntent.class); + + assertEquals(original.getUuid(), deserialized.getUuid()); + assertEquals(original.getPaymentMethodType(), deserialized.getPaymentMethodType()); + assertEquals(original.getAmount(), deserialized.getAmount()); + assertEquals(original.getCurrency(), deserialized.getCurrency()); + assertEquals(original.getStatus(), deserialized.getStatus()); + assertEquals(original.getServiceFee(), deserialized.getServiceFee()); + assertEquals(original.getServiceFeeCurrency(), deserialized.getServiceFeeCurrency()); + } +} From 35951fe1ea87f93e2777261253f23b23d2681d3d Mon Sep 17 00:00:00 2001 From: Rain Ramm Date: Fri, 10 Apr 2026 08:33:39 +0000 Subject: [PATCH 2/5] Address CodeRabbit review feedback - Validate Payment.amount > 0, not just non-null - Defensive copy for list fields in OrderResponse and CreateOrderRequest - Defensive copy for map field in PaymentIntent - Add language tag to design doc code fence - Add blank-URL validation tests for CreateOrderRequest - Strengthen CreateOrderResponse unknown-fields test - Add zero/negative amount validation tests for Payment Co-Authored-By: Claude Opus 4.6 (1M context) --- .../2026-04-10-payment-order-models-design.md | 2 +- .../montonio/sdk/order/model/Payment.java | 3 ++ .../sdk/order/request/CreateOrderRequest.java | 2 +- .../sdk/order/response/OrderResponse.java | 4 +-- .../sdk/order/response/PaymentIntent.java | 2 +- .../montonio/sdk/order/model/PaymentTest.java | 28 +++++++++++++++ .../order/request/CreateOrderRequestTest.java | 34 +++++++++++++++++++ .../response/CreateOrderResponseTest.java | 1 + 8 files changed, 71 insertions(+), 5 deletions(-) diff --git a/docs/plans/2026-04-10-payment-order-models-design.md b/docs/plans/2026-04-10-payment-order-models-design.md index d720531..4707a69 100644 --- a/docs/plans/2026-04-10-payment-order-models-design.md +++ b/docs/plans/2026-04-10-payment-order-models-design.md @@ -23,7 +23,7 @@ Field definitions were derived by cross-referencing: ### 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 diff --git a/src/main/java/ee/bitweb/montonio/sdk/order/model/Payment.java b/src/main/java/ee/bitweb/montonio/sdk/order/model/Payment.java index e52aee0..9411ec0 100644 --- a/src/main/java/ee/bitweb/montonio/sdk/order/model/Payment.java +++ b/src/main/java/ee/bitweb/montonio/sdk/order/model/Payment.java @@ -41,6 +41,9 @@ public class Payment { if (amount == null) { throw new MontonioValidationException("amount", "must not be null"); } + if (amount.signum() <= 0) { + throw new MontonioValidationException("amount", "must be greater than zero"); + } this.method = method; this.currency = currency; this.amount = amount; diff --git a/src/main/java/ee/bitweb/montonio/sdk/order/request/CreateOrderRequest.java b/src/main/java/ee/bitweb/montonio/sdk/order/request/CreateOrderRequest.java index 877e145..bc76d44 100644 --- a/src/main/java/ee/bitweb/montonio/sdk/order/request/CreateOrderRequest.java +++ b/src/main/java/ee/bitweb/montonio/sdk/order/request/CreateOrderRequest.java @@ -77,6 +77,6 @@ public class CreateOrderRequest { this.locale = locale; this.billingAddress = billingAddress; this.shippingAddress = shippingAddress; - this.lineItems = lineItems; + this.lineItems = lineItems == null ? null : List.copyOf(lineItems); } } diff --git a/src/main/java/ee/bitweb/montonio/sdk/order/response/OrderResponse.java b/src/main/java/ee/bitweb/montonio/sdk/order/response/OrderResponse.java index 7ad9974..fd6e632 100644 --- a/src/main/java/ee/bitweb/montonio/sdk/order/response/OrderResponse.java +++ b/src/main/java/ee/bitweb/montonio/sdk/order/response/OrderResponse.java @@ -74,8 +74,8 @@ public OrderResponse( this.currency = currency; this.paymentMethodType = paymentMethodType; this.storeUuid = storeUuid; - this.paymentIntents = paymentIntents; - this.lineItems = lineItems; + this.paymentIntents = paymentIntents == null ? null : List.copyOf(paymentIntents); + this.lineItems = lineItems == null ? null : List.copyOf(lineItems); this.billingAddress = billingAddress; this.shippingAddress = shippingAddress; this.expiresAt = expiresAt; diff --git a/src/main/java/ee/bitweb/montonio/sdk/order/response/PaymentIntent.java b/src/main/java/ee/bitweb/montonio/sdk/order/response/PaymentIntent.java index b1e2edd..7ae8182 100644 --- a/src/main/java/ee/bitweb/montonio/sdk/order/response/PaymentIntent.java +++ b/src/main/java/ee/bitweb/montonio/sdk/order/response/PaymentIntent.java @@ -44,6 +44,6 @@ public PaymentIntent( this.serviceFee = serviceFee; this.serviceFeeCurrency = serviceFeeCurrency; this.createdAt = createdAt; - this.paymentMethodMetadata = paymentMethodMetadata; + this.paymentMethodMetadata = paymentMethodMetadata == null ? null : Map.copyOf(paymentMethodMetadata); } } diff --git a/src/test/java/ee/bitweb/montonio/sdk/order/model/PaymentTest.java b/src/test/java/ee/bitweb/montonio/sdk/order/model/PaymentTest.java index 7e7b0e7..fb28155 100644 --- a/src/test/java/ee/bitweb/montonio/sdk/order/model/PaymentTest.java +++ b/src/test/java/ee/bitweb/montonio/sdk/order/model/PaymentTest.java @@ -97,6 +97,34 @@ void buildWithNullAmountThrows() { assertEquals("amount", exception.getField()); } + @Test + void buildWithZeroAmountThrows() { + MontonioValidationException exception = assertThrows( + MontonioValidationException.class, + () -> Payment.builder() + .method(PaymentMethodType.CARD_PAYMENTS) + .currency(Currency.EUR) + .amount(BigDecimal.ZERO) + .build() + ); + + assertEquals("amount", exception.getField()); + } + + @Test + void buildWithNegativeAmountThrows() { + MontonioValidationException exception = assertThrows( + MontonioValidationException.class, + () -> Payment.builder() + .method(PaymentMethodType.CARD_PAYMENTS) + .currency(Currency.EUR) + .amount(new BigDecimal("-1")) + .build() + ); + + assertEquals("amount", exception.getField()); + } + @Test void serializationRoundTrip() throws Exception { Payment payment = Payment.builder() diff --git a/src/test/java/ee/bitweb/montonio/sdk/order/request/CreateOrderRequestTest.java b/src/test/java/ee/bitweb/montonio/sdk/order/request/CreateOrderRequestTest.java index c8c05be..596d2c2 100644 --- a/src/test/java/ee/bitweb/montonio/sdk/order/request/CreateOrderRequestTest.java +++ b/src/test/java/ee/bitweb/montonio/sdk/order/request/CreateOrderRequestTest.java @@ -155,6 +155,40 @@ void buildWithNullNotificationUrlThrows() { assertEquals("notificationUrl", exception.getField()); } + @Test + void buildWithBlankReturnUrlThrows() { + MontonioValidationException exception = assertThrows( + MontonioValidationException.class, + () -> CreateOrderRequest.builder() + .merchantReference("order-123") + .returnUrl(" ") + .notificationUrl("https://example.com/notify") + .grandTotal(BigDecimal.TEN) + .currency(Currency.EUR) + .payment(validPayment()) + .build() + ); + + assertEquals("returnUrl", exception.getField()); + } + + @Test + void buildWithBlankNotificationUrlThrows() { + MontonioValidationException exception = assertThrows( + MontonioValidationException.class, + () -> CreateOrderRequest.builder() + .merchantReference("order-123") + .returnUrl("https://example.com/return") + .notificationUrl(" ") + .grandTotal(BigDecimal.TEN) + .currency(Currency.EUR) + .payment(validPayment()) + .build() + ); + + assertEquals("notificationUrl", exception.getField()); + } + @Test void buildWithNullGrandTotalThrows() { MontonioValidationException exception = assertThrows( diff --git a/src/test/java/ee/bitweb/montonio/sdk/order/response/CreateOrderResponseTest.java b/src/test/java/ee/bitweb/montonio/sdk/order/response/CreateOrderResponseTest.java index 61839aa..689833e 100644 --- a/src/test/java/ee/bitweb/montonio/sdk/order/response/CreateOrderResponseTest.java +++ b/src/test/java/ee/bitweb/montonio/sdk/order/response/CreateOrderResponseTest.java @@ -52,6 +52,7 @@ void deserializesWithUnknownFieldsIgnored() throws Exception { CreateOrderResponse response = mapper.readValue(json, CreateOrderResponse.class); assertEquals("550e8400-e29b-41d4-a716-446655440000", response.getUuid()); + assertEquals("https://example.com/pay", response.getPaymentUrl()); } @Test From b70197fffe28ba814d8a0b79f530650b7ac5522c Mon Sep 17 00:00:00 2001 From: Rain Ramm Date: Fri, 10 Apr 2026 08:42:55 +0000 Subject: [PATCH 3/5] Address second round of CodeRabbit review feedback - Validate CreateOrderRequest.grandTotal > 0, not just non-null - Add @Nullable to list fields in OrderResponse (paymentIntents, lineItems) - Reference issue #30 in design doc for deferred refund models - Add zero/negative grandTotal validation tests Co-Authored-By: Claude Opus 4.6 (1M context) --- .../2026-04-10-payment-order-models-design.md | 2 +- .../sdk/order/request/CreateOrderRequest.java | 3 ++ .../sdk/order/response/OrderResponse.java | 4 +++ .../order/request/CreateOrderRequestTest.java | 34 +++++++++++++++++++ 4 files changed, 42 insertions(+), 1 deletion(-) diff --git a/docs/plans/2026-04-10-payment-order-models-design.md b/docs/plans/2026-04-10-payment-order-models-design.md index 4707a69..aa14768 100644 --- a/docs/plans/2026-04-10-payment-order-models-design.md +++ b/docs/plans/2026-04-10-payment-order-models-design.md @@ -80,7 +80,7 @@ deserialization complexity for no real gain — the fields don't conflict. `OrderResponse` in the TypeScript client includes `refunds`, `availableForRefund`, and `isRefundableType`. Refund-related fields are mostly omitted (only `isRefundableType` -is included) pending a future refund models issue. Jackson ignores unknown fields. +is included) pending follow-up issue #30 for refund models. Jackson ignores unknown fields. ## File Inventory diff --git a/src/main/java/ee/bitweb/montonio/sdk/order/request/CreateOrderRequest.java b/src/main/java/ee/bitweb/montonio/sdk/order/request/CreateOrderRequest.java index bc76d44..e69b7d8 100644 --- a/src/main/java/ee/bitweb/montonio/sdk/order/request/CreateOrderRequest.java +++ b/src/main/java/ee/bitweb/montonio/sdk/order/request/CreateOrderRequest.java @@ -62,6 +62,9 @@ public class CreateOrderRequest { if (grandTotal == null) { throw new MontonioValidationException("grandTotal", "must not be null"); } + if (grandTotal.signum() <= 0) { + throw new MontonioValidationException("grandTotal", "must be greater than zero"); + } if (currency == null) { throw new MontonioValidationException("currency", "must not be null"); } diff --git a/src/main/java/ee/bitweb/montonio/sdk/order/response/OrderResponse.java b/src/main/java/ee/bitweb/montonio/sdk/order/response/OrderResponse.java index fd6e632..c0b47b2 100644 --- a/src/main/java/ee/bitweb/montonio/sdk/order/response/OrderResponse.java +++ b/src/main/java/ee/bitweb/montonio/sdk/order/response/OrderResponse.java @@ -26,8 +26,12 @@ public final class OrderResponse { private final Currency currency; private final PaymentMethodType paymentMethodType; private final String storeUuid; + @Nullable private final List paymentIntents; + + @Nullable private final List lineItems; + private final Address billingAddress; private final Address shippingAddress; private final String expiresAt; diff --git a/src/test/java/ee/bitweb/montonio/sdk/order/request/CreateOrderRequestTest.java b/src/test/java/ee/bitweb/montonio/sdk/order/request/CreateOrderRequestTest.java index 596d2c2..ad7c903 100644 --- a/src/test/java/ee/bitweb/montonio/sdk/order/request/CreateOrderRequestTest.java +++ b/src/test/java/ee/bitweb/montonio/sdk/order/request/CreateOrderRequestTest.java @@ -205,6 +205,40 @@ void buildWithNullGrandTotalThrows() { assertEquals("grandTotal", exception.getField()); } + @Test + void buildWithZeroGrandTotalThrows() { + MontonioValidationException exception = assertThrows( + MontonioValidationException.class, + () -> CreateOrderRequest.builder() + .merchantReference("order-123") + .returnUrl("https://example.com/return") + .notificationUrl("https://example.com/notify") + .grandTotal(BigDecimal.ZERO) + .currency(Currency.EUR) + .payment(validPayment()) + .build() + ); + + assertEquals("grandTotal", exception.getField()); + } + + @Test + void buildWithNegativeGrandTotalThrows() { + MontonioValidationException exception = assertThrows( + MontonioValidationException.class, + () -> CreateOrderRequest.builder() + .merchantReference("order-123") + .returnUrl("https://example.com/return") + .notificationUrl("https://example.com/notify") + .grandTotal(new BigDecimal("-1")) + .currency(Currency.EUR) + .payment(validPayment()) + .build() + ); + + assertEquals("grandTotal", exception.getField()); + } + @Test void buildWithNullCurrencyThrows() { MontonioValidationException exception = assertThrows( From 193959e098b7605aed1ed75cae6970e780756312 Mon Sep 17 00:00:00 2001 From: Rain Ramm Date: Fri, 10 Apr 2026 09:01:10 +0000 Subject: [PATCH 4/5] Add CODEOWNERS file Assigns @BitWeb/montonio-sdk-maintainers as owners for all key areas: core SDK source/tests, Gradle build files, GitHub config, and AI tooling. Partial progress on #6 Co-Authored-By: Claude Opus 4.6 (1M context) --- .github/CODEOWNERS | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 .github/CODEOWNERS diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..c9049c0 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,17 @@ +# Default owners for everything in the repo +* @BitWeb/montonio-sdk-maintainers + +# Core SDK source and tests +/src/main/ @BitWeb/montonio-sdk-maintainers +/src/test/ @BitWeb/montonio-sdk-maintainers + +# Build and infrastructure +build.gradle @BitWeb/montonio-sdk-maintainers +*.gradle @BitWeb/montonio-sdk-maintainers +settings.gradle @BitWeb/montonio-sdk-maintainers + +# CI/CD and GitHub configuration +/.github/ @BitWeb/montonio-sdk-maintainers + +# AI tooling configuration +CLAUDE.md @BitWeb/montonio-sdk-maintainers From f0dde528e1c92a4799910988775f9d9fd5626551 Mon Sep 17 00:00:00 2001 From: Rain Ramm Date: Fri, 10 Apr 2026 09:13:45 +0000 Subject: [PATCH 5/5] Simplify CODEOWNERS to use @BitWeb/tech for all files Co-Authored-By: Claude Opus 4.6 (1M context) --- .github/CODEOWNERS | 18 +----------------- 1 file changed, 1 insertion(+), 17 deletions(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index c9049c0..f853048 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1,17 +1 @@ -# Default owners for everything in the repo -* @BitWeb/montonio-sdk-maintainers - -# Core SDK source and tests -/src/main/ @BitWeb/montonio-sdk-maintainers -/src/test/ @BitWeb/montonio-sdk-maintainers - -# Build and infrastructure -build.gradle @BitWeb/montonio-sdk-maintainers -*.gradle @BitWeb/montonio-sdk-maintainers -settings.gradle @BitWeb/montonio-sdk-maintainers - -# CI/CD and GitHub configuration -/.github/ @BitWeb/montonio-sdk-maintainers - -# AI tooling configuration -CLAUDE.md @BitWeb/montonio-sdk-maintainers +* @BitWeb/tech