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