Skip to content

Case 21: Household & purchase qualification

This case corresponds to USE-CASES scenario 21, “Household / home purchase / car purchase qualification under multiple policies (requirement 16)”. It belongs to the government and enterprise services domain, targeting a unified citizen qualification application portal for human resources, public security, housing, and other departments. It automatically performs five-dimension points tiering for household registration, contribution-year threshold checks for home purchase and the new-energy passenger car quota, and applies a one-vote veto to subjects with serious dishonesty records.

Citizens apply for three types of public service qualifications through a unified government service portal: household registration (SETTLE), home purchase (HOUSE_PURCHASE), and the new-energy passenger car quota (CAR_PURCHASE). All three share the same set of application elements, sourced from human resources, tax, public security, housing, and other departments: age, education (doctor / master / bachelor / college / other), consecutive months of local social security contributions, consecutive months of local individual income tax payments, whether the applicant already owns a home in the city, whether the applicant already has a local passenger car registered, and whether there is a record of serious dishonesty. Household registration additionally requires a declaration channel (employment EMPLOYEE / investment INVESTMENT / talent introduction TALENT), years since founding the enterprise and last year’s tax payments, and a talent title (high-level talent TOP_TALENT / urgently needed talent ELITE / none).

Household registration uses a points system, scored across five dimensions: education, social security, age, talent title, and investment tax payments, with maximum scores of 40 / 40 / 15 / 30 / 20 respectively; the investment tax score is only counted for the investment channel. The points conclusion has three tiers: 80 points or above approves the registration (APPROVE), 60-79 points goes to manual review with public notice (REVIEW), and anything else is rejected (REJECT). The three household registration channels also have prerequisite conditions: the employment channel requires at least 6 months of social security, the investment channel requires the enterprise to have been founded for at least 1 year with annual tax payments of no less than 100,000 yuan, and the talent introduction channel requires a high-level or urgently needed talent certification.

Home purchase and car purchase do not use points; they set thresholds on consecutive months of social security or income tax contributions, and meeting either one is sufficient: under the standard policy, a non-local-hukou family buying a home needs at least 36 months, relaxed to 3 months for high-level or urgently needed talent; the new-energy passenger car quota requires at least 24 months under the standard policy, relaxed to 6 months for talent, and the applicant must not already have a local passenger car registered; those who do not meet the direct application conditions must join the regular quota lottery. Home purchase is additionally subject to purchase restrictions: non-local-hukou families that already own a home are rejected outright.

Decision results are unified into four conclusions: ALLOW (qualification granted) / APPROVE (household registration approved) / REVIEW (manual review with public notice) / REJECT (not approved), carrying per-dimension scores and threshold check details (details) plus policy explanations (reasons), directly supporting the generation of service window notices and case filing records; a record of serious dishonesty triggers a one-vote veto at the unified entry point for all three matters.

  • Scattered and frequently changing policy rules: the approval lines 80/60, the segmented social security scoring, the 36-month home purchase and 24-month car purchase thresholds, and the relaxed talent months may all be revised annually with urban population policy; once hard-coded into government systems, every adjustment requires a new release and a new security compliance assessment.
  • Three matters share the same elements but use different decision logic: household registration uses multi-dimensional points, home and car purchases use contribution-year thresholds, plus hard constraints such as purchase restrictions and already owning a car; hard-coding the branches into the case workflow makes it easy to mix up policy rules.
  • Channel prerequisites must intercept before points calculation: applicants with less than 6 months of employment social security, insufficient enterprise years or tax payments for investment, or no certification for talent must not enter the points calculation; a missed check causes wrongful approvals.
  • Hard veto items must be front-loaded and traceable: the one-vote veto for serious dishonesty, the purchase restriction for existing homeowners, and the ban on re-applying for a car quota when already owning a local car must prevent accidents like “approved because the points are enough”.
  • Review conclusions must be explainable to citizens item by item: every score, the social security and tax contribution months, and the basis for relaxed thresholds must be traceable and open to reconsideration; black-box decisions cannot meet government disclosure requirements.
  • Legacy government systems with domestic-stack and JDK 8 constraints: government clouds mostly run on domestically produced environments, so a heavy standalone rule platform cannot be introduced just for qualification decisions.

