fix(procurement): reject unsupported purchase payload fields
This commit is contained in:
+6
-1
@@ -28,7 +28,12 @@ public final class OverseasProcurementRequests {
|
||||
@NotBlank @Size(max = 128) String contactName,
|
||||
@NotBlank @Size(max = 64) @jakarta.validation.constraints.Pattern(regexp="[+0-9][0-9 ()-]{5,63}") String contactPhone,
|
||||
@Size(max = 1000) String remark,
|
||||
@NotBlank @Size(max = 255) String idempotencyKey) {}
|
||||
@NotBlank @Size(max = 255) String idempotencyKey) {
|
||||
@com.fasterxml.jackson.annotation.JsonAnySetter
|
||||
public void rejectUnknownField(String name, JsonNode value) {
|
||||
throw new IllegalArgumentException("采购下单仅支持单商品、单规格及一次购买批次,不支持扩展字段:" + name);
|
||||
}
|
||||
}
|
||||
|
||||
public record Query(
|
||||
@Min(1) long page, @Min(1) @Max(200) long size,
|
||||
|
||||
+23
@@ -10,6 +10,29 @@ import java.util.List;
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
|
||||
class ProcurementRequestBoundaryTest {
|
||||
@Test void createRejectsMultiProductAndBatchExtensionsWithApplicationJacksonDefaults() {
|
||||
new org.springframework.boot.test.context.runner.ApplicationContextRunner()
|
||||
.withConfiguration(org.springframework.boot.autoconfigure.AutoConfigurations.of(
|
||||
org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration.class))
|
||||
.withUserConfiguration(com.baiyue.common.web.LongIdJacksonConfiguration.class)
|
||||
.run(context -> {
|
||||
var json=context.getBean(ObjectMapper.class);
|
||||
assertThat(json.isEnabled(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)).isFalse();
|
||||
String valid="""
|
||||
{"purchaseCategory":"sample","productName":"Sample","productSpecification":"A",
|
||||
"purchaseQuantity":1,"budgetUnitPrice":10,"currency":"CNY","receivingAddress":"Address",
|
||||
"contactName":"Buyer","contactPhone":"13800000000","idempotencyKey":"one"}
|
||||
""";
|
||||
assertThat(json.readValue(valid,OverseasProcurementRequests.Create.class).productName()).isEqualTo("Sample");
|
||||
for(String field:List.of("items","products","batches","purchaseBatches","shipmentBatches")) {
|
||||
var extended=(com.fasterxml.jackson.databind.node.ObjectNode)json.readTree(valid);
|
||||
extended.putArray(field).addObject().put("productName","Second product");
|
||||
assertThatThrownBy(()->json.treeToValue(extended,OverseasProcurementRequests.Create.class))
|
||||
.as(field).hasMessageContaining("单商品");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Test void missingCommandVersionCannotDefaultToZero() throws Exception {
|
||||
var json=new ObjectMapper().findAndRegisterModules();
|
||||
try(var factory=jakarta.validation.Validation.buildDefaultValidatorFactory()) {
|
||||
|
||||
+24
@@ -109,6 +109,30 @@ class ProcurementPhysicalPersistenceTest {
|
||||
assertThat(jdbc.queryForObject("SELECT COUNT(*) FROM ll_procurement_manifest",Integer.class)).isZero();
|
||||
}
|
||||
|
||||
@Test void nonPositiveMeasurementsAndOverPreciseWeightCannotPersistPacking() {
|
||||
var receipt=receive(Leg.OVERSEAS,null,10,0,0,"r1").receipts().get(0);
|
||||
var source=List.of(new Source(receipt.id(),10));
|
||||
for(BigDecimal invalid:List.of(BigDecimal.ZERO,BigDecimal.ONE.negate())) {
|
||||
for(int axis=0;axis<4;axis++) {
|
||||
BigDecimal[] values={BigDecimal.TEN,BigDecimal.TEN,BigDecimal.TEN,BigDecimal.ONE};values[axis]=invalid;
|
||||
var carton=new Carton("C1",values[0],values[1],values[2],values[3],source);
|
||||
assertThatThrownBy(()->pack(List.of(carton))).hasMessageContaining("正数");
|
||||
}
|
||||
}
|
||||
assertThatThrownBy(()->pack(List.of(new Carton("C1",BigDecimal.TEN,BigDecimal.TEN,BigDecimal.TEN,new BigDecimal("1.0001"),source))))
|
||||
.hasMessageContaining("最多三位");
|
||||
assertThat(jdbc.queryForObject("SELECT COUNT(*) FROM ll_procurement_manifest",Integer.class)).isZero();
|
||||
}
|
||||
|
||||
@Test void foreignReceiptAndIncompleteAllocationCannotBecomeConfirmedPacking() {
|
||||
var receipt=receive(Leg.OVERSEAS,null,10,0,0,"r1").receipts().get(0);finish(Leg.OVERSEAS);
|
||||
assertThatThrownBy(()->pack(List.of(carton("C1",new Source(999,10))))).hasMessageContaining("不属于本单");
|
||||
var draft=pack(List.of(carton("C1",new Source(receipt.id(),9))));
|
||||
assertThatThrownBy(()->tx.execute(s->service.confirmPacking("9","PO-1","7",new ConfirmPacking(version(),"incomplete",draft.packingVersion(),"checked"))))
|
||||
.hasMessageContaining("全部");
|
||||
assertThat(service.read("9","PO-1").packingStatus()).isEqualTo("DRAFT");
|
||||
}
|
||||
|
||||
@Test void repackingCreatesANewVersionAndCannotRewriteAnActualDispatch() {
|
||||
var receipt=receive(Leg.OVERSEAS,null,10,0,0,"r1").receipts().get(0);finish(Leg.OVERSEAS);
|
||||
var first=pack(List.of(carton("C1",new Source(receipt.id(),10))));
|
||||
|
||||
+30
@@ -113,6 +113,36 @@ class ProcurementPurchasePersistenceTest {
|
||||
assertThat(confirmed.purchase().confirmed()).isFalse();verify(finance,never()).confirm(anyString(),anyString(),anyString(),any());
|
||||
}
|
||||
|
||||
@Test void unreviewedPurchaseFactCannotConfirmOrCharge() {
|
||||
jdbc.update("UPDATE ll_overseas_procurement_order SET reviewed_at=NULL,status='PENDING_REVIEW' WHERE id=11");
|
||||
var recorded=tx.execute(s->service.recordPurchase("9","PO-1","7",purchase("80","Sample")));
|
||||
assertThat(recorded.purchase().recorded()).isTrue();
|
||||
assertThat(recorded.purchase().confirmed()).isFalse();
|
||||
assertThat(recorded.purchase().issue()).contains("尚未审核");
|
||||
var again=tx.execute(s->service.confirmPurchase("9","PO-1","7",new Command(1L,"confirm-unreviewed","actual fact")));
|
||||
assertThat(again.purchase().confirmed()).isFalse();
|
||||
assertThat(again.status()).isEqualTo("PENDING_REVIEW");
|
||||
verify(finance,never()).confirm(anyString(),anyString(),anyString(),any());
|
||||
}
|
||||
|
||||
@Test void missingBudgetCannotConfirmOrInventACharge() {
|
||||
jdbc.update("UPDATE ll_overseas_procurement_order SET budget_unit_price=NULL WHERE id=11");
|
||||
var recorded=tx.execute(s->service.recordPurchase("9","PO-1","7",purchase("80","Sample")));
|
||||
assertThat(recorded.purchase().recorded()).isTrue();
|
||||
assertThat(recorded.purchase().confirmed()).isFalse();
|
||||
assertThat(recorded.purchase().issue()).contains("预算尚未明确");
|
||||
assertThat(jdbc.queryForObject("SELECT purchase_amount FROM ll_procurement_flow",BigDecimal.class)).isEqualByComparingTo("80");
|
||||
verify(finance,never()).confirm(anyString(),anyString(),anyString(),any());
|
||||
}
|
||||
|
||||
@Test void anotherPurchaseKeyCannotAppendOrOverwriteRecordedPurchase() {
|
||||
tx.execute(s->service.recordPurchase("9","PO-1","7",purchase("80","Sample")));
|
||||
var another=new Purchase(1L,"purchase-2","Sample","A",10,new BigDecimal("90"),"CNY",LocalDateTime.now().minusDays(1),LocalDateTime.now().plusDays(3),1,"additional purchase");
|
||||
assertThatThrownBy(()->tx.execute(s->service.recordPurchase("9","PO-1","7",another))).hasMessageContaining("不能再次购买");
|
||||
assertThat(jdbc.queryForObject("SELECT purchase_amount FROM ll_procurement_flow",BigDecimal.class)).isEqualByComparingTo("80");
|
||||
verify(finance,times(1)).confirm(anyString(),anyString(),anyString(),any());
|
||||
}
|
||||
|
||||
@Test void cancellationCannotErasePurchasedFacts() {
|
||||
tx.execute(s->service.recordPurchase("9","PO-1","7",purchase("80","Sample")));
|
||||
assertThatThrownBy(()->tx.execute(s->service.cancel("9","PO-1","7",new Command(1L,"cancel","reason")))).hasMessageContaining("购买后不能取消");
|
||||
|
||||
Reference in New Issue
Block a user