feat: add denomination plan APIs
parent
79b1110c1f
commit
bda520215c
@ -0,0 +1,19 @@
|
||||
package com.yau.digitalrmb.issuance.application.query;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
public final class DenominationPlanItemView {
|
||||
private final BigDecimal denomination;
|
||||
private final int quantity;
|
||||
private final BigDecimal subtotal;
|
||||
|
||||
public DenominationPlanItemView(BigDecimal denomination, int quantity, BigDecimal subtotal) {
|
||||
this.denomination = denomination;
|
||||
this.quantity = quantity;
|
||||
this.subtotal = subtotal;
|
||||
}
|
||||
|
||||
public BigDecimal getDenomination() { return denomination; }
|
||||
public int getQuantity() { return quantity; }
|
||||
public BigDecimal getSubtotal() { return subtotal; }
|
||||
}
|
||||
@ -0,0 +1,25 @@
|
||||
package com.yau.digitalrmb.issuance.application.query;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
public final class DenominationPlanView {
|
||||
private final List<DenominationPlanItemView> items;
|
||||
private final int totalQuantity;
|
||||
private final BigDecimal totalAmount;
|
||||
private final boolean valid;
|
||||
|
||||
public DenominationPlanView(List<DenominationPlanItemView> items, int totalQuantity, BigDecimal totalAmount,
|
||||
boolean valid) {
|
||||
this.items = Collections.unmodifiableList(items);
|
||||
this.totalQuantity = totalQuantity;
|
||||
this.totalAmount = totalAmount;
|
||||
this.valid = valid;
|
||||
}
|
||||
|
||||
public List<DenominationPlanItemView> getItems() { return items; }
|
||||
public int getTotalQuantity() { return totalQuantity; }
|
||||
public BigDecimal getTotalAmount() { return totalAmount; }
|
||||
public boolean isValid() { return valid; }
|
||||
}
|
||||
@ -0,0 +1,103 @@
|
||||
package com.yau.digitalrmb.issuance.application.service;
|
||||
|
||||
import com.yau.digitalrmb.issuance.application.query.DenominationPlanItemView;
|
||||
import com.yau.digitalrmb.issuance.application.query.DenominationPlanView;
|
||||
import com.yau.digitalrmb.issuance.interfaces.dto.DenominationItemRequest;
|
||||
import com.yau.digitalrmb.shared.api.ErrorCode;
|
||||
import com.yau.digitalrmb.shared.exception.BusinessException;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Service
|
||||
public class DenominationPlanService {
|
||||
private static final List<BigDecimal> STANDARD_DENOMINATIONS = Arrays.asList(
|
||||
new BigDecimal("100.00"), new BigDecimal("50.00"), new BigDecimal("20.00"),
|
||||
new BigDecimal("10.00"), new BigDecimal("5.00"), new BigDecimal("1.00"),
|
||||
new BigDecimal("0.50"), new BigDecimal("0.20"), new BigDecimal("0.10"),
|
||||
new BigDecimal("0.05"), new BigDecimal("0.01"));
|
||||
private static final int[] DEFAULT_QUANTITIES = {400, 100, 200, 50, 80, 100, 0, 0, 0, 0, 0};
|
||||
private static final BigDecimal DEFAULT_TOTAL_AMOUNT = new BigDecimal("50000.00");
|
||||
|
||||
public DenominationPlanView defaultPlan() {
|
||||
List<DenominationItemRequest> items = new ArrayList<DenominationItemRequest>();
|
||||
for (int index = 0; index < STANDARD_DENOMINATIONS.size(); index++) {
|
||||
DenominationItemRequest item = new DenominationItemRequest();
|
||||
item.setDenomination(STANDARD_DENOMINATIONS.get(index));
|
||||
item.setQuantity(DEFAULT_QUANTITIES[index]);
|
||||
items.add(item);
|
||||
}
|
||||
return validate(DEFAULT_TOTAL_AMOUNT, items);
|
||||
}
|
||||
|
||||
public DenominationPlanView validate(BigDecimal totalAmount, List<DenominationItemRequest> items) {
|
||||
BigDecimal requestedTotal = normalizeAmount(totalAmount);
|
||||
Map<BigDecimal, Integer> quantities = quantitiesByDenomination(items);
|
||||
List<DenominationPlanItemView> planItems = new ArrayList<DenominationPlanItemView>();
|
||||
BigDecimal calculatedTotal = BigDecimal.ZERO.setScale(2);
|
||||
int totalQuantity = 0;
|
||||
for (BigDecimal denomination : STANDARD_DENOMINATIONS) {
|
||||
int quantity = quantities.get(denomination);
|
||||
BigDecimal subtotal = denomination.multiply(BigDecimal.valueOf(quantity)).setScale(2);
|
||||
planItems.add(new DenominationPlanItemView(denomination, quantity, subtotal));
|
||||
calculatedTotal = calculatedTotal.add(subtotal);
|
||||
totalQuantity = Math.addExact(totalQuantity, quantity);
|
||||
}
|
||||
return new DenominationPlanView(planItems, totalQuantity, calculatedTotal,
|
||||
calculatedTotal.compareTo(requestedTotal) == 0);
|
||||
}
|
||||
|
||||
private Map<BigDecimal, Integer> quantitiesByDenomination(List<DenominationItemRequest> items) {
|
||||
if (items == null || items.size() != STANDARD_DENOMINATIONS.size()) {
|
||||
throw validationError("items must contain every standard denomination");
|
||||
}
|
||||
Map<BigDecimal, Integer> quantities = new HashMap<BigDecimal, Integer>();
|
||||
for (DenominationItemRequest item : items) {
|
||||
if (item == null || item.getDenomination() == null || item.getQuantity() == null) {
|
||||
throw validationError("denomination item must include denomination and quantity");
|
||||
}
|
||||
BigDecimal denomination = normalizeDenomination(item.getDenomination());
|
||||
if (!STANDARD_DENOMINATIONS.contains(denomination) || item.getQuantity() < 0
|
||||
|| quantities.put(denomination, item.getQuantity()) != null) {
|
||||
throw validationError("denomination plan contains an invalid item");
|
||||
}
|
||||
}
|
||||
if (quantities.size() != STANDARD_DENOMINATIONS.size()) {
|
||||
throw validationError("denomination plan must not omit a standard denomination");
|
||||
}
|
||||
return quantities;
|
||||
}
|
||||
|
||||
private BigDecimal normalizeAmount(BigDecimal totalAmount) {
|
||||
if (totalAmount == null) {
|
||||
throw validationError("totalAmount must not be null");
|
||||
}
|
||||
try {
|
||||
BigDecimal amount = totalAmount.setScale(2, RoundingMode.UNNECESSARY);
|
||||
if (amount.signum() <= 0) {
|
||||
throw validationError("totalAmount must be positive");
|
||||
}
|
||||
return amount;
|
||||
} catch (ArithmeticException exception) {
|
||||
throw validationError("totalAmount must have at most two decimal places");
|
||||
}
|
||||
}
|
||||
|
||||
private BigDecimal normalizeDenomination(BigDecimal denomination) {
|
||||
try {
|
||||
return denomination.setScale(2, RoundingMode.UNNECESSARY);
|
||||
} catch (ArithmeticException exception) {
|
||||
throw validationError("denomination must have at most two decimal places");
|
||||
}
|
||||
}
|
||||
|
||||
private BusinessException validationError(String message) {
|
||||
return new BusinessException(ErrorCode.VALIDATION_ERROR, message);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,14 @@
|
||||
package com.yau.digitalrmb.issuance.interfaces.dto;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
|
||||
public class DenominationPlanRequest {
|
||||
private BigDecimal totalAmount;
|
||||
private List<DenominationItemRequest> items;
|
||||
|
||||
public BigDecimal getTotalAmount() { return totalAmount; }
|
||||
public void setTotalAmount(BigDecimal totalAmount) { this.totalAmount = totalAmount; }
|
||||
public List<DenominationItemRequest> getItems() { return items; }
|
||||
public void setItems(List<DenominationItemRequest> items) { this.items = items; }
|
||||
}
|
||||
Loading…
Reference in New Issue