This case follows the six-piece skeleton: input model + decision result model + rule interface + XML rule script + Service orchestration + 6 groups of tests based on real business scenarios.

Layer Responsibility Files
Domain model Citizen qualification application elements and review decision result (conclusion / points / details / policy reasons) CityQualificationApplication.java, CityQualificationDecision.java
Rule set and XML Five-dimension points rules, household registration tiers, home/car contribution-month thresholds and talent relaxations CityQualificationRules.java, city-qualification.xml
Orchestration service Matter routing, dishonesty one-vote veto, channel thresholds, purchase restriction checks, points aggregation and explanation assembly CityQualificationService.java
Tests 6 policy scenarios (talent high-score approval / employment low-score rejection / investment borderline review / thresholds and dishonesty / home purchase / car purchase) CityQualificationServiceTest.java
Rule method Rule Score / output
educationScore Education points: doctor 40 / master 30 / bachelor 20 / college 10 / other 0 0-40
securityScore Social security: 10 points per full 12 months: 40 for >=48 months, 30 for >=36, 20 for >=24, 10 for >=12, otherwise 0 0-40
ageSettleScore Age points: 15 for ≤35, 10 for ≤45, 5 for ≤55, otherwise 0 0-15
titleScore Talent title points: TOP_TALENT 30, ELITE 15, otherwise 0 0-30
investTaxScore Enterprise tax paid last year under investment registration: 20 for >=1 million yuan, 10 for >=500k, 5 for >=100k, otherwise 0 0-20
settleResult Household registration tiers: points >=80 APPROVE, >=60 REVIEW, otherwise REJECT Three conclusions
houseEligible Home purchase eligibility: social security or tax >=36 months; TOP_TALENT / ELITE relaxed to >=3 months boolean
carEligible New-energy quota: provided no local car is registered, social security or tax >=24 months; talent relaxed to >=6 months boolean

Place the following files into the test directory of a JQuick-Java project following the source package structure: the two models go into src/test/java/com/github/paohaijiao/demo/model/, the rule interface and test class into src/test/java/com/github/paohaijiao/demo/, the Service into src/test/java/com/github/paohaijiao/demo/service/, and the XML script into src/test/resources/demo/, keeping the original package names (com.github.paohaijiao.demo / .model / .service). Then run mvn test -Dtest=CityQualificationServiceTest to execute all 6 test cases.

