Skip to content

Instantly share code, notes, and snippets.

@kawasima
Last active March 31, 2026 04:20
Show Gist options
  • Select an option

  • Save kawasima/673944f21a824f805ec83e89d5ae680c to your computer and use it in GitHub Desktop.

Select an option

Save kawasima/673944f21a824f805ec83e89d5ae680c to your computer and use it in GitHub Desktop.
import static net.unit8.raoh.json.JsonDecoders.*;
JsonDecoder<Quantity> quantityDec =
int_().min(1).map(Quantity::new);
JsonDecoder<ShippingAddress> addressDec =
field("shippingAddress",
string().nonBlank().map(ShippingAddress::new));
// ProductRepositoryを受け取り、flatMapで存在チェックまで行う
static JsonDecoder<Tuple2<List<OrderLine>, ShippingAddress>>
createOrderDec(ProductRepository products) {
var orderLineDec = combine(
field("productId", string().nonBlank()
.map(ProductId::new)
.flatMap(products::findById)),
field("quantity", quantityDec)
).map(OrderLine::new);
return combine(
field("items", list(orderLineDec).nonempty()),
addressDec
);
}
record OrderId(String value) {}
record ProductId(String value) {}
record Quantity(int value) {}
record Email(String value) {}
record Money(BigDecimal value) {
Money {
if (value.compareTo(BigDecimal.ZERO) < 0) {
throw new IllegalArgumentException(
"Money cannot be negative");
}
}
Money multiply(int n) {
return new Money(
value.multiply(BigDecimal.valueOf(n)));
}
Money add(Money other) {
return new Money(value.add(other.value));
}
}
record OrderItem(ProductId productId, Quantity quantity) {}
record ShippingAddress(String value) {}
record OrderLine(Product product, Quantity quantity) {}
record Order(
OrderId id,
List<OrderItem> items,
ShippingAddress address,
Money total,
OrderStatus status
) {}
class CreateOrder
implements Function<CreateOrder.Input, Order> {
record Input(
List<OrderLine> lines,
ShippingAddress address,
int activeOrderCount
) {}
private static final int MAX_ACTIVE_ORDERS = 10;
private static final BigDecimal DISCOUNT_THRESHOLD
= new BigDecimal("10000");
private static final BigDecimal DISCOUNT_RATE
= new BigDecimal("0.1");
@Override
public Order apply(Input input) {
if (input.activeOrderCount() >= MAX_ACTIVE_ORDERS) {
throw new DomainException(
"注文数の上限に達しています");
}
var total = calculateTotal(input.lines());
var items = input.lines().stream()
.map(l -> new OrderItem(
l.product().id(), l.quantity()))
.toList();
return new Order(
new OrderId(UUID.randomUUID().toString()),
items,
input.address(),
total,
OrderStatus.CREATED
);
}
private Money calculateTotal(List<OrderLine> lines) {
var subtotal = lines.stream().reduce(
new Money(BigDecimal.ZERO),
(acc, line) -> acc.add(
line.product().price()
.multiply(line.quantity().value())
),
Money::add
);
if (subtotal.value()
.compareTo(DISCOUNT_THRESHOLD) >= 0) {
var discounted = subtotal.value().multiply(
BigDecimal.ONE.subtract(DISCOUNT_RATE)
);
return new Money(discounted);
}
return subtotal;
}
}
class CancelOrder
implements Function<Order, Order> {
@Override
public Order apply(Order order) {
if (order.status() != OrderStatus.CREATED) {
throw new DomainException(
"作成済み以外の注文はキャンセルできません");
}
return new Order(
order.id(), order.items(),
order.address(), order.total(),
OrderStatus.CANCELLED
);
}
}
class CalculateRefund
implements Function<Order, Money> {
@Override
public Money apply(Order order) {
return switch (order.status()) {
case CANCELLED -> order.total();
case SHIPPED -> new Money(
order.total().value()
.multiply(new BigDecimal("0.8")));
default -> throw new DomainException(
"返金対象外の状態です");
};
}
}
import static net.unit8.raoh.encode.ObjectEncoders.*;
Encoder<Money, BigDecimal> moneyEnc =
decimal().contramap(Money::value);
Encoder<Order, Map<String, Object>> orderEnc = object(
property("id", Order::id, string().contramap(OrderId::value)),
property("total", Order::total, moneyEnc),
property("status", Order::status, enumOf(OrderStatus.class)),
property("items", Order::items, list(object(
property("productId", OrderItem::productId, string().contramap(ProductId::value)),
property("quantity", OrderItem::quantity, int_().contramap(Quantity::value))
)))
);
@RestController
@RequestMapping("/orders")
class OrderController {
private final OrderRepository orders;
private final ProductRepository products;
private final PaymentGateway payment;
private final CreateOrder createOrder = new CreateOrder();
private final CancelOrder cancelOrder = new CancelOrder();
private final CalculateRefund calcRefund
= new CalculateRefund();
@PostMapping
ResponseEntity<?> create(@RequestBody JsonNode body) {
return switch (createOrderDec(products).decode(body)) {
case Ok(var lines, var address) -> {
var activeCount
= orders.countActive(currentUserId());
var order = createOrder.apply(
new CreateOrder.Input(
lines, address, activeCount
)
);
payment.authorize(order.total());
orders.save(order);
yield ResponseEntity.status(201)
.body(orderEnc.encode(order));
}
case Err(var issues) ->
ResponseEntity.badRequest()
.body(issues.toJsonList());
};
}
@PostMapping("/{id}/cancel")
ResponseEntity<?> cancel(@PathVariable String id) {
var order = orders.findById(new OrderId(id));
var cancelled = cancelOrder.apply(order);
orders.save(cancelled);
return ResponseEntity.ok(
orderEnc.encode(cancelled));
}
@PostMapping("/{id}/refund")
ResponseEntity<?> refund(@PathVariable String id) {
var order = orders.findById(new OrderId(id));
var amount = calcRefund.apply(order);
payment.refund(order.id(), amount);
return ResponseEntity.ok(Map.of(
"refundAmount", amount.value()));
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment