From 39c363069d68e0f3024f87d22ca8d5ae7c599086 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E9=B9=8F?= Date: Mon, 29 Jun 2026 10:35:22 +0800 Subject: [PATCH] Add business loop validation and coverage audit --- .../front/BusinessLoopCoverageValidator.java | 257 ++++++++++++++++++ .../front/BusinessLoopPlanValidator.java | 116 ++++++++ .../BusinessLoopCoverageValidatorTest.java | 193 +++++++++++++ .../front/BusinessLoopPlanValidatorTest.java | 195 +++++++++++++ 4 files changed, 761 insertions(+) create mode 100644 RuoYi-Vue/ruoyi-generator/src/main/java/com/ruoyi/generator/service/front/BusinessLoopCoverageValidator.java create mode 100644 RuoYi-Vue/ruoyi-generator/src/main/java/com/ruoyi/generator/service/front/BusinessLoopPlanValidator.java create mode 100644 RuoYi-Vue/ruoyi-generator/src/test/java/com/ruoyi/generator/service/front/BusinessLoopCoverageValidatorTest.java create mode 100644 RuoYi-Vue/ruoyi-generator/src/test/java/com/ruoyi/generator/service/front/BusinessLoopPlanValidatorTest.java diff --git a/RuoYi-Vue/ruoyi-generator/src/main/java/com/ruoyi/generator/service/front/BusinessLoopCoverageValidator.java b/RuoYi-Vue/ruoyi-generator/src/main/java/com/ruoyi/generator/service/front/BusinessLoopCoverageValidator.java new file mode 100644 index 0000000..1c475ec --- /dev/null +++ b/RuoYi-Vue/ruoyi-generator/src/main/java/com/ruoyi/generator/service/front/BusinessLoopCoverageValidator.java @@ -0,0 +1,257 @@ +package com.ruoyi.generator.service.front; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; +import org.springframework.stereotype.Component; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.generator.domain.front.dto.BusinessActionDesign; +import com.ruoyi.generator.domain.front.dto.BusinessActionEffectDesign; +import com.ruoyi.generator.domain.front.dto.BusinessLoopAuditResult; +import com.ruoyi.generator.domain.front.dto.BusinessLoopPlan; +import com.ruoyi.generator.domain.front.dto.BusinessLoopPlan.BusinessLoopAction; +import com.ruoyi.generator.domain.front.dto.BusinessLoopPlan.BusinessLoopQuantityEffect; +import com.ruoyi.generator.domain.front.dto.BusinessLoopPlan.BusinessLoopRecordEffect; +import com.ruoyi.generator.domain.front.dto.BusinessLoopPlan.BusinessLoopStateMachine; +import com.ruoyi.generator.domain.front.dto.DatabaseColumnDesign; +import com.ruoyi.generator.domain.front.dto.DatabaseDesignResponse; +import com.ruoyi.generator.domain.front.dto.DatabaseTableDesign; + +@Component +public class BusinessLoopCoverageValidator +{ + public BusinessLoopAuditResult auditDatabase(BusinessLoopPlan plan, DatabaseDesignResponse database) + { + BusinessLoopAuditResult result = newResult(plan); + Map> columnsByTable = collectColumnsByTable(database); + if (plan == null) + { + result.getErrors().add("Business loop plan is required"); + finish(result); + return result; + } + for (BusinessLoopStateMachine stateMachine : safe(plan.getStateMachines())) + { + if (stateMachine == null) + { + continue; + } + if (!hasColumn(columnsByTable, stateMachine.getTableName(), stateMachine.getStatusField())) + { + result.getErrors().add("Missing state status field: " + + formatField(stateMachine.getTableName(), stateMachine.getStatusField())); + } + } + for (BusinessLoopQuantityEffect quantityRule : safe(plan.getQuantityRules())) + { + if (quantityRule == null) + { + continue; + } + if (!hasColumn(columnsByTable, quantityRule.getTableName(), quantityRule.getQuantityField())) + { + result.getErrors().add("Missing quantity field: " + + formatField(quantityRule.getTableName(), quantityRule.getQuantityField())); + } + } + Set tableNames = columnsByTable.keySet(); + for (BusinessLoopRecordEffect recordRule : safe(plan.getRecordRules())) + { + if (recordRule == null) + { + continue; + } + if (!tableNames.contains(normalize(recordRule.getTableName()))) + { + result.getErrors().add("Missing record table: " + StringUtils.defaultString(recordRule.getTableName())); + } + } + finish(result); + return result; + } + + public BusinessLoopAuditResult auditBusinessActions(BusinessLoopPlan plan, List actions) + { + BusinessLoopAuditResult result = newResult(plan); + Map actionsByCode = collectActionsByCode(actions); + if (plan == null) + { + result.getErrors().add("Business loop plan is required"); + finish(result); + return result; + } + for (BusinessLoopAction planAction : safe(plan.getActions())) + { + if (planAction == null || !planAction.isRequired()) + { + continue; + } + BusinessActionDesign action = actionsByCode.get(planAction.getCode()); + if (action == null) + { + result.getMissingRequiredActions().add(planAction.getCode()); + continue; + } + validateRequiredEffects(result, planAction, action); + } + finish(result); + return result; + } + + private void validateRequiredEffects(BusinessLoopAuditResult result, BusinessLoopAction planAction, + BusinessActionDesign action) + { + if (size(planAction.getStateTransitions()) > 0 && !hasAnyEffect(action, "SET_STATUS", "UPDATE_FIELDS", "UPDATE_FIELD")) + { + result.getErrors().add("Missing status effect for required action: " + planAction.getCode()); + } + if (size(planAction.getQuantityEffects()) > 0 && !hasAnyEffect(action, "INCREASE_NUMBER", "DECREASE_NUMBER")) + { + result.getErrors().add("Missing quantity effect for required action: " + planAction.getCode()); + } + if (size(planAction.getRecordEffects()) > 0 && !hasAnyEffect(action, "INSERT_ROW", "UPDATE_FIELDS")) + { + result.getErrors().add("Missing record effect for required action: " + planAction.getCode()); + } + } + + private boolean hasAnyEffect(BusinessActionDesign action, String... types) + { + if (action == null || action.getEffects() == null) + { + return false; + } + Set expectedTypes = new HashSet(); + for (String type : types) + { + expectedTypes.add(type); + } + for (BusinessActionEffectDesign effect : action.getEffects()) + { + if (effect == null) + { + continue; + } + String type = StringUtils.defaultString(effect.getType()).toUpperCase(Locale.ENGLISH); + if (expectedTypes.contains(type)) + { + return true; + } + } + return false; + } + + private Map collectActionsByCode(List actions) + { + Map actionsByCode = new HashMap(); + if (actions == null) + { + return actionsByCode; + } + for (BusinessActionDesign action : actions) + { + if (action != null && StringUtils.isNotBlank(action.getCode())) + { + actionsByCode.put(action.getCode(), action); + } + } + return actionsByCode; + } + + private Map> collectColumnsByTable(DatabaseDesignResponse database) + { + Map> columnsByTable = new HashMap>(); + if (database == null || database.getTables() == null) + { + return columnsByTable; + } + for (DatabaseTableDesign table : database.getTables()) + { + if (table == null || StringUtils.isBlank(table.getTableName())) + { + continue; + } + Set columns = new HashSet(); + if (table.getColumns() != null) + { + for (DatabaseColumnDesign column : table.getColumns()) + { + if (column != null && StringUtils.isNotBlank(column.getColumnName())) + { + columns.add(normalize(column.getColumnName())); + } + } + } + columnsByTable.put(normalize(table.getTableName()), columns); + } + return columnsByTable; + } + + private boolean hasColumn(Map> columnsByTable, String tableName, String columnName) + { + Set columns = columnsByTable.get(normalize(tableName)); + return columns != null && columns.contains(normalize(columnName)); + } + + private String formatField(String tableName, String fieldName) + { + return StringUtils.defaultString(tableName) + "." + StringUtils.defaultString(fieldName); + } + + private BusinessLoopAuditResult newResult(BusinessLoopPlan plan) + { + BusinessLoopAuditResult result = new BusinessLoopAuditResult(); + result.setLoopActions(size(plan == null ? null : plan.getActions())); + result.setStateTransitions(countStateTransitions(plan)); + result.setQuantityRules(size(plan == null ? null : plan.getQuantityRules())); + result.setRecordRules(size(plan == null ? null : plan.getRecordRules())); + return result; + } + + private void finish(BusinessLoopAuditResult result) + { + result.setComplete(result.getErrors().isEmpty() && result.getMissingRequiredActions().isEmpty()); + } + + private int countStateTransitions(BusinessLoopPlan plan) + { + if (plan == null) + { + return 0; + } + int count = 0; + for (BusinessLoopStateMachine stateMachine : safe(plan.getStateMachines())) + { + if (stateMachine != null) + { + count += size(stateMachine.getTransitions()); + } + } + for (BusinessLoopAction action : safe(plan.getActions())) + { + if (action != null) + { + count += size(action.getStateTransitions()); + } + } + return count; + } + + private String normalize(String value) + { + return StringUtils.defaultString(value).toLowerCase(Locale.ENGLISH); + } + + private List safe(List items) + { + return items == null ? java.util.Collections.emptyList() : items; + } + + private int size(List items) + { + return items == null ? 0 : items.size(); + } +} diff --git a/RuoYi-Vue/ruoyi-generator/src/main/java/com/ruoyi/generator/service/front/BusinessLoopPlanValidator.java b/RuoYi-Vue/ruoyi-generator/src/main/java/com/ruoyi/generator/service/front/BusinessLoopPlanValidator.java new file mode 100644 index 0000000..b72cd3e --- /dev/null +++ b/RuoYi-Vue/ruoyi-generator/src/main/java/com/ruoyi/generator/service/front/BusinessLoopPlanValidator.java @@ -0,0 +1,116 @@ +package com.ruoyi.generator.service.front; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.regex.Pattern; +import org.springframework.stereotype.Component; +import com.ruoyi.common.exception.ServiceException; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.generator.domain.front.dto.BusinessLoopPlan; +import com.ruoyi.generator.domain.front.dto.BusinessLoopPlan.BusinessLoopAction; +import com.ruoyi.generator.domain.front.dto.BusinessLoopPlan.BusinessLoopObject; +import com.ruoyi.generator.domain.front.dto.BusinessLoopPlan.BusinessLoopStateMachine; + +@Component +public class BusinessLoopPlanValidator +{ + private static final int MAX_CORE_OBJECTS = 20; + private static final int MAX_ACTIONS = 30; + private static final Pattern ACTION_CODE = Pattern.compile("^[a-z][a-z0-9_]{1,63}$"); + + public void validate(BusinessLoopPlan plan) + { + if (plan == null) + { + throw new ServiceException("Business loop plan is required"); + } + validateCoreObjects(plan.getCoreObjects()); + validateActions(plan.getActions()); + if (countStateTransitions(plan) == 0) + { + throw new ServiceException("Business loop must contain at least one state transition"); + } + if (size(plan.getQuantityRules()) == 0 && size(plan.getRecordRules()) == 0) + { + throw new ServiceException("Business loop must contain at least one quantity or record rule"); + } + } + + private void validateCoreObjects(List coreObjects) + { + if (size(coreObjects) == 0) + { + throw new ServiceException("Business loop core objects cannot be empty"); + } + if (coreObjects.size() > MAX_CORE_OBJECTS) + { + throw new ServiceException("Business loop core object count exceeds limit"); + } + } + + private void validateActions(List actions) + { + if (size(actions) == 0) + { + throw new ServiceException("Business loop actions cannot be empty"); + } + if (actions.size() > MAX_ACTIONS) + { + throw new ServiceException("Business loop action count exceeds limit"); + } + Set actionCodes = new HashSet(); + boolean hasRequiredAction = false; + for (BusinessLoopAction action : actions) + { + if (action == null) + { + throw new ServiceException("Business loop action cannot be empty"); + } + if (StringUtils.isBlank(action.getCode()) || !ACTION_CODE.matcher(action.getCode()).matches()) + { + throw new ServiceException("Business loop action code is invalid"); + } + if (!actionCodes.add(action.getCode())) + { + throw new ServiceException("Business loop action code duplicated: " + action.getCode()); + } + hasRequiredAction = hasRequiredAction || action.isRequired(); + } + if (!hasRequiredAction) + { + throw new ServiceException("Business loop must contain at least one required action"); + } + } + + private int countStateTransitions(BusinessLoopPlan plan) + { + int count = 0; + if (plan.getStateMachines() != null) + { + for (BusinessLoopStateMachine stateMachine : plan.getStateMachines()) + { + if (stateMachine != null) + { + count += size(stateMachine.getTransitions()); + } + } + } + if (plan.getActions() != null) + { + for (BusinessLoopAction action : plan.getActions()) + { + if (action != null) + { + count += size(action.getStateTransitions()); + } + } + } + return count; + } + + private int size(List items) + { + return items == null ? 0 : items.size(); + } +} diff --git a/RuoYi-Vue/ruoyi-generator/src/test/java/com/ruoyi/generator/service/front/BusinessLoopCoverageValidatorTest.java b/RuoYi-Vue/ruoyi-generator/src/test/java/com/ruoyi/generator/service/front/BusinessLoopCoverageValidatorTest.java new file mode 100644 index 0000000..0fe8311 --- /dev/null +++ b/RuoYi-Vue/ruoyi-generator/src/test/java/com/ruoyi/generator/service/front/BusinessLoopCoverageValidatorTest.java @@ -0,0 +1,193 @@ +package com.ruoyi.generator.service.front; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import java.util.Arrays; +import java.util.Collections; +import org.junit.Test; +import com.ruoyi.generator.domain.front.dto.BusinessActionDesign; +import com.ruoyi.generator.domain.front.dto.BusinessActionEffectDesign; +import com.ruoyi.generator.domain.front.dto.BusinessLoopAuditResult; +import com.ruoyi.generator.domain.front.dto.BusinessLoopPlan; +import com.ruoyi.generator.domain.front.dto.BusinessLoopPlan.BusinessLoopAction; +import com.ruoyi.generator.domain.front.dto.BusinessLoopPlan.BusinessLoopObject; +import com.ruoyi.generator.domain.front.dto.BusinessLoopPlan.BusinessLoopQuantityEffect; +import com.ruoyi.generator.domain.front.dto.BusinessLoopPlan.BusinessLoopRecordEffect; +import com.ruoyi.generator.domain.front.dto.BusinessLoopPlan.BusinessLoopStateMachine; +import com.ruoyi.generator.domain.front.dto.BusinessLoopPlan.BusinessLoopStateTransition; +import com.ruoyi.generator.domain.front.dto.DatabaseColumnDesign; +import com.ruoyi.generator.domain.front.dto.DatabaseDesignResponse; +import com.ruoyi.generator.domain.front.dto.DatabaseTableDesign; + +public class BusinessLoopCoverageValidatorTest +{ + private final BusinessLoopCoverageValidator validator = new BusinessLoopCoverageValidator(); + + @Test + public void databaseAuditPassesWhenTablesAndFieldsCoverPlan() + { + BusinessLoopAuditResult result = validator.auditDatabase(completePlan(), completeDatabase()); + + assertTrue(result.isComplete()); + assertTrue(result.getErrors().isEmpty()); + assertEquals(1, result.getLoopActions()); + assertEquals(2, result.getStateTransitions()); + assertEquals(1, result.getQuantityRules()); + assertEquals(1, result.getRecordRules()); + } + + @Test + public void databaseAuditFailsWhenQuantityFieldMissing() + { + DatabaseDesignResponse database = completeDatabase(); + database.getTables().get(1).setColumns(Collections.emptyList()); + + BusinessLoopAuditResult result = validator.auditDatabase(completePlan(), database); + + assertFalse(result.isComplete()); + assertTrue(contains(result.getErrors().toString(), "available_count")); + } + + @Test + public void businessActionAuditFailsWhenRequiredActionMissing() + { + BusinessLoopAuditResult result = validator.auditBusinessActions(completePlan(), + Collections.emptyList()); + + assertFalse(result.isComplete()); + assertTrue(result.getMissingRequiredActions().contains("confirm_borrow")); + } + + @Test + public void businessActionAuditPassesWhenRequiredStatusAndQuantityEffectsExist() + { + BusinessActionDesign action = new BusinessActionDesign(); + action.setCode("confirm_borrow"); + action.setEffects(Arrays.asList(effect("SET_STATUS"), effect("DECREASE_NUMBER"))); + + BusinessLoopAuditResult result = validator.auditBusinessActions(completePlan(), Arrays.asList(action)); + + assertTrue(result.isComplete()); + assertTrue(result.getMissingRequiredActions().isEmpty()); + assertTrue(result.getErrors().isEmpty()); + } + + private BusinessLoopPlan completePlan() + { + BusinessLoopPlan plan = new BusinessLoopPlan(); + plan.setDomainName("library"); + plan.setCoreObjects(Arrays.asList(coreObject())); + plan.setStateMachines(Arrays.asList(stateMachine())); + plan.setActions(Arrays.asList(requiredAction())); + plan.setQuantityRules(Arrays.asList(quantityRule())); + plan.setRecordRules(Arrays.asList(recordRule())); + return plan; + } + + private BusinessLoopObject coreObject() + { + BusinessLoopObject object = new BusinessLoopObject(); + object.setCode("borrow_order"); + object.setName("Borrow order"); + object.setTableName("borrow_order"); + return object; + } + + private BusinessLoopStateMachine stateMachine() + { + BusinessLoopStateMachine stateMachine = new BusinessLoopStateMachine(); + stateMachine.setObjectCode("borrow_order"); + stateMachine.setTableName("borrow_order"); + stateMachine.setStatusField("borrow_status"); + stateMachine.setStates(Arrays.asList("submitted", "borrowed")); + stateMachine.setTransitions(Arrays.asList(transition())); + return stateMachine; + } + + private BusinessLoopAction requiredAction() + { + BusinessLoopAction action = new BusinessLoopAction(); + action.setCode("confirm_borrow"); + action.setName("Confirm borrow"); + action.setOwnerTable("borrow_order"); + action.setRequired(true); + action.setStateTransitions(Arrays.asList(transition())); + action.setQuantityEffects(Arrays.asList(quantityRule())); + return action; + } + + private BusinessLoopStateTransition transition() + { + BusinessLoopStateTransition transition = new BusinessLoopStateTransition(); + transition.setActionCode("confirm_borrow"); + transition.setFromState("submitted"); + transition.setToState("borrowed"); + return transition; + } + + private BusinessLoopQuantityEffect quantityRule() + { + BusinessLoopQuantityEffect effect = new BusinessLoopQuantityEffect(); + effect.setActionCode("confirm_borrow"); + effect.setObjectCode("book_stock"); + effect.setTableName("book_stock"); + effect.setQuantityField("available_count"); + effect.setEffectType("decrease"); + effect.setAmount("1"); + return effect; + } + + private BusinessLoopRecordEffect recordRule() + { + BusinessLoopRecordEffect effect = new BusinessLoopRecordEffect(); + effect.setActionCode("confirm_borrow"); + effect.setObjectCode("borrow_record"); + effect.setTableName("borrow_record"); + effect.setOperation("insert"); + return effect; + } + + private DatabaseDesignResponse completeDatabase() + { + DatabaseDesignResponse response = new DatabaseDesignResponse(); + response.setTables(Arrays.asList( + table("borrow_order", "id", "borrow_status"), + table("book_stock", "id", "available_count"), + table("borrow_record", "id"))); + return response; + } + + private DatabaseTableDesign table(String tableName, String... columnNames) + { + DatabaseTableDesign table = new DatabaseTableDesign(); + table.setTableName(tableName); + DatabaseColumnDesign[] columns = new DatabaseColumnDesign[columnNames.length]; + for (int i = 0; i < columnNames.length; i++) + { + columns[i] = column(columnNames[i]); + } + table.setColumns(Arrays.asList(columns)); + return table; + } + + private DatabaseColumnDesign column(String columnName) + { + DatabaseColumnDesign column = new DatabaseColumnDesign(); + column.setColumnName(columnName); + return column; + } + + private BusinessActionEffectDesign effect(String type) + { + BusinessActionEffectDesign effect = new BusinessActionEffectDesign(); + effect.setType(type); + return effect; + } + + private boolean contains(String text, String needle) + { + return text != null && text.contains(needle); + } +} diff --git a/RuoYi-Vue/ruoyi-generator/src/test/java/com/ruoyi/generator/service/front/BusinessLoopPlanValidatorTest.java b/RuoYi-Vue/ruoyi-generator/src/test/java/com/ruoyi/generator/service/front/BusinessLoopPlanValidatorTest.java new file mode 100644 index 0000000..80fbab8 --- /dev/null +++ b/RuoYi-Vue/ruoyi-generator/src/test/java/com/ruoyi/generator/service/front/BusinessLoopPlanValidatorTest.java @@ -0,0 +1,195 @@ +package com.ruoyi.generator.service.front; + +import static org.junit.Assert.assertTrue; + +import java.util.Arrays; +import java.util.Collections; +import org.junit.Test; +import com.ruoyi.common.exception.ServiceException; +import com.ruoyi.generator.domain.front.dto.BusinessLoopPlan; +import com.ruoyi.generator.domain.front.dto.BusinessLoopPlan.BusinessLoopAction; +import com.ruoyi.generator.domain.front.dto.BusinessLoopPlan.BusinessLoopObject; +import com.ruoyi.generator.domain.front.dto.BusinessLoopPlan.BusinessLoopQuantityEffect; +import com.ruoyi.generator.domain.front.dto.BusinessLoopPlan.BusinessLoopRecordEffect; +import com.ruoyi.generator.domain.front.dto.BusinessLoopPlan.BusinessLoopStateMachine; +import com.ruoyi.generator.domain.front.dto.BusinessLoopPlan.BusinessLoopStateTransition; + +public class BusinessLoopPlanValidatorTest +{ + private final BusinessLoopPlanValidator validator = new BusinessLoopPlanValidator(); + + @Test + public void acceptsCompleteLoopPlan() + { + validator.validate(completePlan()); + } + + @Test + public void rejectsPlanWithoutStateTransition() + { + BusinessLoopPlan plan = completePlan(); + plan.getStateMachines().get(0).setTransitions(Collections.emptyList()); + plan.getActions().get(0).setStateTransitions(Collections.emptyList()); + + ServiceException exception = expectServiceException(new ThrowingRunnable() + { + @Override + public void run() + { + validator.validate(plan); + } + }); + + assertTrue(exception.getMessage().contains("state transition")); + } + + @Test + public void rejectsPlanWithoutQuantityOrRecordRule() + { + BusinessLoopPlan plan = completePlan(); + plan.setQuantityRules(Collections.emptyList()); + plan.setRecordRules(Collections.emptyList()); + + ServiceException exception = expectServiceException(new ThrowingRunnable() + { + @Override + public void run() + { + validator.validate(plan); + } + }); + + assertTrue(exception.getMessage().contains("quantity or record")); + } + + @Test + public void rejectsDuplicatedActionCode() + { + BusinessLoopPlan plan = completePlan(); + plan.setActions(Arrays.asList(requiredAction("confirm_borrow"), requiredAction("confirm_borrow"))); + + ServiceException exception = expectServiceException(new ThrowingRunnable() + { + @Override + public void run() + { + validator.validate(plan); + } + }); + + assertTrue(exception.getMessage().contains("duplicated")); + } + + @Test + public void rejectsPlanWithoutRequiredAction() + { + BusinessLoopPlan plan = completePlan(); + plan.getActions().get(0).setRequired(false); + + ServiceException exception = expectServiceException(new ThrowingRunnable() + { + @Override + public void run() + { + validator.validate(plan); + } + }); + + assertTrue(exception.getMessage().contains("required action")); + } + + private BusinessLoopPlan completePlan() + { + BusinessLoopPlan plan = new BusinessLoopPlan(); + plan.setDomainName("library"); + plan.setCoreObjects(Arrays.asList(coreObject())); + plan.setStateMachines(Arrays.asList(stateMachine())); + plan.setActions(Arrays.asList(requiredAction("confirm_borrow"))); + plan.setQuantityRules(Arrays.asList(quantityRule())); + plan.setRecordRules(Arrays.asList(recordRule())); + return plan; + } + + private BusinessLoopObject coreObject() + { + BusinessLoopObject object = new BusinessLoopObject(); + object.setCode("borrow_order"); + object.setName("Borrow order"); + object.setTableName("borrow_order"); + return object; + } + + private BusinessLoopStateMachine stateMachine() + { + BusinessLoopStateMachine stateMachine = new BusinessLoopStateMachine(); + stateMachine.setObjectCode("borrow_order"); + stateMachine.setTableName("borrow_order"); + stateMachine.setStatusField("borrow_status"); + stateMachine.setStates(Arrays.asList("submitted", "borrowed")); + stateMachine.setTransitions(Arrays.asList(transition())); + return stateMachine; + } + + private BusinessLoopAction requiredAction(String code) + { + BusinessLoopAction action = new BusinessLoopAction(); + action.setCode(code); + action.setName("Confirm borrow"); + action.setActor("librarian"); + action.setOwnerObject("borrow_order"); + action.setOwnerTable("borrow_order"); + action.setRequired(true); + action.setStateTransitions(Arrays.asList(transition())); + action.setQuantityEffects(Arrays.asList(quantityRule())); + return action; + } + + private BusinessLoopStateTransition transition() + { + BusinessLoopStateTransition transition = new BusinessLoopStateTransition(); + transition.setActionCode("confirm_borrow"); + transition.setFromState("submitted"); + transition.setToState("borrowed"); + return transition; + } + + private BusinessLoopQuantityEffect quantityRule() + { + BusinessLoopQuantityEffect effect = new BusinessLoopQuantityEffect(); + effect.setActionCode("confirm_borrow"); + effect.setObjectCode("book_stock"); + effect.setTableName("book_stock"); + effect.setQuantityField("available_count"); + effect.setEffectType("decrease"); + effect.setAmount("1"); + return effect; + } + + private BusinessLoopRecordEffect recordRule() + { + BusinessLoopRecordEffect effect = new BusinessLoopRecordEffect(); + effect.setActionCode("confirm_borrow"); + effect.setObjectCode("borrow_record"); + effect.setTableName("borrow_record"); + effect.setOperation("insert"); + return effect; + } + + private ServiceException expectServiceException(ThrowingRunnable runnable) + { + try + { + runnable.run(); + } + catch (ServiceException e) + { + return e; + } + throw new AssertionError("Expected ServiceException"); + } + + private interface ThrowingRunnable + { + void run(); + } +}