package com.github.paohaijiao.demo.model;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* 市民资格申请要素(落户 / 购房 / 购车三类政策入口共用)
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class CityQualificationApplication {
/**
* 申请单号
*/
private String applicationId;
/**
* 申请人姓名
*/
private String applicantName;
/**
* 申请事项:SETTLE-落户 / HOUSE_PURCHASE-购房 / CAR_PURCHASE-购车
*/
private String benefit;
/**
* 年龄
*/
private int age;
/**
* 学历:DOCTOR / MASTER / BACHELOR / COLLEGE / OTHER
*/
private String education;
/**
* 本市社保连续缴纳月数
*/
private int socialSecurityMonths;
/**
* 本市个税连续缴纳月数
*/
private int taxPaymentMonths;
/**
* 在本市是否已拥有住房
*/
private boolean hasPropertyInCity;
/**
* 名下是否已有本市小客车
*/
private boolean hasCarInCity;
/**
* 落户通道:EMPLOYEE-就业 / INVESTMENT-投资 / TALENT-人才引进
*/
private String channel;
/**
* 创办企业年限(投资落户)
*/
private int companyYears;
/**
* 创办企业上年纳税额(万元,投资落户)
*/
private double enterpriseTaxWan;
/**
* 人才称号:TOP_TALENT-高层次人才 / ELITE-紧缺人才 / NONE-无
*/
private String talentTitle;
/**
* 是否存在严重失信记录
*/
private boolean badCreditRecord;
}
package com.github.paohaijiao.demo.model;
import lombok.Data;
import java.util.ArrayList;
import java.util.List;
/**
* 市民资格审核决策结果(落户积分 / 购房购车门槛共用)
*/
@Data
public class CityQualificationDecision {
/**
* 申请单号
*/
private String applicationId;
/**
* 申请事项
*/
private String benefit;
/**
* 审核结论:ALLOW-资格通过 / APPROVE-核准落户 / REVIEW-复核公示 / REJECT-不通过
*/
private String result;
/**
* 落户积分(仅落户事项计分,其余事项为 0)
*/
private int points;
/**
* 各项得分与门槛核验明细
*/
private List<String> details = new ArrayList<String>();
/**
* 不通过原因与政策说明
*/
private List<String> reasons = new ArrayList<String>();
public void addDetail(String detail) {
this.details.add(detail);
}
public void addReason(String reason) {
this.reasons.add(reason);
}
}
package com.github.paohaijiao.demo;
import com.github.paohaijiao.xml.param.Param;
/**
* 场景 21:落户 / 购房 / 购车资格多政策判定规则集
* <p>
* 落户按学历、社保、年龄、人才称号、投资纳税五维积分(80 核准 / 60 复核);
* 购房购车按社保或个税连续缴纳月数设置门槛,人才通道缩短年限。
* 规则脚本见 {@code src/test/resources/demo/city-qualification.xml}。
*/
public interface CityQualificationRules {
/** 学历积分:博士 40 / 硕士 30 / 本科 20 / 大专 10 / 其他 0 */
int educationScore(@Param("education") String education);
/** 社保积分:每满 12 个月 10 分,48 个月封顶 40 分 */
int securityScore(@Param("months") int months);
/** 年龄积分:35 岁以下 15 / 45 岁以下 10 / 55 岁以下 5 */
int ageSettleScore(@Param("age") int age);
/** 人才称号积分:高层次 30 / 紧缺 15 / 无 0 */
int titleScore(@Param("talentTitle") String talentTitle);
/** 投资纳税积分:年纳税 100/50/10 万对应 20/10/5 分 */
int investTaxScore(@Param("enterpriseTaxWan") double enterpriseTaxWan);
/** 落户结论:80 分以上核准,60 分以上复核,其余不通过 */
String settleResult(@Param("points") int points);
/** 购房资格:社保或个税满 36 个月;高层次/紧缺人才放宽至 3 个月 */
boolean houseEligible(@Param("ssMonths") int ssMonths,
@Param("taxMonths") int taxMonths,
@Param("talentTitle") String talentTitle);
/** 购车(新能源指标)资格:普通满 24 个月、人才满 6 个月,且名下无本市小客车 */
boolean carEligible(@Param("ssMonths") int ssMonths,
@Param("taxMonths") int taxMonths,
@Param("noCarInCity") boolean noCarInCity,
@Param("talentTitle") String talentTitle);
}
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE javas PUBLIC "-//PAOHAIJIAO//DTD API JAVA 1.0//EN"
"classpath:paohaijiao/dtd/Jquick-java.dtd">
<javas namespace="com.github.paohaijiao.demo.CityQualificationRules">
<!-- 学历积分 -->
<java name="educationScore" returnClass="int">
<![CDATA[
int def educationScore(str:education) {
if (education == "DOCTOR") {
return 40;
} else if (education == "MASTER") {
return 30;
} else if (education == "BACHELOR") {
return 20;
} else if (education == "COLLEGE") {
return 10;
} else {
return 0;
}
}
]]>
</java>
<!-- 社保积分:分段等价于每满 12 个月 10 分,48 月封顶 -->
<java name="securityScore" returnClass="int">
<![CDATA[
int def securityScore(int:months) {
if (months >= 48) {
return 40;
} else if (months >= 36) {
return 30;
} else if (months >= 24) {
return 20;
} else if (months >= 12) {
return 10;
} else {
return 0;
}
}
]]>
</java>
<!-- 年龄积分 -->
<java name="ageSettleScore" returnClass="int">
<![CDATA[
int def ageSettleScore(int:age) {
if (age <= 35) {
return 15;
} else if (age <= 45) {
return 10;
} else if (age <= 55) {
return 5;
} else {
return 0;
}
}
]]>
</java>
<!-- 人才称号积分 -->
<java name="titleScore" returnClass="int">
<![CDATA[
int def titleScore(str:talentTitle) {
if (talentTitle == "TOP_TALENT") {
return 30;
} else if (talentTitle == "ELITE") {
return 15;
} else {
return 0;
}
}
]]>
</java>
<!-- 投资纳税积分 -->
<java name="investTaxScore" returnClass="int">
<![CDATA[
int def investTaxScore(double:enterpriseTaxWan) {
if (enterpriseTaxWan >= 100.0) {
return 20;
} else if (enterpriseTaxWan >= 50.0) {
return 10;
} else if (enterpriseTaxWan >= 10.0) {
return 5;
} else {
return 0;
}
}
]]>
</java>
<!-- 落户结论分档 -->
<java name="settleResult" returnClass="java.lang.String">
<![CDATA[
str def settleResult(int:points) {
if (points >= 80) {
return "APPROVE";
} else if (points >= 60) {
return "REVIEW";
} else {
return "REJECT";
}
}
]]>
</java>
<!-- 购房资格:普通 36 月,人才 3 月(社保、个税满足其一即可) -->
<java name="houseEligible" returnClass="boolean">
<![CDATA[
boolean def houseEligible(int:ssMonths, int:taxMonths, str:talentTitle) {
boolean eligible = false;
if (ssMonths >= 36 || taxMonths >= 36) {
eligible = true;
}
if (talentTitle == "TOP_TALENT") {
if (ssMonths >= 3 || taxMonths >= 3) {
eligible = true;
}
}
if (talentTitle == "ELITE") {
if (ssMonths >= 3 || taxMonths >= 3) {
eligible = true;
}
}
return eligible;
}
]]>
</java>
<!-- 购车新能源指标:普通 24 月,人才 6 月,且名下无车 -->
<java name="carEligible" returnClass="boolean">
<![CDATA[
boolean def carEligible(int:ssMonths, int:taxMonths, boolean:noCarInCity, str:talentTitle) {
boolean eligible = false;
if (noCarInCity) {
if (ssMonths >= 24 || taxMonths >= 24) {
eligible = true;
}
if (talentTitle == "TOP_TALENT") {
if (ssMonths >= 6 || taxMonths >= 6) {
eligible = true;
}
}
if (talentTitle == "ELITE") {
if (ssMonths >= 6 || taxMonths >= 6) {
eligible = true;
}
}
}
return eligible;
}
]]>
</java>
</javas>
package com.github.paohaijiao.demo.service;
import com.github.paohaijiao.demo.CityQualificationRules;
import com.github.paohaijiao.demo.model.CityQualificationApplication;
import com.github.paohaijiao.demo.model.CityQualificationDecision;
/**
* 场景 21:落户 / 购房 / 购车资格多政策判定服务
* <p>
* 职责划分:
* <ul>
* <li><b>Java 侧</b>:事项路由、失信一票否决、落户通道门槛、限购核验、积分汇总</li>
* <li><b>规则侧</b>:五维积分口径、落户分档线、购房购车缴纳月数门槛,全部外置在 XML 中</li>
* </ul>
*/
public class CityQualificationService {
private final CityQualificationRules rules;
public CityQualificationService(CityQualificationRules rules) {
this.rules = rules;
}
public CityQualificationDecision evaluate(CityQualificationApplication app) {
CityQualificationDecision decision = new CityQualificationDecision();
decision.setApplicationId(app.getApplicationId());
decision.setBenefit(app.getBenefit());
if (app.isBadCreditRecord()) {
decision.setResult("REJECT");
decision.addReason("申请人存在严重失信记录,触发一票否决");
return decision;
}
if ("SETTLE".equals(app.getBenefit())) {
evaluateSettle(app, decision);
} else if ("HOUSE_PURCHASE".equals(app.getBenefit())) {
evaluateHouse(app, decision);
} else {
evaluateCar(app, decision);
}
return decision;
}
private void evaluateSettle(CityQualificationApplication app, CityQualificationDecision decision) {
if ("EMPLOYEE".equals(app.getChannel()) && app.getSocialSecurityMonths() < 6) {
decision.setResult("REJECT");
decision.addReason("就业落户人员在本市连续缴纳社保不满 6 个月,不符合通道基本条件");
return;
}
if ("INVESTMENT".equals(app.getChannel())
&& (app.getCompanyYears() < 1 || app.getEnterpriseTaxWan() < 10.0)) {
decision.setResult("REJECT");
decision.addReason("投资落户须创办企业满 1 年且年纳税不低于 10 万元");
return;
}
if ("TALENT".equals(app.getChannel()) && "NONE".equals(app.getTalentTitle())) {
decision.setResult("REJECT");
decision.addReason("人才引进通道须持有高层次或紧缺人才认定证明");
return;
}
int edu = rules.educationScore(app.getEducation());
int security = rules.securityScore(app.getSocialSecurityMonths());
int age = rules.ageSettleScore(app.getAge());
int title = rules.titleScore(app.getTalentTitle());
int invest = "INVESTMENT".equals(app.getChannel())
? rules.investTaxScore(app.getEnterpriseTaxWan()) : 0;
int points = edu + security + age + title + invest;
decision.addDetail("学历积分: " + edu + " / 40");
decision.addDetail("社保积分: " + security + " / 40");
decision.addDetail("年龄积分: " + age + " / 15");
decision.addDetail("人才称号积分: " + title + " / 30");
decision.addDetail("投资纳税积分: " + invest + " / 20");
decision.addDetail("落户总积分: " + points);
decision.setPoints(points);
String result = rules.settleResult(points);
decision.setResult(result);
if ("APPROVE".equals(result)) {
decision.addReason("积分 " + points + " 分达到核准线(80 分),准予落户");
} else if ("REVIEW".equals(result)) {
decision.addReason("积分 " + points + " 分处于复核公示区间(60-79 分),转人工复核");
} else {
decision.addReason("积分 " + points + " 分未达落户标准(60 分)");
}
}
private void evaluateHouse(CityQualificationApplication app, CityQualificationDecision decision) {
if (app.isHasPropertyInCity()) {
decision.setResult("REJECT");
decision.addReason("非本市户籍家庭在本市已拥有 1 套住房,受限购政策约束");
return;
}
boolean eligible = rules.houseEligible(app.getSocialSecurityMonths(),
app.getTaxPaymentMonths(), app.getTalentTitle());
decision.addDetail("社保连续缴纳: " + app.getSocialSecurityMonths() + " 个月");
decision.addDetail("个税连续缴纳: " + app.getTaxPaymentMonths() + " 个月");
if (eligible) {
decision.setResult("ALLOW");
decision.addReason("满足非本市户籍家庭购房缴纳年限要求,具备购房资格");
} else {
decision.setResult("REJECT");
decision.addReason("社保或个税连续缴纳不满 36 个月,且不符合人才放宽条件");
}
}
private void evaluateCar(CityQualificationApplication app, CityQualificationDecision decision) {
if (app.isHasCarInCity()) {
decision.setResult("REJECT");
decision.addReason("申请人名下已登记本市小客车,不能重复申领新能源指标");
return;
}
boolean eligible = rules.carEligible(app.getSocialSecurityMonths(),
app.getTaxPaymentMonths(), true, app.getTalentTitle());
decision.addDetail("社保连续缴纳: " + app.getSocialSecurityMonths() + " 个月");
decision.addDetail("个税连续缴纳: " + app.getTaxPaymentMonths() + " 个月");
if (eligible) {
decision.setResult("ALLOW");
decision.addReason("满足新能源小客车指标申请条件,准予核发指标");
} else {
decision.setResult("REJECT");
decision.addReason("不满足新能源指标直接申请条件,需参与普通指标摇号");
}
}
}
package com.github.paohaijiao.demo;
import com.github.paohaijiao.demo.model.CityQualificationApplication;
import com.github.paohaijiao.demo.model.CityQualificationDecision;
import com.github.paohaijiao.demo.service.CityQualificationService;
import com.github.paohaijiao.xml.JQuickJava;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
/**
* 场景 21:落户 / 购房 / 购车资格多政策判定
* <p>
* 覆盖 6 组政策形态:人才通道高分落户、就业通道积分不足、投资通道临界复核、
* 通道门槛与失信否决、购房年限与限购、购车普通与人才通道。
*/
public class CityQualificationServiceTest {
private CityQualificationService service;
@Before
public void setUp() {
CityQualificationRules rules = JQuickJava.create()
.createApi(CityQualificationRules.class, "demo/city-qualification.xml");
this.service = new CityQualificationService(rules);
}
/**
* 博士高层次人才走人才引进通道,30 岁社保 12 月 → 95 分核准落户
*/
@Test
public void shouldApproveTopTalentSettlement() {
CityQualificationApplication app = settleApplication("CQ2026001", "TALENT");
app.setAge(30);
app.setEducation("DOCTOR");
app.setSocialSecurityMonths(12);
app.setTalentTitle("TOP_TALENT");
CityQualificationDecision decision = service.evaluate(app);
Assert.assertEquals(95, decision.getPoints());
Assert.assertEquals("APPROVE", decision.getResult());
Assert.assertTrue(decision.getDetails().contains("人才称号积分: 30 / 30"));
Assert.assertTrue(decision.getReasons().contains(
"积分 95 分达到核准线(80 分),准予落户"));
}
/**
* 本科就业人员 40 岁社保 24 月无称号 → 50 分,不满足落户线
*/
@Test
public void shouldRejectLowPointsEmployeeSettlement() {
CityQualificationApplication app = settleApplication("CQ2026002", "EMPLOYEE");
app.setAge(40);
app.setEducation("BACHELOR");
app.setSocialSecurityMonths(24);
app.setTalentTitle("NONE");
CityQualificationDecision decision = service.evaluate(app);
Assert.assertEquals(50, decision.getPoints());
Assert.assertEquals("REJECT", decision.getResult());
Assert.assertTrue(decision.getReasons().contains("积分 50 分未达落户标准(60 分)"));
}
/**
* 硕士企业主 42 岁,企业 3 年纳税 80 万 → 60 分恰入复核区间
*/
@Test
public void shouldReviewInvestmentSettlementAtThreshold() {
CityQualificationApplication app = settleApplication("CQ2026003", "INVESTMENT");
app.setAge(42);
app.setEducation("MASTER");
app.setSocialSecurityMonths(12);
app.setTalentTitle("NONE");
app.setCompanyYears(3);
app.setEnterpriseTaxWan(80.0);
CityQualificationDecision decision = service.evaluate(app);
Assert.assertEquals(60, decision.getPoints());
Assert.assertEquals("REVIEW", decision.getResult());
Assert.assertTrue(decision.getDetails().contains("投资纳税积分: 10 / 20"));
Assert.assertTrue(decision.getReasons().contains(
"积分 60 分处于复核公示区间(60-79 分),转人工复核"));
}
/**
* 通道门槛核验与失信一票否决
*/
@Test
public void shouldRejectChannelThresholdAndBadCredit() {
CityQualificationApplication shortSocialSecurity = settleApplication("CQ2026004", "EMPLOYEE");
shortSocialSecurity.setAge(30);
shortSocialSecurity.setEducation("MASTER");
shortSocialSecurity.setSocialSecurityMonths(3);
CityQualificationDecision d1 = service.evaluate(shortSocialSecurity);
Assert.assertEquals("REJECT", d1.getResult());
Assert.assertTrue(d1.getReasons().contains(
"就业落户人员在本市连续缴纳社保不满 6 个月,不符合通道基本条件"));
CityQualificationApplication badCredit = settleApplication("CQ2026005", "TALENT");
badCredit.setAge(32);
badCredit.setEducation("DOCTOR");
badCredit.setSocialSecurityMonths(48);
badCredit.setTalentTitle("TOP_TALENT");
badCredit.setBadCreditRecord(true);
CityQualificationDecision d2 = service.evaluate(badCredit);
Assert.assertEquals("REJECT", d2.getResult());
Assert.assertTrue(d2.getReasons().contains("申请人存在严重失信记录,触发一票否决"));
CityQualificationApplication noTitle = settleApplication("CQ2026006", "TALENT");
noTitle.setAge(28);
noTitle.setEducation("DOCTOR");
noTitle.setSocialSecurityMonths(36);
noTitle.setTalentTitle("NONE");
CityQualificationDecision d3 = service.evaluate(noTitle);
Assert.assertEquals("REJECT", d3.getResult());
Assert.assertTrue(d3.getReasons().contains("人才引进通道须持有高层次或紧缺人才认定证明"));
}
/**
* 购房资格:社保满 36 月无房通过;已有房产被限购;缴纳不足被拒
*/
@Test
public void shouldCheckHousePurchaseEligibilityAndLimit() {
CityQualificationApplication eligible = houseApplication("CQ2026007");
eligible.setSocialSecurityMonths(40);
CityQualificationDecision d1 = service.evaluate(eligible);
Assert.assertEquals("ALLOW", d1.getResult());
Assert.assertTrue(d1.getReasons().contains("满足非本市户籍家庭购房缴纳年限要求,具备购房资格"));
CityQualificationApplication ownsProperty = houseApplication("CQ2026008");
ownsProperty.setSocialSecurityMonths(60);
ownsProperty.setHasPropertyInCity(true);
CityQualificationDecision d2 = service.evaluate(ownsProperty);
Assert.assertEquals("REJECT", d2.getResult());
Assert.assertTrue(d2.getReasons().contains(
"非本市户籍家庭在本市已拥有 1 套住房,受限购政策约束"));
CityQualificationApplication insufficient = houseApplication("CQ2026009");
insufficient.setSocialSecurityMonths(12);
CityQualificationDecision d3 = service.evaluate(insufficient);
Assert.assertEquals("REJECT", d3.getResult());
Assert.assertTrue(d3.getReasons().contains(
"社保或个税连续缴纳不满 36 个月,且不符合人才放宽条件"));
}
/**
* 购车指标:普通通道 24 月通过、12 月进入摇号;紧缺人才 8 月走快速通道
*/
@Test
public void shouldCheckCarQuotaForNormalAndTalentChannels() {
CityQualificationApplication normal = carApplication("CQ2026010");
normal.setSocialSecurityMonths(24);
Assert.assertEquals("ALLOW", service.evaluate(normal).getResult());
CityQualificationApplication tooShort = carApplication("CQ2026011");
tooShort.setSocialSecurityMonths(12);
CityQualificationDecision d2 = service.evaluate(tooShort);
Assert.assertEquals("REJECT", d2.getResult());
Assert.assertTrue(d2.getReasons().contains("不满足新能源指标直接申请条件,需参与普通指标摇号"));
CityQualificationApplication elite = carApplication("CQ2026012");
elite.setSocialSecurityMonths(8);
elite.setTalentTitle("ELITE");
Assert.assertEquals("ALLOW", service.evaluate(elite).getResult());
CityQualificationApplication alreadyHasCar = carApplication("CQ2026013");
alreadyHasCar.setSocialSecurityMonths(36);
alreadyHasCar.setHasCarInCity(true);
CityQualificationDecision d4 = service.evaluate(alreadyHasCar);
Assert.assertEquals("REJECT", d4.getResult());
Assert.assertTrue(d4.getReasons().contains("申请人名下已登记本市小客车,不能重复申领新能源指标"));
}
private CityQualificationApplication settleApplication(String id, String channel) {
CityQualificationApplication app = new CityQualificationApplication();
app.setApplicationId(id);
app.setApplicantName("申请人");
app.setBenefit("SETTLE");
app.setChannel(channel);
app.setAge(30);
app.setEducation("OTHER");
app.setSocialSecurityMonths(0);
app.setTaxPaymentMonths(0);
app.setHasPropertyInCity(false);
app.setHasCarInCity(false);
app.setCompanyYears(0);
app.setEnterpriseTaxWan(0.0);
app.setTalentTitle("NONE");
app.setBadCreditRecord(false);
return app;
}
private CityQualificationApplication houseApplication(String id) {
CityQualificationApplication app = settleApplication(id, "EMPLOYEE");
app.setBenefit("HOUSE_PURCHASE");
return app;
}
private CityQualificationApplication carApplication(String id) {
CityQualificationApplication app = settleApplication(id, "EMPLOYEE");
app.setBenefit("CAR_PURCHASE");
return app;
}
}

