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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,13 @@

@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
/**
* A daily payout schedule.
*
* <p>For SaaS seller (ISV) sub-entities this runs on working days only, Monday to Friday, with no
* payout at weekends, and is based on the available balance as of 00:00 in the sub-entity's time
* zone. Standard sub-entities are paid out every day.
*/
public final class ScheduleFrequencyDailyRequest extends ScheduleRequest {

public ScheduleFrequencyDailyRequest() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,13 @@
@Setter
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
/**
* A monthly payout schedule.
*
* <p>SaaS seller (ISV) sub-entities accept only these combinations, in any order: {@code [1]},
* {@code [15]}, {@code [1, 15]} or {@code [1, 16]}. Their payout is based on the available balance
* as of 00:00 in the sub-entity's time zone. Standard sub-entities accept any day from 1 to 28.
*/
public final class ScheduleFrequencyMonthlyRequest extends ScheduleRequest {

@SerializedName("by_month_day")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,13 @@
@Setter
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
/**
* A weekly payout schedule.
*
* <p>SaaS seller (ISV) sub-entities accept working days only, Monday to Friday: a schedule set to
* a Saturday or Sunday is rejected. Their payout is based on the available balance as of 00:00 in
* the sub-entity's time zone. Standard sub-entities accept any day.
*/
public final class ScheduleFrequencyWeeklyRequest extends ScheduleRequest {

@SerializedName("by_day")
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.checkout.accounts.payout.schedule.request;

import com.google.gson.annotations.SerializedName;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
Expand All @@ -15,6 +16,34 @@ public final class UpdateScheduleRequest {

private int threshold;

/**
* The ID of the platforms payment instrument to pay out to on this schedule.
*
* <p>Optional for SaaS seller (ISV) schedules, but when supplied it must reference a
* verified payment instrument, otherwise the request is rejected.
*/
@SerializedName("payment_instrument_id")
private String paymentInstrumentId;

/**
* The amount, in the minor units of the schedule's currency, to retain in the sub-entity's
* available balance. Checkout.com pays out only the funds above this, and generates no payout
* if there are none. Defaults to {@code 0} when not set.
*
* <p>SaaS seller (ISV) schedules only.
*/
@SerializedName("balance_minimum")
private Long balanceMinimum;

/**
* Whether to carry forward to the next payout any balance below the configured minimum.
* Defaults to {@code false} when not set.
*
* <p>SaaS seller (ISV) schedules only.
*/
@SerializedName("carry_forward_enabled")
private Boolean carryForwardEnabled;

private ScheduleRequest recurrence;

}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.checkout.accounts.payout.schedule.response;

import com.google.gson.annotations.SerializedName;
import lombok.Data;

@Data
Expand All @@ -9,6 +10,28 @@ public final class CurrencySchedule {

private Integer threshold;

/**
* The ID of the platforms payment instrument this schedule pays out to.
*/
@SerializedName("payment_instrument_id")
private String paymentInstrumentId;

/**
* The amount, in the minor units of the schedule's currency, retained in the sub-entity's
* available balance. Only the funds above this are paid out.
*
* <p>Returned for SaaS seller (ISV) schedules.
*/
@SerializedName("balance_minimum")
private Long balanceMinimum;

/**
* Whether a balance below the configured minimum is carried forward to the next payout.
* Always returned for SaaS sellers, where it defaults to {@code false}.
*/
@SerializedName("carry_forward_enabled")
private Boolean carryForwardEnabled;

private ScheduleResponse recurrence;

}
Original file line number Diff line number Diff line change
@@ -1,16 +1,25 @@
package com.checkout.payments.response;

import com.google.gson.annotations.SerializedName;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.time.Instant;

@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public final class PaymentInstructionResponse {

private Instant valueDate;

/**
* The scheme's categorisation of the client, for example {@code FD}, {@code MT} or {@code AA}.
*/
@SerializedName("funds_transfer_type")
private String fundsTransferType;

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
package com.checkout.accounts;

import com.checkout.GsonSerializer;
import com.checkout.accounts.payout.schedule.DaySchedule;
import com.checkout.accounts.payout.schedule.request.ScheduleFrequencyMonthlyRequest;
import com.checkout.accounts.payout.schedule.request.ScheduleFrequencyWeeklyRequest;
import com.checkout.accounts.payout.schedule.request.UpdateScheduleRequest;
import com.checkout.accounts.payout.schedule.response.CurrencySchedule;
import org.junit.jupiter.api.Test;

import java.util.Arrays;
import java.util.Collections;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;

/**
* Covers the SaaS seller (ISV) payout schedule fields added by the 2026-08-05 spec.
*/
class PayoutScheduleIsvSerializationTest {

private final GsonSerializer serializer = new GsonSerializer();

@Test
void shouldSerializeIsvScheduleFields() {
final UpdateScheduleRequest request = UpdateScheduleRequest.builder()
.enabled(true)
.threshold(100)
.balanceMinimum(500L)
.carryForwardEnabled(true)
.paymentInstrumentId("ppi_w4jelhppmfiufdnatam37wrfc4")
.recurrence(ScheduleFrequencyWeeklyRequest.builder()
.byDays(Collections.singletonList(DaySchedule.MONDAY))
.build())
.build();

final String json = serializer.toJson(request);

assertTrue(json.contains("\"balance_minimum\":500"), json);
assertTrue(json.contains("\"carry_forward_enabled\":true"), json);
assertTrue(json.contains("\"payment_instrument_id\":\"ppi_w4jelhppmfiufdnatam37wrfc4\""), json);
assertTrue(json.contains("\"by_day\":[\"monday\"]"), json);
}

/**
* A standard sub-entity has no balance minimum and no carry-forward, so neither may appear in
* its request body. If they leaked in as nulls or zeros the API would read a standard schedule
* as an ISV one.
*/
@Test
void shouldOmitIsvFieldsWhenUnset() {
final UpdateScheduleRequest request = UpdateScheduleRequest.builder()
.enabled(true)
.threshold(100)
.recurrence(ScheduleFrequencyMonthlyRequest.builder()
.byMonthDays(Arrays.asList(1, 15))
.build())
.build();

final String json = serializer.toJson(request);

assertFalse(json.contains("balance_minimum"), json);
assertFalse(json.contains("carry_forward_enabled"), json);
assertFalse(json.contains("payment_instrument_id"), json);
assertTrue(json.contains("\"by_month_day\":[1,15]"), json);
}

@Test
void shouldDeserializeIsvScheduleFields() {
final String json = "{\"enabled\":true,\"threshold\":100,\"balance_minimum\":500,"
+ "\"carry_forward_enabled\":true,"
+ "\"payment_instrument_id\":\"ppi_w4jelhppmfiufdnatam37wrfc4\","
+ "\"recurrence\":{\"frequency\":\"Weekly\",\"by_day\":[\"monday\"]}}";

final CurrencySchedule schedule = serializer.fromJson(json, CurrencySchedule.class);

assertTrue(schedule.getEnabled());
assertEquals(100, schedule.getThreshold());
assertEquals(500L, schedule.getBalanceMinimum());
assertTrue(schedule.getCarryForwardEnabled());
assertEquals("ppi_w4jelhppmfiufdnatam37wrfc4", schedule.getPaymentInstrumentId());
}

/**
* A standard schedule omits both fields, and they must come back null rather than 0/false:
* a caller cannot otherwise tell "not applicable" from "set to zero".
*/
@Test
void shouldLeaveIsvFieldsNullForAStandardSchedule() {
final String json = "{\"enabled\":true,\"threshold\":100,"
+ "\"recurrence\":{\"frequency\":\"Daily\"}}";

final CurrencySchedule schedule = serializer.fromJson(json, CurrencySchedule.class);

assertNull(schedule.getBalanceMinimum());
assertNull(schedule.getCarryForwardEnabled());
assertNull(schedule.getPaymentInstrumentId());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package com.checkout.payments;

import com.checkout.GsonSerializer;
import com.checkout.payments.response.PayoutResponse;
import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;

/**
* Covers {@code instruction.funds_transfer_type} on the card payout response, added by the
* 2026-08-05 spec. The field was already modelled on the payout *request*; this is the response
* side, which had only {@code value_date}.
*/
class PayoutInstructionSerializationTest {

private final GsonSerializer serializer = new GsonSerializer();

@Test
void shouldDeserializeFundsTransferTypeOnThePayoutInstruction() {
final String json = "{\"id\":\"pay_1\",\"status\":\"Accepted\",\"reference\":\"ORD-1\","
+ "\"instruction\":{\"value_date\":\"2026-08-05T10:00:00Z\","
+ "\"funds_transfer_type\":\"AA\"}}";

final PayoutResponse response = serializer.fromJson(json, PayoutResponse.class);

assertNotNull(response.getInstruction());
assertEquals("AA", response.getInstruction().getFundsTransferType());
assertNotNull(response.getInstruction().getValueDate());
}

/**
* The scheme does not always categorise the client, so the field has to survive being absent
* rather than defaulting to something that reads as a real categorisation.
*/
@Test
void shouldLeaveFundsTransferTypeNullWhenAbsent() {
final String json = "{\"id\":\"pay_1\",\"status\":\"Accepted\","
+ "\"instruction\":{\"value_date\":\"2026-08-05T10:00:00Z\"}}";

final PayoutResponse response = serializer.fromJson(json, PayoutResponse.class);

assertNotNull(response.getInstruction());
assertNull(response.getInstruction().getFundsTransferType());
}
}
Loading