Run from the project root:

Terminal window
mvn test -Dtest=CityQualificationServiceTest
Test method Business scenario constructed Key assertions / expected decision
shouldApproveTopTalentSettlement Doctorate, age 30, 12 months of social security, TOP_TALENT via the talent channel 95 points APPROVE; details contain “人才称号积分: 30 / 30”; reasons contain “积分 95 分达到核准线(80 分),准予落户”
shouldRejectLowPointsEmployeeSettlement Bachelor’s degree, age 40, 24 months of social security, no title, employment channel 50 points REJECT; reasons contain “积分 50 分未达落户标准(60 分)”
shouldReviewInvestmentSettlementAtThreshold Master’s business owner, age 42, 12 months of social security, 3-year-old company, 800k yuan tax paid, investment channel 60 points, exactly in the review range (REVIEW); details contain “投资纳税积分: 10 / 20”; reasons contain “60-79 分,转人工复核”
shouldRejectChannelThresholdAndBadCredit Three sub-scenarios: master’s employment with only 3 months of social security; doctorate talent with 48 months but a dishonesty record; doctorate talent with 36 months but no title All REJECT; hitting respectively the 6-month employment channel threshold, the “严重失信记录,触发一票否决” veto, and the talent channel certification threshold
shouldCheckHousePurchaseEligibilityAndLimit Three sub-scenarios: 40 months of social security with no home; 60 months but already owns a home; 12 months ALLOW / REJECT (purchase restriction) / REJECT (less than 36 months and not talent) respectively
shouldCheckCarQuotaForNormalAndTalentChannels Four sub-scenarios: 24 months standard; 12 months standard; ELITE with 8 months; 36 months but already owns a car ALLOW / REJECT (join the lottery) / ALLOW (talent relaxed to 6 months) / REJECT (no repeat applications) respectively
  • All policy thresholds externalized to XML: education scores, social security tiers, age brackets, tax brackets, the 80/60 tier lines, and the 36/24/6/3-month thresholds all live in the script; a city policy revision only requires adjusting the XML and rebuilding the proxy, with no release or renewed security assessment for the main government system.
  • One method per policy rule, independently tunable and auditable: the 8 rule methods each encapsulate a single policy rule; whether the investment tax score is counted is decided by the Service per channel, and the script has no notion of channels, keeping responsibilities cleanly separated.
  • Hard vetoes and channel thresholds short-circuit on the Java side: the dishonesty one-vote veto runs first at the unified entry; the three channel prerequisites, the purchase restriction, and the existing-car check all short-circuit before points or threshold evaluation, eliminating “high-score but non-compliant approvals”.
  • Boolean flag accumulation fits the script syntax: houseEligible / carEligible start with boolean eligible = false and progressively set it to true across the standard rule and the two talent tiers, closing the “either social security or tax” plus talent relaxation logic inside a single script method.
  • Decision details and policy reasons recorded per case: the five-dimension scores (with their maximum denominators), contribution months, and approval/review/rejection rules are assembled uniformly by the Service and can be printed directly on the service window notice, supporting administrative reconsideration and audits.
  • @Param name binding + ASM dynamic proxy: the 8 rule methods share a single proxy instance; Java 8 with no heavy dependencies, embeddable into legacy government case-handling systems and domestically produced JDK environments.